1 คะแนน โดย GN⁺ 5 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Conventional Commits พยายามใส่ความหมายให้ข้อความคอมมิตด้วยรูปแบบ <type>[optional scope]: <description> แต่กลับให้ความสำคัญกับประเภทของการเปลี่ยนแปลงก่อน และทำให้ข้อมูลที่จำเป็นต่อการไล่ดูย้อนหลังจริง ๆ ถูกเลื่อนไปไว้ด้านหลัง โดยปล่อยให้ขอบเขตเป็นเพียงตัวเลือก
  • ผู้มีส่วนร่วม ผู้ดีบัก และผู้รับมือเหตุขัดข้อง มักค้นหาใน commit log ว่าการเปลี่ยนแปลงแตะโค้ดส่วนใด และเพราะบั๊กสามารถเกิดได้จากการเปลี่ยนแปลงทุกประเภท scope จึงสำคัญกว่า type
  • ตัวอย่างอย่าง fix(compiler): prevent namespaced SVG <style> elements from being stripped ก็ทำให้รู้ได้จากคำอธิบายอยู่แล้วว่าเป็นการแก้บั๊ก และคอมมิตอย่าง refactor(core): Update webmcp support to use document.modelContext ก็อาจคร่อมทั้งการแก้ไข รีแฟกเตอร์ และเพิ่มฟีเจอร์ได้ในคอมมิตเดียว ทำให้ type ทั้งซ้ำซ้อนและจำกัดเกินไป
  • การสร้าง CHANGELOG อัตโนมัติและการตัดสินใจเพิ่มเลขเวอร์ชันแบบ semantic มีปัญหาเพราะผู้อ่าน commit log กับ changelog เป็นคนละกลุ่มกัน และผลลัพธ์อาจคลาดเคลื่อนได้จากการ revert การทำลายความเข้ากันได้ย้อนหลังโดยไม่ตั้งใจ หรือการแก้ปัญหาความเข้ากันไม่ได้ในภายหลัง
  • ข้อความคอมมิตแบบ scope prefix แสดงให้เห็นหัวข้อของการเปลี่ยนแปลงก่อน และเงื่อนไขการ build/deploy ก็ควรอิงจากไฟล์ที่เปลี่ยนผ่าน git diff มากกว่าการดู type ในชื่อเรื่อง

ลำดับความสำคัญที่ผิด

  • Conventional Commits มีเป้าหมายจะใส่ความหมายให้ข้อความคอมมิต เพื่อช่วยให้นักพัฒนาและผู้ใช้ปลายทางเข้าใจการเปลี่ยนแปลง
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
  • บรรทัดชื่อเรื่องประกอบด้วย <type> อย่าง fix, feat, chore, docs, refactor รวมถึง scope ที่เป็นตัวเลือก และ description
  • ข้อบกพร่องหลักคือโครงสร้างนี้ให้ความสำคัญกับ type ซึ่งเป็นชนิดของการเปลี่ยนแปลง มากกว่า scope ซึ่งเป็นตัวการหรือพื้นที่ที่ถูกเปลี่ยน
  • การที่ scope เป็นตัวเลือก ทำให้ข้อมูลที่สำคัญที่สุดในคอมมิตอาจหายไป และการวาง type ไว้ต้นชื่อเรื่องก็กลับหัวลำดับความสำคัญ

ทำไม scope สำคัญกว่า type

  • ผู้มีส่วนร่วมอ่าน commit log เพื่อดูการเปลี่ยนแปลงนับจากการมีส่วนร่วมครั้งล่าสุด ดูภาพรวมการไหลของโปรเจกต์ และหาคอมมิตที่อาจชนกับงานที่กำลังทำอยู่ระหว่าง pull หรือ rebase
  • ผู้ดีบักจะมองหาการเปลี่ยนแปลงที่ไปแตะพื้นที่ซึ่งเกี่ยวข้องกับคอมโพเนนต์ที่เกิดบั๊ก และเพราะบั๊กเกิดได้กับการเปลี่ยนแปลงทุก type ข้อมูล type จึงไม่ค่อยช่วยอะไร
  • ผู้รับมือเหตุขัดข้องจะไล่ดู commit log รอบช่วงเวลาที่เกิดเหตุเพื่อหาพื้นที่ที่น่าจะเป็นต้นเหตุ เช่น หากจุดที่ error ของ inbound API พุ่งขึ้นมีคอมมิต scope auth อยู่ ก็ย่อมเป็นผู้ต้องสงสัยที่มีน้ำหนัก
  • สำหรับคนที่อ่าน commit log ข้อมูลสำคัญไม่ใช่ว่าการเปลี่ยนแปลงนั้นเป็นประเภทไหน แต่คือมันไปแตะพื้นที่ใด

