เขียนข้อความ commit ให้ใช้กับ CI/CD ได้ด้วย Conventional Commits
ปกติเรามักจะเขียนข้อความ commit ไว้แบบง่าย ๆ คือ ฉันทำอะไรไปนะ แบบสรุป ๆ ลงใน commit นั้น ๆ บางคนไม่ได้ให้ความสำคัญกับข้อความ commit ด้วยซ้ำ แต่จริง ๆ แล้วข้อความ commit นี่มันทำอะไรให้เราได้มากกว่าทีเราคิด เช่น การที่ทีมจะต้องมานั่งทำ Release note ทุก ๆ release เอง หรือจะต้องมานั่งดูว่า เอ๊ะ commit นี้มันทำเกี่ยวกับเรื่องไหน ฟีเจอร์ไหนหว่า มันน่าจะดีถ้า commit ของเรามันมีรูปแบบอะไรบางอย่างที่จะช่วยให้เราทำงานได้สะดวกขึ้น และยิ่งถ้ามันช่วยให้เราไม่ต้องมานั่งทำ Release note เองได้ก็เยี่ยมไปเลย
โพสต์นี้เลยอยากจะแนะนำให้รู้จักสิ่งที่เรียกว่า Conventional Commits
ก่อนจะไปรู้จัก Conventional Commits เรามารู้จักกับ Commit ปกติกันก่อน
Commit คืออะไร?
Commit หรือข้อความ commit (Commit message) คือข้อความที่ใช้อธิบายถึงสิ่งที่เปลี่ยนแปลงใน git commit หนึ่ง ๆ
หลาย ๆ คนอาจจะไม่รู้ว่าเราสามารถเขียน commit ได้หลายบรรทัด แต่เวลาเราดูใน git UI ปกติ เราจะเห็นแค่บรรทัดแรกของข้อความ commit ส่วนที่เหลือเราจะเห็นเมื่อเรากดดูรายละเอียดของ commit นั้น ๆ หรือถ้าเราใช้คำสั่ง git log
เราก็จะเห็นรายละเอียดเช่นกัน
วิธีพิมพ์ข้อความ commit หลาย ๆ บรรทัด คือ git commit -m "ข้อความ...
แล้วอย่าเพิ่งพิมพ์ "
ปิดท้าย แล้วเคาะ Enter
แล้วพิมพ์ต่อได้เลย พอพิมพ์ครบแล้วค่อยปิดท้ายด้วย "
แล้วค่อยเคาะ Enter
อีกทีเพื่อ commit
ส่วนใครใช้ UI ส่วนใหญ่ก็จะทำให้เป็นกล่องใหญ่ ๆ ให้สามารถใส่หลาย ๆ บรรทัดได้อยู่แล้ว ก็ใส่ได้เลย
Conventional Commits คืออะไร?
Conventional Commits คือข้อตกลงในการเขียนข้อความ commit ให้มีรูปแบบเดียวกัน โดยจะถูกกำหนดให้มีรูปแบบที่ชัดเจน เรียกว่า Conventional Commits Specfication
ซึ่งตอนที่กำลังเขียนโพสต์นี้ spec อยู่ที่เวอร์ชั่น 1.0.0 แล้ว
Conventional Commits ช่วยอะไรบ้าง?
เนื่องจาก Conventional Commits มีรูปแบบที่ชัดเจน ทำให้กระบวนการต่าง ๆ ที่จำเป็นต้องใช้ข้อความจากการ commit สามารถทำได้อย่างอัตโนมัติ
- การสร้างไฟล์ CHANGELOG หรือการออก Release note
- การปรับ version ที่มีรูปแบบ SemVer โดยอัตโนมัติ
- ทำให้การสื่อสารกับเพื่อนร่วมทีม หรือผู้ที่เกี่ยวข้องมีรูปแบบมากขึ้น เข้าใจได้ง่ายขึ้น
ลักษณะของ Conventional Commits
ข้อความที่เป็น Conventional Commits จะอยู่ในรูปแบบตามนี้
<type>[optional scope]: <description>
[optional body]
[optional footer]
จะเห็นได้ว่า มีส่วนประกอบทั้งหมด 5 ส่วนคือ
- type หรือชนิดของ commit โดยจะมีตัวหลัก ๆ คือ feat และ fix ที่เป็นมาตรฐานตาม spec ส่วนชนิดอื่น ๆ ก็สามารถใช้ได้เช่นกัน แค่มันไม่ได้อยู่ใน spec นี้ เช่น build, chore, ci, docs, style หรือชนิดอื่น ๆ ตามแต่จะตกลงกัน
- optional scope หรือขอบเขตที commit นี้ครอบคลุมถึง จะหมายถึงส่วนของโค้ด หรือส่วนของ module ที่ commit นี้อ้างถึง ไม่เขียนก็ได้ แต่ถ้าจะเขียน เวลาเขียน ให้เขียนอยู่ในวงเล็บ
()
- description หรือคำอธิบาย commit คือข้อความ commit แบบสรุปสั้น ๆ ว่า commit นี้เกี่ยวกับอะไร ทำอะไรไป
- optional body หรือเนื้อหาของ commit คือรายละเอียดของ commit ว่า commit นี้ทำอะไรไปบ้าง อาจจะมีหลาย ๆ ย่อหน้าก็ได้ ไม่เขียนก็ได้
- optional footer หรือคำลงท้ายของ commit จะเป็นตัวใช้ reference ต่าง ๆ ของ commit จะไม่เขียนก็ได้ แต่ถ้าเขียน จะอยู่ในรูปแบบ
token: ข้อความ
หรือtoken #ข้อความ
โดย token จะหมายถึงกลุ่มคำที่ใช้-
แทนเว้นวรรค เช่นReviewed-by
เป็นต้น
การระบุ Breaking Change
หาก commit นั้น ๆ เป็น Breaking change หรือมีผลกระทบกับสิ่งที่ release ไปก่อนหน้า ให้ระบุด้วยว่าเป็น Breaking change โดยให้ใส่ !
ตามหลัง type (หรือถ้ามี scope ก็ให้ตามหลัง scope) ทันที หรือจะใส่ลงใน footer เป็นตัวอักษรพิมพ์ใหญ่ทั้งหมด และตามด้วย :
เช่น
แบบที่มี scope
feat(core)!: Use new payment gateway engine 2.0.0
แบบที่ไม่มี scope
feat!: Use new payment gateway engine 2.0.0
แบบที่ใส่ลงใน footer
feat: Use new payment gateway engine 2.0.0BREAKING CHANGE: New payment gateway engine 2.0.0 provides additional core feature we need to implement the new feature.
แบบที่ใส่ทั้งใน type และ footer
feat!: Use new payment gateway engine 2.0.0BREAKING CHANGE: New payment gateway engine 2.0.0 provides additional core feature we need to implement the new feature.
แบบที่ใส่ทั้งใน type, scope และ footer
feat(core)!: Use new payment gateway engine 2.0.0BREAKING CHANGE: New payment gateway engine 2.0.0 provides additional core feature we need to implement the new feature.
มันจะช่วยกระบวนการใน CI/CD ได้ยังไง
เพราะว่า Conventional Commits ถูกออกแบบมาให้สอดคล้องกับการใช้เวอร์ชั่นแบบ SemVer นั่นคือ
- commit ที่ type เป็น feat จะมีความหมายเป็น
MINOR
ใน SemVer - commit ที่ type เป็น fix จะมีความหมายเป็น
PATCH
ใน SemVer - ถ้ามีการระบุว่า commit นั้นเป็น BREAKING CHANGE จะมีความหมายเป็น
MAJOR
ใน SemVer
นั่นคือ เราสามารถให้เครื่องมือมาอ่านข้อความ commit และประเมินเลขเวอร์ชั่นถัดไปได้เลย และด้วยความที่มันถูกเขียนอย่างมี pattern เราสามารถใช้เครื่องมือมาดึงเอาส่วนที่เป็นรายละเอียดของ commit มาสร้างเป็น Release note ได้โดยอัตโนมัติ
ถ้ามีการ commit บ่อย ๆ แบบแก้นิดหน่อยก็ commit จะทำ Conventional Commits ยังไงดี
การ commit บ่อย ๆ เป็นเรื่องที่ดี จริง ๆ แล้วเป็นเรื่องที่น่าสนับสนุนด้วยซ้ำ แต่ถ้าเราอยากจะทำ Conventional Commits การมี commit ที่มีข้อความ commit ที่ชัดเจนก็เป็นสิ่งจำเป็น ความจริงแล้วเราสามารถยุบ commit หลาย ๆ commit เข้าด้วยกันก่อนที่จะ push ไปที่ upstream ได้
ลองดูเทคนิคที่เรียกว่า git squash
และ squash rebase workflow
เพื่อรวม commit เข้าด้วยกันดู
ถ้านาน ๆ ที commit ที จนมีของหลาย ๆ ฟีเจอร์ปนกันในแต่ละ commit จะทำ Conventional Commits ยังไงดี
การรวมของหลาย ๆ อย่างเข้าด้วยกันใน commit เดียว ในระยะสั้นอาจจะดูสะดวก แต่ในระยะยาวแล้ว การทำแบบนี้จะทำให้เกิดปัญหาขึ้นมาได้ เช่น
- การทำงานร่วมกับคนอื่น เพราะคนอื่นมาอ่านข้อความ commit แล้วก็จะไม่เข้าใจว่ามันทำอะไรกันแน่
- บางคนใช้เทคนิคที่เรียกว่า
cherrypick
ก็จะทำได้ยากขึ้นด้วยเช่นกัน เพราะมีของหลายอย่างที่อยู่ปะปนกันใน commit เดียว - การจัดการ PR ก็จะทำได้ลำบากกว่าเดิม เพราะมีของหลายอย่างใน PR จะเลือก review แค่บางส่วนก็ทำได้ยาก และการ approve PR บางส่วนก็เป็นไปไม่ได้
ดังนั้น ทางที่ดี พยายามทำให้มันเป็นหลาย ๆ commit จะดีกว่านะ
รายละเอียดเพิ่มเติมเกี่ยวกับ Conventional Commits
ถ้าหากอยากรู้รายละเอียดเพิ่มเติมหรืออ่านเกี่ยวกับ Conventional Commits Specification เต็ม ๆ สามารถดูได้ที่ https://www.conventionalcommits.org/ แบบแปลไทยก็มี ที่ https://www.conventionalcommits.org/th/