- 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
ข้อจำกัดของคำมั่นเรื่องระบบอัตโนมัติ
- แนวคิดการใช้เครื่องมืออย่าง 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 kernel สิ่งที่เป็น scope ตามธรรมชาติคือ subsystem, ในโปรเจกต์ Go คือ package path, และในสถาปัตยกรรมแบบ microservices ก็คือชื่อ microservice
- scopedcommits.com เสนอให้กลับไปใช้รูปแบบข้อความคอมมิตที่ยึด scope เป็นศูนย์กลาง และแยกการสร้าง CHANGELOG ออกจากการจัดการ commit log
- ข้อดีของ Conventional Commits ไม่ได้นำไปสู่ประโยชน์จริงอย่างที่คาด และความนิยมในโปรเจกต์โอเพนซอร์ส รวมถึงแนวโน้มที่ AI มักเลือกใช้เป็นค่าเริ่มต้น ก็ยิ่งทำให้ข้อความคอมมิตที่ปะปน anti-pattern แพร่กระจายออกไป
2 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ดูเหมือนว่าโปรแกรมเมอร์มักจะถกเถียงและบ่นกันเสมอแม้กระทั่งเรื่องเล็กน้อยอย่างแท็บกับสเปซว่าควรตั้งค่าแบบไหนถึงจะ เหมาะสมที่สุด
ไม่ได้หมายความว่า Conventional Commits คือวิธีที่ดีที่สุดราวกับเป็นบัญชาจากพระเจ้าในการเขียน commit message แต่คิดว่าการมี โครงสร้างที่กำหนดไว้ และการทำให้ความคาดหวังต่อ commit message ตรงกันนั้นมีประสิทธิภาพและสำคัญกว่ามาก
ผู้เขียนให้ความสำคัญมากว่าขอบเขตสำคัญกว่าประเภท แต่ผมคิดว่าความต่างระหว่าง
fix(compiler)กับcompiler fixไม่ใช่ประเด็นที่ต้องจริงจังถึงขนาดนั้นในวงการเทคโนโลยีมีหลายอย่างที่กลายเป็นมาตรฐานทั้งที่อาจไม่ดีที่สุด เช่น JSON เอง ถ้าจะสร้างใหม่ตั้งแต่ต้น หลายคนก็คงเห็นว่าควรรองรับคอมเมนต์ รูปแบบตัวเลขที่ชัดเจนกว่า ฯลฯ
ถึงอย่างนั้นมันก็กลายเป็นมาตรฐานเพราะดีกว่าสิ่งก่อนหน้าในหลายบริบท และถึงจะมีรูปแบบที่ดีกว่า Conventional Commits อยู่บ้าง แต่ก็ดูยังไม่ดีกว่ามากพอที่จะคุ้มกับการสร้างแนวทางแข่งขันใหม่สำหรับโครงสร้างของ commit message
commit message จะมีโครงสร้างหลวม ๆ ก็ยังยอดเยี่ยมได้ถ้าสื่อธรรมชาติของการเปลี่ยนแปลงได้ดี ในทางกลับกัน ต่อให้มีโครงสร้างมากก็ยังอาจสับสนหรือไม่มีข้อมูลเลยก็ได้
โดยรวมแล้วเห็นด้วยกับผู้เขียนว่า Conventional Commits ไม่ได้แก้ปัญหาแกนหลักซึ่งก็คือ commit message ที่แย่
XML ก็ถือว่าดีพอและเป็นมาตรฐาน, SOAP ก็ถือว่าดีพอและเป็นมาตรฐาน
ประเด็นคือ Conventional Commits นั้นดีพอและเป็นมาตรฐานมากพอจนไม่คุ้มจะไปดูโครงสร้างอื่น แต่คำว่า “คุ้ม” นั้นเป็นเรื่องอัตวิสัย
ถ้าคุณ commit และอ่าน PR ทุกวัน แรงเสียดทานเล็ก ๆ ที่รูปแบบ Conventional Commits สร้างขึ้นก็อาจสะสมได้ และหากไม่มองมันเป็นกฎธรรมชาติ การเปิดทางเลือกอื่นไว้ก็ช่วยทีมที่ชอบแนวนั้นได้
ยังไงเสียทีมส่วนใหญ่ก็ไม่ได้สร้าง changelog กันอยู่แล้ว
จริงอยู่ว่าขอบเขตสำคัญ แต่ก็เหมือนเป็นสิ่งที่อนุมานได้จากเนื้อหา commit
เวลาตรวจ diff การดู path ที่ถูกแตะต้องเป็น sanity check ที่สำคัญ และ diff ของ “test” ไม่ควรไปแก้โค้ด authentication ฝั่ง production
ถึงอย่างนั้นถ้าจะให้แสดงใน
--onelineผมก็คิดว่าfeat(auth):ดีกว่าfeat:ไม่เห็นด้วยกับคำกล่าวที่ว่าผู้อ่านเป้าหมายเป็นฝ่ายผิด
commit แบบ
featควรอธิบายการเปลี่ยนแปลงจากมุมมองของผลิตภัณฑ์จริง ๆ และควรจัดระเบียบโดยสะสมการเปลี่ยนแปลง refactor ที่ไม่มีความหมายในตัวมันเองไว้ก่อน แล้วค่อยวางการเปลี่ยนแปลงฟีเจอร์ใหม่เล็ก ๆ ทับไว้ด้านบนนี่ก็เป็นสิ่งที่มีประโยชน์ที่สุดที่จะใส่ไว้ในคำอธิบาย diff ส่วนบริบททางเทคนิคอย่าง “ทำไมถึงเลือกอัลกอริทึม X” ควรใส่ไว้ในคอมเมนต์หรือ
DECISIONS.mdเพื่อไม่ให้หายไปในบริษัทที่เคลื่อนตัวเร็ว คงมีแต่คนที่หมกมุ่นเท่านั้นที่มาคอยใส่ใจงานน่าเบื่อแบบนี้ในประวัติ commit แต่สำหรับโครงการโอเพนซอร์ส ผมคิดว่าการซ่อนบริบทไว้ใน commit message สำคัญกว่ามาก
มีเหตุผลที่เรามีรูปแบบอย่าง Markdown และ plain text ไม่ได้มีแค่ JSON อย่างเดียว
เคยรีวิว commit มากเกินไปแล้วที่หัวข้อเขียนว่า
small fixแต่ความจริงไม่ใช่การแก้เล็กน้อยเลยข้อสรุปจริงๆ คือแต่ละโปรเจ็กต์มีความต้องการต่างกัน
ตลอดเวลากว่า 30 ปีที่ใช้การจัดการซอร์สมา ไม่เคยมีครั้งไหนเลยที่การใส่คอมโพเนนต์ไว้ในคำอธิบายด้วยรูปแบบมาตรฐาน (ในบทความเรียกว่า scope) จะเป็นงานที่มีประโยชน์
แค่ดูว่าไฟล์ที่ได้รับผลกระทบอยู่ตรงไหนใน source tree ก็ชัดเจนแล้วว่าคอมโพเนนต์ไหนเปลี่ยนไป และ
bug,fix,featureก็ไม่ได้เพิ่มคุณค่าอะไรที่มีประโยชน์ถ้าไม่สำคัญก็คงไม่ถูกเช็กอิน
สิ่งเดียวที่รู้สึกว่ามีประโยชน์คือ ลิงก์หรือ ID ของคำขอเปลี่ยนแปลงที่เกี่ยวข้อง ซึ่งในบทความไม่ได้พูดถึงเลย
ใน commit มีข้อมูลอยู่แล้วว่าอะไรเปลี่ยนไป แต่สิ่งที่ขาดคือบริบทว่าทำไมถึงเปลี่ยน
แม้แต่ในโปรเจ็กต์ส่วนตัวก็ยังใส่ reference ของ JIRA ไว้ในวงเล็บเหลี่ยมหน้าคำอธิบาย และถึงจะเป็นเรื่องที่บังเอิญตัดสินใจแก้ระหว่างพัฒนา ก็จะสร้าง JIRA แบบสั้นๆ 1 บรรทัดเพื่อเอา ID และเขียนเหตุผลไว้ตรงนั้น
การจับให้ได้ว่า “ทำไม” คือจุดประสงค์ทั้งหมดของข้อความนั้น และการแปะลิงก์ไปยัง resource ภายนอกที่สักวันอาจหายไป ไม่ใช่ตัวแทนที่ดี
ถ้าเป็น GitHub issue ก็สามารถย้อนจาก commit ไปยังการคุยใน PR ได้ และใน PR ก็ควรมี issue ที่เชื่อมไว้รวมถึง pointer อื่นๆ
แน่นอนว่าเราทิ้ง JIRA ไปเกือบหมดตอนย้ายมาใช้ GitHub issue และอีกไม่กี่ปีต่อมาอินสแตนซ์นั้นก็ถูกปิดและลบไป
ตอนนี้แท็ก JIRA เหล่านั้นก็ไร้ประโยชน์ทั้งหมด
เพราะงั้นผมเลยมองว่ากลับกันแล้ว ควรมีการผูกกันอย่างแน่นแฟ้นระหว่าง issue tracker กับ git repository
สิ่งที่ต้องการจริงๆ คือความสามารถในการย้ายไปที่อื่นได้ แต่ก็ไม่รู้ว่าจะได้สิ่งนั้นมาอย่างไรถ้าไม่มีการผูกกันแน่นๆ
ในอุดมคติควรมีรูปแบบมาตรฐานแบบเปิด แต่ในโลกความเป็นจริง GitHub คือกอริลลายักษ์ที่กำหนดรูปแบบ และถ้า clone อย่าง GitLab ฯลฯ นำเข้า metadata ของโปรเจ็กต์บน GitHub หรืออย่างน้อย PR ได้ ก็แทบจะเทียบเท่ากับสิ่งนั้นในทางปฏิบัติ
ไม่ว่าอย่างไร การมีนโยบายทิ้ง pointer แบบตายตัวไปยังผลิตภัณฑ์ของ Atlassian ที่อีก 5 ปีข้างหน้าอาจไม่ได้ใช้แล้ว ก็ไม่ใช่เรื่องดี
ผมยอมรับนโยบายที่ว่า git commit ควรยืนได้ด้วยตัวเองอย่างสมบูรณ์ และควรหลอมรวมข้อมูลทั้งหมดเกี่ยวกับ “ทำไม” ของการเปลี่ยนแปลงไว้ใน commit message หรือคอมเมนต์ในซอร์สแทน
แต่ถึงอย่างนั้นผมก็คิดว่าวิธีนี้ก็ยังล้มเหลวอยู่ดี เพราะคนมักเขียนใน git commit สั้นเกินไป ทำข้อมูลหายไประหว่างสรุป issue และบทสนทนาโต้ตอบไปมาใน PR ก็มีประโยชน์เพราะมีมากกว่าการสรุปเหตุผลของการเปลี่ยนแปลงด้วยเสียงของคนคนเดียว
ถ้าจัดกลุ่มฟีเจอร์ใหม่ไว้ก่อน แล้วค่อยตามด้วยการแก้บั๊ก ผู้ใช้ที่ไม่ใช่สายเทคนิคจะอ่านง่ายขึ้นนิดหน่อย
commit message ไม่ได้มีไว้เพื่อสร้าง changelog แต่มีไว้เพื่อ นักพัฒนาในอนาคต
ช่วงเวลาหลักที่นักพัฒนาคนนั้นจะอ่าน commit message คือเวลาที่ไม่เข้าใจว่าทำไม commit นั้นถึงมีอยู่
เป็นสถานการณ์ที่สงสัยว่าไม่ใช่ว่าแก้อะไรไป แต่บรรทัดนั้นมีจุดประสงค์อะไร
ดังนั้นจึงรัน
blameเพื่อดู commit แล้วพบว่านักพัฒนาต้นฉบับออกจากบริษัทไปแล้ว JIRA เก่าก็อาจหายไปแล้ว และเบาะแสเดียวที่เหลืออยู่คือ commit messagehttps://dev.to/splix/the-why-behind-the-code-2bb1
ใส่บริบทไว้ในเนื้อความของ commit ไม่ได้หรือ?
คำว่า
choreของหลายคนที่ใช้ Conventional Commits ทำให้รู้สึกรำคาญมาตลอดส่วนตัวแล้วผมชอบหัวข้อ commit แบบ สไตล์ linux kernel มากกว่า ซึ่งก็โชคดีที่มีการพูดถึงไว้ในนี้เหมือนกัน
[0] https://www.kernel.org/doc/html/v7.0/process/submitting-patc...
ทัศนคติที่
choreสื่อออกมานั้นชวนต่อต้านมากมันให้ความรู้สึกเหมือนอย่างอื่นทั้งหมดต้องถูกติดป้ายว่า
funหรือindifferentด้วย และ การตัดสินเชิงอารมณ์ แบบนั้นไม่ควรอยู่ใน commit messageupkeepความหมายเหมือนกัน แต่ไม่มีนัยดูแคลน
แถมยังเหมือนแกล้งทำเป็นรู้ผลกระทบโดยรวมของ commit ล่วงหน้า ทั้งที่จริงๆ ไม่รู้
พอมี Conventional Commits ทั้งเพื่อนร่วมทีมและ LLM ก็ต้องเสียเวลาและเปลืองโทเคนไปกับการตั้งชื่อแบบงี่เง่านั้น
ความไม่พอใจหลักต่อ Conventional Commits คือมันไม่ใส่ หมายเลข issue ไว้ในหัวข้อ commit
แม้แต่ในเอกสารมาตรฐานก็ยังไม่ได้พูดถึงในฐานะตัวเลือกด้วยซ้ำ
สำหรับผม นี่แทบเป็นข้อมูลที่สำคัญที่สุดใน commit message
ตลอด 15 ปีที่ผ่านมา ผมไม่รู้เลยว่ามีกี่ครั้งที่ต้องไล่เทียบคำอธิบาย issue ที่ commit เก่าอ้างถึง เพื่อทำความเข้าใจบริบททั้งหมดของการเปลี่ยนแปลง
ผมเคยรู้สึกว่านิสัยนี้เป็นเหมือนมาตรฐานอย่างหนึ่ง แต่เพิ่งมารู้ว่าไม่ใช่หลังจากได้รู้จัก Conventional Commits
ผมไม่เข้าใจเลยว่าทำไมมันถึงได้รับความนิยม
โดยส่วนตัวแล้วผมชอบใส่ issue เป็น git trailer มากกว่า
fix thing in fooIssue: ABC-123Git มีฟีเจอร์ในตัวมากมายสำหรับ parsing และจัดรูปแบบ trailer แบบนี้ จึงสร้าง alias ของ
git logแบบคัสตอมไว้ parse เพื่อดู inline หรือใช้ใน CI ได้ง่ายถ้ากำลังไล่อ่าน changelog ที่มีข้อจำกัดเรื่องจำนวนตัวอักษรอยู่แล้ว ก็ไม่ได้สนใจเท่าไรนักว่ามี
XYZ-999999อยู่ในข้อความ commit หลักการติดแท็กเป็น trailer ก็โอเค แต่ผมอยากเห็นมากกว่าว่า commit นั้นทำอะไร มากกว่าหมายเลข Jira issue
ผมคิดว่าเป็นปัญหาเฉพาะตอนที่มีการบังคับว่าต้องใส่ issue key ไว้หน้าสุดของหัวเรื่องเท่านั้น
แม้แบบนั้นผมก็คิดว่ามันไม่ดีต่อความอ่านง่าย
ไม่เข้าใจว่าทำไมถึงใส่มันต่อท้ายไว้ที่ไหนสักแห่งหลังรูปแบบจุกจิกของ Conventional Commits เฉยๆ ไม่ได้
issue key ก็ควรถูกดึงออกมาได้ด้วย regex ประมาณว่าตัวเลขตามหลังคำนำหน้าแบบตัวอักษรและตัวเลข ดังนั้น “มาตรฐาน” แบบนี้แทบไม่จำเป็นต้องกันพื้นที่เฉพาะให้เลย
ส่วนตัวถ้าไม่ใช้ Conventional Commits และเป็น commit ที่เกี่ยวกับ issue นั้น ผมก็มักใส่ไว้ท้ายประโยคในวงเล็บ
ถ้าเป็นความสัมพันธ์ที่ชัดกว่านั้น เช่นเป็นการแก้ issue จริงๆ ก็จะใส่
Fixestrailer ในข้อความด้วยน่าสนใจดี
พวกเราใช้รูปแบบอย่าง
fix(ABC-123): some message hereมาตลอด ซึ่งลิงก์ได้ดีและแสดงผลใน release note อัตโนมัติออกมาดูดีมากนั่นไม่ใช่มาตรฐาน แต่เป็น ธรรมเนียมปฏิบัติ
แค่กำหนดมาตรฐานภายในทีมว่าจะใส่ ticket ID ไว้ในข้อความ commit อย่างไรก็พอ
ถ้าอยากให้เครื่องอ่านได้ ก็ใช้ footer/trailer
สำหรับ Conventional Commits ผมแทบไม่มีอะไรดีจะพูดถึงเลย
รูปแบบนี้ไปกินพื้นที่ในส่วนของข้อความที่คนอ่านบ่อยที่สุด และ category หรือ type ก็ให้ข้อมูลน้อย
ถ้าใส่คำกริยาภาษาอังกฤษตรงไปตรงมาเป็นประโยคไว้ในหัวเรื่อง ก็แทนได้หมด และประโยคธรรมดาอ่านง่ายกว่าการใช้เครื่องหมายวรรคตอนสามแบบอย่าง
:,(),!มากพอทนกับการมี “ขอบเขต” อยู่ในหัวเรื่องได้ และสิ่งนั้นก็มีมาก่อนธรรมเนียมนี้ด้วยซ้ำ
ที่ทำงานผมสร้างเว็บแอปสำหรับผู้ใช้ที่ไม่ใช่สายเทคนิค และ changelog สำหรับผู้ใช้เหล่านั้นก็เขียนเป็นภาษานอร์เวย์ได้อย่างดี
ข้อความ commit ไม่เกี่ยวกับผู้ใช้ และสำหรับพวกเรา คงอีกนานกว่าจะมีการบังคับว่า commit ทุกอันต้องดีพอจะเข้าไปอยู่ใน changelog สำหรับผู้ใช้ปลายทาง
ดังนั้นใช้ footer/trailer แทนก็พอ
จุดที่ Conventional Commits ช่วยได้จริงคือ การ deploy ต่อเนื่อง
เราสามารถติดแท็ก SemVer และ deploy อัตโนมัติทุกครั้งที่ merge เข้า
mainได้ เพราะนักพัฒนาได้ตัดสินใจเรื่องที่จำเป็นต่อการติดแท็กและการจัดการเวอร์ชันไปแล้วตอนเขียนข้อความ commitผมยอมรับเต็มที่ว่าแนวทางนี้ไม่เหมาะกับโปรเจกต์ขนาดใหญ่อย่าง Linux kernel
แต่สำหรับ 99% ของโปรเจกต์ การจับ Conventional Commits คู่กับ SemVer เป็นการปรับปรุงครั้งใหญ่จากกระบวนการ release ปัจจุบัน และทำให้ทำอัตโนมัติได้ง่ายขึ้นมาก
git tagsgit describeมักเพียงพอสำหรับการจัดการเวอร์ชันแบบ deploy ต่อเนื่อง และv1.2.3-4-gabcdefก็อธิบาย commit ได้แม่นยำพอในแบบที่ git พอใจ ขณะเดียวกันก็คล้าย SemVer พอจะตั้งความคาดหวังได้โดยเฉพาะเมื่อมีการเพิ่ม
git tagsใหม่จากการตัดสินใจของมนุษย์เท่านั้น เช่นการบอกว่านี่เป็น breaking change จึงควรติดแท็ก major ใหม่ตอนนี้สำหรับเลขเวอร์ชันรูปแบบ
git describeข้อถกเถียงที่แท้จริงแทบมีแค่ว่าจะเปลี่ยนขีดตัวแรกเป็นเครื่องหมายบวกเพื่อให้ตรงกับความคาดหวังของ SemVer มากขึ้นหรือไม่ และถ้ามีคุณค่าในการบังคับให้เป็นไปตามความคาดหวังของ SemVer เช่นการเรียงลำดับเวอร์ชันให้ถูกต้องใน package manager ก็แปลงได้ด้วย regex ง่ายๆgit describeทำให้ automation ฝั่ง CD ง่าย แต่ยังเปิดให้มนุษย์เป็นคนตัดสินใจเรื่องเลขเวอร์ชันผ่านการเลือกgit tagหรือ GitHub Releases แทนการเดาจากคีย์เวิร์ดวิเศษในประวัติ commitที่ทำงาน เราก็บังคับ “แท็ก” ด้วย ขึ้นอยู่กับว่ากลุ่มที่สนใจการเปลี่ยนแปลงคือใคร
โดยแท็กในที่นี้ไม่ใช่ git tag แต่เป็นสตริงในชื่อ PR และเราใช้ “แท็ก” นั้นสร้าง changelog สำหรับแต่ละทีม
ถ้าอยากตั้งเวอร์ชันด้วยวิธีประหลาดแบบนั้น ก็ใส่วลีวิเศษไว้ในเนื้อความของ commit ก็ได้
แบบนั้นก็ไม่ถูกจำกัดว่าต้องเป็นแค่คำเดียวด้วย
ผมค่อนข้างเกลียดสไตล์การตั้งหัวเรื่องแบบนี้
ดูเหมือนคำอย่าง “Stop something” จะฮิตมาก ซึ่งมันเป็นเชิงสั่งการและให้ความรู้สึกว่า “ฉันถูกแน่นอน”
ไม่เข้าใจว่าทำไมไม่ใช้แนวอย่าง “In favour of something” หรือ “A case against something” แทน
คุณไม่จำเป็นต้องเห็นด้วยกับจุดยืนนั้น แต่การเรียกร้องให้ทำให้ถ้อยคำอ่อนลงเป็นการตอบโต้ที่อ่อนมาก
considered harmfulแต่ก็ยังมีความเป็นพิษนิดหน่อยแก่นของมันดูเหมือนจะพยายามทำให้ความชอบส่วนตัวตามอำเภอใจบางอย่าง เช่นอยากสลับลำดับของ A กับ B ดูเป็นเรื่องใหญ่กว่าความเป็นจริง
สำหรับหลายคนมันหยาบคาย แต่ เศรษฐกิจความสนใจ ให้รางวัลกับวิธีแบบนั้น
แก้ไข: ดูเหมือนเขาจะเปลี่ยนชื่อเรื่องให้ยั่วยุน้อยลงแล้ว
เป็นการตัดสินใจที่ดี
ผมไม่ได้ชอบ Conventional Commits เท่าไร แต่ก็ปล่อยให้แต่ละคนใช้สิ่งที่ตัวเองต้องการเถอะ
มีมีมที่มีอิทธิพลต่อชื่อเรื่องแนวนี้อยู่บ้าง
แปลกนะ
เหตุผลหลักที่ใช้ข้อความคอมมิตสไตล์นี้คือเพื่อ ทำ CI/CD อัตโนมัติ
แก้ไข: ตอนอ่านรอบแรกฉันพลาดส่วนนี้ในบทความไป แต่จริง ๆ เขาพูดถึงไว้แล้ว
ขอโทษด้วย
ประเภทคอมมิตจึงอยู่ข้างหน้า เพราะมันบอกเวิร์กโฟลว์อัตโนมัติว่าจะจัดการคอมมิตนั้นอย่างไร
ตัวอย่างเช่น ถ้าทำ CD แล้วมีคอมมิต
fix:หลายอัน ก็จะเพิ่มแค่เลขแพตช์ของ semantic versionถ้าคอมมิต
feat:ก็จะเพิ่มไมเนอร์เวอร์ชัน และfeat!คือเพิ่มเมเจอร์เวอร์ชันต่อให้ไม่ได้ใช้ CD กับการรีลีส ข้อความคอมมิตเชิงความหมายก็ยังถูกใช้เพื่อทำ changelog อัตโนมัติได้
แน่นอนว่าโดยปกติไม่ควรใส่ข้อความ Git commit ลงใน changelog ตรง ๆ
เพราะข้อความนั้นเขียนถึงนักพัฒนา ไม่ใช่ผู้ใช้
การจัดการ semantic version จะพังเมื่อมีการ revert และ changelog อัตโนมัติก็มี ผู้อ่านเป้าหมาย ผิดคน
เดี๋ยวนี้ใช้ CalVer แทน SemVer เลยไม่ใช่ปัญหาแล้ว แต่ก็ยังชอบแนวคิดเรื่องการเพิ่มเวอร์ชันอัตโนมัติแบบฉลาด ๆ
ต่อให้มี
fixหรือfeatอยู่ในหัวข้อคอมมิต มันก็ไม่ได้ให้ข้อมูลที่มีประโยชน์กับคนที่ไล่อ่านล็อกประเด็นคือควรเลิก Conventional Commits เพื่อให้ AI สร้างคอมมิตได้ง่ายขึ้นต่างหาก
ถ้ากลับลำดับกัน ความหงุดหงิดหลักของฉันจะถูกแก้จริง ๆ
ฟีเจอร์ นี่มันคืออะไรกันแน่?
refactor(core): Update webmcp support to use document.modelContextอย่างที่ผู้เขียนบอก เส้นแบ่งระหว่างการแก้ไข การปรับปรุง และการเก็บกวาดทั่วไปนั้นเลือนราง และการแยกแต่ละความเปลี่ยนแปลงเชิงความหมายออกเป็นคอมมิตคนละอัน สุดท้ายก็อาจโดน squash อยู่ดี เลยกลายเป็นงานที่ไม่เป็นประโยชน์กับใคร
ฉันมองว่า Conventional Commits เป็นผลพลอยได้จากความพยายามทำ SemVer อัตโนมัติ มากกว่าจะเป็นการแก้ปัญหาอื่นโดยตรง
และฉันก็คิดว่า changelog ไม่ควรถูกทำอัตโนมัติอยู่แล้ว
ถ้าต้องการรายการก็ไปดู
git logได้changelog เป็นโอกาสที่จะสื่อสารกับผู้อ่านวงกว้างขึ้นว่า ภายในเกิดอะไรขึ้นจริงบ้าง
“ผู้อ่าน changelog ต่างจากผู้อ่าน commit log อย่างสิ้นเชิง”
“changelog มีไว้สำหรับผู้ใช้”
เรือรำนั้นน่าจะออกไปแล้ว
บริษัทส่วนใหญ่พอใจกับ “Bug Fixes & Performance Improvements”
อย่างน้อยถ้าจะไม่ลงแรงทำอะไรเลย changelog ที่สร้างขึ้นมาก็ยังดีกว่าไม่มีเลย
uv:นำหน้าคอมมิตที่ผู้ใช้จะเห็นจากนั้นทุกสัปดาห์ก็ค้นหาคอมมิตพวกนั้น แล้วเอาข้อความไปใช้ตรง ๆ หรือขัดเกลาเล็กน้อย
แล้วก็ใส่ไว้ในเมนู Help/Release-notes ของตัวผลิตภัณฑ์ด้วย
มันก็ตลกดีที่จะมาบอกให้ฉันเลิกทำสิ่งที่ทั้งตัวเองไม่เคยทำและไม่เคยแม้แต่ได้ยินมาก่อน
ปกติแล้วก็มักจะใส่คำนำหน้าพิเศษเฉพาะกับพวก database schema migration หรือเรื่องสำคัญอื่น ๆ
ดูเหมือนเขาจะตั้งชื่อคอมมิตไม่เก่ง และคงตั้งชื่อสัญลักษณ์ต่าง ๆ ไม่เก่งเหมือนกัน
เป็นปัญหาเรื่องฝีมือ เขาแค่มาโพสต์คร่ำครวญในที่สาธารณะ ก็ปล่อยผ่านไปเถอะ
ความคิดเห็นจาก Lobste.rs
ดีใจที่ได้เห็นบทความที่สรุปข้อโต้แย้งต่อ conventional commits ด้วยเหตุผล ไม่ใช่แค่ความรู้สึกต่อต้านแบบสัญชาตญาณ
ที่ผ่านมาก็ไม่ได้คิดลึกนักว่าทำไมถึงไม่ชอบ และเคยสงสัยว่าอาจเป็นเพราะมันถูกโยงกับโค้ดที่ LLM สร้างขึ้น โดยเฉพาะ
chore:ที่เกลียดที่สุด อยากให้เลิกประดิษฐ์ Hungarian notation ขึ้นมาใหม่ตั้งแต่แรก มันไม่ควรถูกสร้างขึ้นมาเลยchore:ตอนนี้ก็ไม่มีอยู่ใน Angular commit style guide แล้ว และดูเหมือนพวกเขาจะตระหนักได้ว่ามันกำกวมเกินไปเลยถูกรวมเข้าไปในbuild:แม้แต่ตอนที่ยังอยู่ในสไตล์ของ Angular คำอธิบายของ
chore:ก็ระบุการใช้งานค่อนข้างเฉพาะเจาะจง แต่ในบางโปรเจ็กต์โอเพนซอร์สดูเหมือนจะถูกแปะตามบรรยากาศกับงานที่เจ้าตัวขี้เกียจทำเอามาก ๆฉันไม่ได้ชอบ conventional commits แต่ดูเหมือนทางเลือกที่เสนอจะมองข้ามเหตุผลที่ว่า scope เป็นตัวเลือกเสริม
ในโปรเจ็กต์เล็กที่ไม่ได้มีโมดูลชัดเจนหลายส่วน แนวคิดเรื่อง “scope” ก็ไม่ได้มีประโยชน์มากนัก อีกแนวปฏิบัติที่มีประโยชน์และทั้งสองฝั่งไม่ได้พูดถึงคือการใส่เลข issue หรือตั๋วไว้ในหัวข้อคอมมิต เพราะช่วยให้เข้าใจบริบทเพิ่มเติมของการเปลี่ยนแปลงได้ง่ายขึ้น และช่วยมากเป็นพิเศษตอนรีวิวโค้ด แต่ก็ไม่ชอบถ้าบังคับให้เลขตั๋วเป็นข้อกำหนด เพราะจะทำให้เกิดตั๋วไร้ประโยชน์เต็มไปหมดแม้เป็นการเปลี่ยนเล็กน้อย ถ้าเป็นการแก้บั๊กหรือทำงานเฉพาะอย่าง ก็ควรเชื่อมโยงกับบั๊กหรืองานนั้น
ยังดีกว่า “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 มาใช้ตามเดิม บางทีเรื่องนี้ก็อาจแค่ต้องทำให้คนใช้งานมันเก่งขึ้นไม่ใช่หรือ