ความซ้ำซ้อนและข้อจำกัดของ type

  • fix(compiler): prevent namespaced SVG <style> elements from being stripped ทำให้รู้ได้จาก description อยู่แล้วว่าเป็นการแก้บั๊ก ดังนั้น type fix จึงซ้ำซ้อน
  • พื้นที่ในบรรทัดชื่อคอมมิตมีจำกัด การเสียอักขระไปกับ type ที่ดูออกได้จาก description อยู่แล้วจึงไม่ช่วยอะไร
  • refactor(core): Update webmcp support to use document.modelContext เป็นการอัปเดตฟีเจอร์ webmcp ในคอมโพเนนต์ core ให้รองรับทั้ง document.modelContext และ navigator.modelContext
  • การเปลี่ยนแปลงนี้อาจถูกมองได้พร้อมกันว่าเป็นการแก้บั๊ก รีแฟกเตอร์ และเพิ่มฟีเจอร์ใหม่ แต่ข้อมูลที่สำคัญจริง ๆ คือมันเป็นการเปลี่ยนแปลงในคอมโพเนนต์ core/webmcp

ข้อจำกัดของคำมั่นเรื่องระบบอัตโนมัติ

  • แนวคิดการใช้เครื่องมืออย่าง git-cliff หรือ conventional-changelog เพื่อสร้าง CHANGELOG อัตโนมัติจากคอมมิต มีปัญหาที่ผู้อ่าน commit log กับ changelog เป็นคนละกลุ่มกัน
  • CHANGELOG มีไว้สำหรับผู้ใช้ โดยเน้นให้เข้าใจความแตกต่างเชิงฟังก์ชันและเชิงธุรกิจระหว่างเวอร์ชัน
  • commit log มีไว้สำหรับนักพัฒนา โดยเน้นให้อ่านการเปลี่ยนแปลงของ codebase ตามเวลาและความเคลื่อนไหวในมุมมองของ scope
  • ในโปรเจกต์ที่มีความซับซ้อนระดับกลางขึ้นไป ฟีเจอร์ที่มีความหมายหนึ่งอย่างมักถูกส่งเข้ามาผ่านหลายคอมมิต โดยสำหรับนักพัฒนา ขั้นตอนการพัฒนามีประโยชน์ แต่สำหรับผู้ใช้ปลายทาง สิ่งสำคัญมีเพียงฟีเจอร์ใหม่ที่ได้สุดท้าย
  • คอมมิตแบบ revert สำคัญต่อการไหลของ commit log สำหรับนักพัฒนา แต่สำหรับผู้ใช้ปลายทาง การเปลี่ยนแปลงที่ถูกย้อนกลับก็ไม่ต่างจากการไม่เคยเกิดขึ้นเลย
  • การเพิ่มเลขเวอร์ชันแบบ semantic โดยอิงจาก commit type อาจทำให้มีการเพิ่ม major ทั้งที่การเปลี่ยนแปลงที่ทำลายความเข้ากันได้ย้อนหลังถูก revert ไปแล้ว หรือเพิ่ม minor/patch ผิดเพราะเพิ่งมารู้ทีหลังว่ามันทำให้เข้ากันไม่ได้ หรือแม้แต่ตัดสินว่าเป็น breaking change ทั้งที่เมื่อรวมกับคอมมิตถัดมาแล้วปัญหานั้นหายไป
  • แม้จะสามารถแก้ประวัติด้วย rebase ในสถานการณ์แบบนี้ได้ แต่ workflow อาจห้ามหรือทำให้วิธีนี้พังได้ และยังลดความน่าเชื่อถือของลำดับเรื่องราวที่ commit log พยายามสื่อ
  • หากใช้ type ในชื่อคอมมิตเป็นตัว trigger กระบวนการ build/deploy ก็อาจถูกหลบเลี่ยงโดยคอมมิตอย่าง docs: fix typos ที่จริง ๆ แล้วแอบใส่ช่องโหว่ลงในระบบยืนยันตัวตน
  • เงื่อนไขของ build/deploy ควรถูกกำหนดจากการระบุไฟล์ที่เปลี่ยนผ่าน git diff มากกว่าดูชื่อคอมมิต

ปัญหาในการใช้งานและทางเลือก

  • Conventional Commits เปิดให้แต่ละโปรเจกต์กำหนดชุด type ของตัวเอง แต่หลายโปรเจกต์กลับใช้ type เริ่มต้นของ commitlint แบบยกชุด ซึ่งอาจไม่เข้ากับลักษณะเฉพาะของโปรเจกต์นั้น
  • ข้อกำหนดของ Conventional Commits นิยามไว้ทางเทคนิคจริง ๆ แค่ fix และ feat ส่วน type อื่นปล่อยให้แต่ละโปรเจกต์ตัดสินใจเอง
  • ในสภาพแวดล้อมองค์กร บางครั้งข้อกำหนดด้านการจัดการการเปลี่ยนแปลงและการตรวจสอบบังคับให้ใส่หมายเลข ticket ในทุกข้อความคอมมิต และหากใช้ <scope> เป็นตำแหน่งของหมายเลข ticket ก็จะทำให้ metadata ที่มีประโยชน์หายไป
  • Linux, FreeBSD, Git, Go, NixOS, Node.js ใช้ข้อความคอมมิตแบบ scope prefix ที่เหมาะกับโปรเจกต์ของตน
โปรเจกต์ รูปแบบ ตัวอย่าง
Linux subsystem: description i2c: virtio: mark device ready before registering the adapter
FreeBSD prefix: Description linuxulator: Return EINVAL for invalid inotify flags
Git area: description gitlab-ci: update macOS image
Go package: description net/http/cookiejar: add godoc links
nixpkgs pkg-name: description xwayland: 24.1.11 -> 24.1.12
Node.js subsystem: description stream: fast-path stateless transform flush results
  • ใน Linux kernel สิ่งที่เป็น scope ตามธรรมชาติคือ subsystem, ในโปรเจกต์ Go คือ package path, และในสถาปัตยกรรมแบบ microservices ก็คือชื่อ microservice
  • scopedcommits.com เสนอให้กลับไปใช้รูปแบบข้อความคอมมิตที่ยึด scope เป็นศูนย์กลาง และแยกการสร้าง CHANGELOG ออกจากการจัดการ commit log
  • ข้อดีของ Conventional Commits ไม่ได้นำไปสู่ประโยชน์จริงอย่างที่คาด และความนิยมในโปรเจกต์โอเพนซอร์ส รวมถึงแนวโน้มที่ AI มักเลือกใช้เป็นค่าเริ่มต้น ก็ยิ่งทำให้ข้อความคอมมิตที่ปะปน anti-pattern แพร่กระจายออกไป

1 ความคิดเห็น

 
GN⁺ 5 시간 전
ความคิดเห็นจาก Lobste.rs
  • ดีใจที่ได้เห็นบทความที่สรุปข้อโต้แย้งต่อ conventional commits ด้วยเหตุผล ไม่ใช่แค่ความรู้สึกต่อต้านแบบสัญชาตญาณ
    ที่ผ่านมาก็ไม่ได้คิดลึกนักว่าทำไมถึงไม่ชอบ และเคยสงสัยว่าอาจเป็นเพราะมันถูกโยงกับโค้ดที่ LLM สร้างขึ้น โดยเฉพาะ chore: ที่เกลียดที่สุด อยากให้เลิกประดิษฐ์ Hungarian notation ขึ้นมาใหม่ตั้งแต่แรก มันไม่ควรถูกสร้างขึ้นมาเลย

    • โดยเฉพาะ chore: ตอนนี้ก็ไม่มีอยู่ใน Angular commit style guide แล้ว และดูเหมือนพวกเขาจะตระหนักได้ว่ามันกำกวมเกินไปเลยถูกรวมเข้าไปใน build:
      แม้แต่ตอนที่ยังอยู่ในสไตล์ของ Angular คำอธิบายของ chore: ก็ระบุการใช้งานค่อนข้างเฉพาะเจาะจง แต่ในบางโปรเจ็กต์โอเพนซอร์สดูเหมือนจะถูกแปะตามบรรยากาศกับงานที่เจ้าตัวขี้เกียจทำเอามาก ๆ
  • ฉันไม่ได้ชอบ conventional commits แต่ดูเหมือนทางเลือกที่เสนอจะมองข้ามเหตุผลที่ว่า scope เป็นตัวเลือกเสริม
    ในโปรเจ็กต์เล็กที่ไม่ได้มีโมดูลชัดเจนหลายส่วน แนวคิดเรื่อง “scope” ก็ไม่ได้มีประโยชน์มากนัก อีกแนวปฏิบัติที่มีประโยชน์และทั้งสองฝั่งไม่ได้พูดถึงคือการใส่เลข issue หรือตั๋วไว้ในหัวข้อคอมมิต เพราะช่วยให้เข้าใจบริบทเพิ่มเติมของการเปลี่ยนแปลงได้ง่ายขึ้น และช่วยมากเป็นพิเศษตอนรีวิวโค้ด แต่ก็ไม่ชอบถ้าบังคับให้เลขตั๋วเป็นข้อกำหนด เพราะจะทำให้เกิดตั๋วไร้ประโยชน์เต็มไปหมดแม้เป็นการเปลี่ยนเล็กน้อย ถ้าเป็นการแก้บั๊กหรือทำงานเฉพาะอย่าง ก็ควรเชื่อมโยงกับบั๊กหรืองานนั้น

    • ถ้าไม่ต้องใช้ scope ก็แค่ละไว้ได้
      ยังดีกว่า “type” ของคอมมิตที่ซ้ำซ้อนซึ่งควรจะเห็นอยู่แล้วจากแค่บรรทัดหัวข้อ
    • ถ้าจะให้ดีจริง ๆ ฉันมองว่าไม่ควรมี รูปแบบคอมมิตที่บังคับตายตัว เลย และควรใช้ถ้อยคำที่เหมาะกับคอมมิตนั้น ๆ
      ถ้าการเปลี่ยนแปลงสัมพันธ์กับตั๋วอย่างชัดเจน ก็ใช้คอมมิตแบบ “เลขตั๋ว” ไป ถ้าไม่ใช่ก็ใช้วิธีอื่น การเปลี่ยนบางอย่างเข้ากับ type ได้ดีแต่ไม่ค่อยเข้ากับ scope และบางอย่างก็ตรงกันข้าม ดังนั้นจะผสม scoped commits กับ conventional commits ก็ได้
  • อยากจะพูดว่า “อย่าใช้ ฟอนต์ความกว้างคงที่ กับข้อความที่เป็นย่อหน้า”
    ถึงอย่างนั้นโดยรวมก็เห็นด้วยกับสมมติฐานของบทความ

  • ถึงข้อความคอมมิตจะไม่ค่อยดี ก็แนะนำให้ลองใช้ git log --name-only หรือ git log --stat บ่อย ๆ เพื่อจับภาพคร่าว ๆ ของขอบเขตการเปลี่ยนแปลง
    แค่ดูชื่อไฟล์ก็ช่วยได้มากพอสมควรว่ามีอะไรเปลี่ยนไปบ้าง โดยไม่ต้องเปิดดูทุกคอมมิต

  • วิธีที่ชอบมากจริง ๆ คือ บังคับใช้สไตล์ conventional commit กับชื่อ PR
    ชื่อ PR ยังให้ maintainer แก้ไขได้หลัง merge โดยไม่ต้องเขียนประวัติคอมมิตใหม่ และถ้าใช้ร่วมกับเครื่องมืออย่าง release-drafter ก็สามารถทำ changelog ที่มีความหมายใน GitHub release ได้แบบอัตโนมัติ มันให้ระดับรายละเอียดที่เหมาะกับผู้มีส่วนได้ส่วนเสียตามที่ผู้เขียนพูดถึง คือแยกให้เห็นฟีเจอร์ การแก้ไข และการเปลี่ยนแบบไม่เข้ากันย้อนหลัง พร้อมทั้งจัดการ semver ที่สมเหตุสมผลสำหรับร่าง GitHub release ถัดไปโดยอัตโนมัติด้วย
    ข้อสังเกตของบทความที่ว่าองค์ประกอบอย่าง parse-lib ไม่ควรเป็นตัวเลือกเสริมนั้นถูกต้อง และฉันก็เห็นด้วยว่าการบังคับ conventional commits ทำให้ผู้มีส่วนร่วมหน้าใหม่ถอยหนีได้ แต่ทางเลือกอื่นก็ไม่ได้ดีกว่าอย่างชัดเจน
    ถึงอย่างนั้นตัวระบุ breaking change แบบ fix!(parse-lib): Don't leave sparse holes when parsing JSON arrays ก็ให้ข้อมูลค่อนข้างมาก มันบอกว่าเป็นการแก้บั๊กในคอมโพเนนต์เฉพาะ และการแก้นั้นมี breaking change ที่หลีกเลี่ยงไม่ได้ตามมาด้วย รวมถึงสื่อความหมายประมาณการเพิ่ม semver แบบ minor ด้วย ของแบบนี้เอาไปใช้เป็นชื่อ PR ได้

  • ยอมรับว่าเคยอินกับ conventional commits มากเกินไปในฐานะวิธีส่งเสริมวินัยในการคอมมิต และสุดท้ายมันก็กลายเป็นความเคยชิน
    ตอนนี้กลับรู้สึกว่ามันทั้งจำกัดและค่อนข้างตามอำเภอใจ ในบางโปรเจ็กต์ฉันเองก็ไม่แน่ใจด้วยซ้ำว่านั่นเป็นธรรมเนียมจริงหรือเปล่า และเลยขยับไปใกล้สไตล์แบบ Linux/Go/Node มากขึ้น ส่วนใน monorepo ที่มีการตั้งค่าหลากหลาย การเขียนแบบ [service]: [what changed] ให้ความรู้สึกเป็นธรรมชาติกว่าการฝืนสร้าง type ขึ้นมา ต่อจากนี้คิดว่าจะลองทดลองกับสไตล์คอมมิตส่วนตัวให้มากขึ้น โดยดูจากว่าอะไรมีประโยชน์จริง แทนที่จะพยายามให้เข้ากับธรรมเนียมที่เข้มงวด และ scoped commits ก็ดูเป็นจุดเริ่มต้นที่ดี

  • chore(lobsters): add my 2 cents on conventionals commits [JIRA-69420]
    เห็นด้วยแทบทั้งหมด แต่มีจุดหนึ่งที่มองต่างออกไปคือส่วนที่บอกว่า “มันแสดงบันทึกแบบแก้ประวัติย้อนหลังให้ผู้มีส่วนร่วมเห็น ซึ่งลดความน่าเชื่อถือของเรื่องราวที่ commit log เล่า” ผู้เขียนน่าจะพูดถึง public branch เป็นหลัก ซึ่งถ้าเป็น public branch ก็เป็นคำแนะนำที่สมเหตุสมผล แต่ไม่ควรเอาไปใช้กับ private branch สิ่งสำคัญคือทำให้คนที่รีวิวการเปลี่ยนแปลงสุดท้ายเข้าใจง่าย ไม่ว่าจะเป็น maintainer หรือฉันเองในอีก 10 ปี ไม่จำเป็นต้องเก็บร่องรอยความคิดที่ไม่ต่อเนื่องกัน หรือแย่กว่านั้นคือกองคอมมิต address review ไว้

  • คำตอบของคำถาม “ทำไม scope ถึงเป็นตัวเลือกเสริม?” ก็คือในโปรเจ็กต์เล็ก ๆ ทั้งโปรเจ็กต์ก็คือ scope อยู่แล้ว
    ฉันเห็นด้วยว่า “type” ของคอมมิตไม่ได้มีประโยชน์มากนัก แต่ก็ยังไม่ค่อยแน่ใจว่าระหว่าง scoped commits กับ conventional commits ต่างกันมากตรงไหน scoped ก็เป็น conventional ที่ตัด “type” ออกไปเท่านั้นเอง และการแยก fix, feat, refactor, chore ก็ถือว่าเป็นการแยกที่ใช้ได้
    ถ้าทุกคนแค่หยิบค่าเริ่มต้นของ commitlint มาใช้ตามเดิม บางทีเรื่องนี้ก็อาจแค่ต้องทำให้คนใช้งานมันเก่งขึ้นไม่ใช่หรือ