- Conventional Commits cố gắng gán ý nghĩa cho thông điệp commit theo dạng
<type>[optional scope]: <description>, nhưng lại đặt loại thay đổi lên trước và coi phạm vi là tùy chọn, khiến thông tin cần thiết cho việc tra cứu thực tế bị đẩy xuống sau
- Người đóng góp, người debug và người ứng phó sự cố đều tìm trong log commit khu vực mã mà thay đổi đã chạm tới; bug có thể phát sinh từ bất kỳ loại thay đổi nào, nên scope quan trọng hơn type
- Với
fix(compiler): prevent namespaced SVG <style> elements from being stripped, chỉ riêng phần mô tả cũng đã cho thấy đây là sửa lỗi; còn refactor(core): Update webmcp support to use document.modelContext cho thấy một commit có thể đồng thời là sửa đổi, refactor và thêm tính năng, nên type vừa trùng lặp vừa hạn chế
- Việc tự động tạo CHANGELOG và quyết định tăng semantic version gặp vấn đề vì độc giả của log commit và changelog là khác nhau; thêm vào đó, revert, việc vô tình phá vỡ tương thích ngược và việc xử lý lỗi phá vỡ về sau có thể khiến kết quả lệch đi
- Thông điệp commit theo kiểu tiền tố scope cho thấy chủ thể thay đổi trước tiên, và điều kiện build hoặc deploy cũng nên dựa trên các tệp thay đổi trong
git diff thay vì loại ở tiêu đề
Ưu tiên sai thứ tự
- Conventional Commits đặt mục tiêu gán ý nghĩa cho thông điệp commit để giúp lập trình viên và người dùng cuối hiểu được thay đổi
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
- Dòng tiêu đề gồm
<type> như fix, feat, chore, docs, refactor, một scope tùy chọn và phần mô tả
- Khuyết điểm cốt lõi là cấu trúc này ưu tiên loại thay đổi là type hơn scope, tức chủ thể thực sự của thay đổi
- Việc scope là tùy chọn khiến thông tin quan trọng nhất của commit có thể bị thiếu, còn việc đặt type ở đầu tiêu đề làm đảo ngược thứ tự ưu tiên
Vì sao scope quan trọng hơn type
- Người đóng góp đọc log commit để tìm các thay đổi kể từ lần đóng góp gần nhất, nắm dòng chảy tổng thể của dự án, hoặc tìm những commit có thể xung đột với phần việc đang làm khi pull hay rebase
- Người debug tìm những thay đổi đã chạm tới khu vực liên quan đến thành phần nơi bug xuất hiện; vì bug có thể phát sinh từ bất kỳ type thay đổi nào, nên thông tin type không giúp ích nhiều
- Người ứng phó sự cố sẽ rà log commit quanh thời điểm xảy ra sự cố để tìm khu vực gây ra vấn đề; nếu tại điểm số lỗi API inbound tăng vọt có một commit thuộc scope
auth, đó sẽ là một ứng viên nguyên nhân rất đáng nghi
- Với người đọc log commit, thông tin quan trọng không phải thay đổi thuộc loại gì mà là nó đã chạm tới khu vực nào
Tính trùng lặp và giới hạn của type
Giới hạn của lời hứa tự động hóa
- Ý tưởng tự động sinh CHANGELOG từ commit bằng các công cụ như git-cliff hay conventional-changelog có vấn đề ở chỗ độc giả của log commit và changelog là hai nhóm khác nhau
- CHANGELOG hướng tới người dùng, tập trung vào việc hiểu khác biệt về mặt chức năng và kinh doanh giữa các phiên bản
- Log commit hướng tới lập trình viên, tập trung vào việc đọc hiểu codebase đã thay đổi theo thời gian ra sao và dòng chảy thay đổi theo góc nhìn scope
- Trong các dự án có độ phức tạp từ trung bình trở lên, một tính năng có ý nghĩa thường được đưa vào qua nhiều commit; với lập trình viên, quá trình triển khai là hữu ích, nhưng với người dùng cuối thì chỉ tính năng hoàn chỉnh mới quan trọng
- Commit revert quan trọng với lập trình viên vì nó là một phần của dòng chảy trong log commit, nhưng với người dùng cuối thì một thay đổi đã bị hoàn tác cũng giống như chưa từng được tạo ra
- Việc tăng semantic version dựa trên type của commit có thể dẫn tới các vấn đề như tăng major dù thay đổi phá vỡ tương thích ngược đã bị revert, tăng minor hoặc patch sai vì chỉ về sau mới phát hiện ra sự phá vỡ, hoặc đánh dấu là breaking change dù nó đã được trung hòa khi gộp với các commit tiếp theo
- Trong những tình huống đó có thể sửa lịch sử bằng rebase, nhưng workflow có thể ngăn cản hoặc làm hỏng việc này, đồng thời làm giảm độ tin cậy của dòng chảy mà log commit truyền tải
- Nếu dùng type trong tiêu đề commit để kích hoạt quy trình build hoặc deploy, thì một commit mang tiêu đề
docs: fix typos vẫn có thể lén đưa lỗ hổng vào hệ thống xác thực và qua mặt công cụ tự động
- Điều kiện build và deploy nên được quyết định bằng cách xác định các tệp đã thay đổi qua
git diff hơn là dựa vào tiêu đề commit
Vấn đề áp dụng và phương án thay thế
- Conventional Commits cho phép dự án tự định nghĩa tập type riêng, nhưng nhiều dự án chỉ lấy nguyên bộ type mặc định của commitlint, điều này có thể không phù hợp với đặc thù từng dự án
- Đặc tả Conventional Commits về mặt kỹ thuật chỉ định nghĩa
fix và feat, còn các type bổ sung được giao cho từng dự án tự quyết
- Trong môi trường doanh nghiệp, yêu cầu quản lý thay đổi và kiểm toán đôi khi buộc mọi thông điệp commit phải có mã ticket; nếu
<scope> bị dùng làm chỗ cho mã ticket thì metadata hữu ích sẽ biến mất
- Linux, FreeBSD, Git, Go, NixOS và Node.js đều dùng thông điệp commit với tiền tố scope phù hợp với dự án
- Trong Linux kernel, scope tự nhiên là subsystem; trong dự án Go là package path; còn trong kiến trúc microservice thì đó là tên microservice
- scopedcommits.com cổ vũ việc quay lại định dạng thông điệp commit lấy scope làm trung tâm và tách riêng việc tạo CHANGELOG khỏi quản lý log commit
- Những lợi thế được cho là của Conventional Commits đã không chuyển hóa thành lợi ích thực tế, còn sự phổ biến trong các dự án mã nguồn mở và xu hướng AI chọn mặc định kiểu này lại khiến thông điệp commit pha trộn anti-pattern lan rộng
1 bình luận
Ý kiến trên Lobste.rs
Thật mừng vì đây là một bài viết sắp xếp các lập luận phản bác conventional commits bằng lý lẽ thay vì chỉ là cảm giác bài xích theo bản năng
Tôi chưa từng nghĩ sâu về việc vì sao mình ghét nó, và cũng tự hỏi có phải vì tôi đã bắt đầu liên hệ nó với mã do LLM tạo ra hay không. Đặc biệt tôi ghét nhất
chore:; mong là chúng ta đừng tái phát minh ký pháp Hungarian. Ngay từ đầu nó đã không nên tồn tạichore:thậm chí không còn trong Angular commit style guide nữa, có lẽ vì họ nhận ra nó quá mơ hồ nên đã gộp vàobuild:Ngay cả khi còn trong style của Angular, mô tả cho
chore:cũng nêu ra các mục đích sử dụng khá cụ thể, nhưng ở một số dự án mã nguồn mở thì có vẻ người ta gắn nó theo cảm tính cho những việc theo đúng nghĩa là thấy ngại làmTôi không thích conventional commits, nhưng phương án thay thế được đề xuất có vẻ bỏ qua lý do scope là tùy chọn
Trong các dự án nhỏ không có nhiều module tách bạch, khái niệm “scope” không hữu ích lắm. Một thực hành hữu ích mà cả hai bên đều bỏ sót là thêm số issue hoặc số ticket vào tiêu đề commit, vì như vậy sẽ dễ nắm được ngữ cảnh bổ sung của thay đổi và đặc biệt hữu ích khi review code. Tuy vậy, tôi không thích việc biến số ticket thành bắt buộc, vì như thế sẽ sinh ra hàng loạt ticket vô dụng cho các thay đổi nhỏ nhặt; nhưng nếu một thay đổi xử lý một bug hay một task cụ thể thì nó nên được liên kết với bug hoặc task đó
Dù sao nó vẫn tốt hơn “type” commit bị lặp ý, thứ mà lẽ ra chỉ cần nhìn dòng tiêu đề là đã thấy rồi
Nếu thay đổi khớp rõ ràng với một ticket thì dùng commit kiểu “số ticket”, còn không thì dùng cách khác. Có thay đổi hợp với type hơn nhưng ít hợp với scope hơn, và ngược lại, nên cũng có thể trộn scoped commits với conventional commits
Tôi chỉ muốn nói là “đừng dùng phông chữ đơn cách cho văn bản dạng đoạn”
Dù vậy, nhìn chung tôi vẫn đồng ý với tiền đề của bài viết
Ngay cả khi commit message không hay, nếu muốn hình dung phạm vi thay đổi thì tôi khuyên nên dùng
git log --name-onlyhoặcgit log --statthường xuyênNhìn tên file cũng giúp khá nhiều để biết mỗi commit đã thay đổi gì mà không cần mở từng commit ra xem hết
Cách tôi thực sự thích là bắt buộc dùng kiểu conventional commit cho tiêu đề PR
Tiêu đề PR vẫn có thể được maintainer chỉnh sửa sau khi merge, không cần viết lại lịch sử commit, và nếu dùng cùng các công cụ như release-drafter thì có thể tự động hóa changelog có ý nghĩa trong GitHub Releases. Nó cung cấp mức độ chi tiết phù hợp với các bên liên quan mà tác giả nhắc đến, tức là tách riêng tính năng, bản sửa lỗi và thay đổi phá vỡ tương thích, đồng thời cũng tự động xử lý bản nháp GitHub Release tiếp theo với semver hợp lý
Nhận xét trong bài rằng một thành phần như
parse-libkhông nên là tùy chọn là đúng, và tôi cũng đồng ý rằng việc ép buộc conventional commits có thể làm người đóng góp mới nản lòng. Nhưng các phương án thay thế cũng không hẳn tốt hơnDù vậy, định danh thay đổi phá vỡ tương thích như
fix!(parse-lib): Don't leave sparse holes when parsing JSON arraysvẫn truyền tải khá nhiều thông tin. Đó là bản sửa lỗi cho một thành phần cụ thể, việc sửa lỗi đó kéo theo một thay đổi phá vỡ tương thích không thể tránh khỏi, và còn hàm ý một mức tăng semver kiểu minor nữa. Những thứ như vậy có thể dùng ở tiêu đề PRTôi thừa nhận mình đã quá sa đà vào conventional commits như một cách khuyến khích kỷ luật commit, và rồi nó thành thói quen
Giờ đây đôi khi tôi thấy nó mang tính hạn chế và tùy tiện. Ở vài dự án tôi thậm chí không rõ đó có phải là thông lệ thực sự hay không, và dần chuyển sang gần với phong cách Linux/Go/Node hơn; trong các monorepo có nhiều cấu hình khác nhau, việc viết
[service]: [what changed]có vẻ tự nhiên hơn là cố ép ra một type. Từ giờ tôi định thử nghiệm nhiều hơn với phong cách commit cá nhân dựa trên tiêu chí cái gì trông hữu ích, thay vì cố khớp vào một quy ước cứng nhắc, và scoped commits có vẻ là một điểm khởi đầu tốtchore(lobsters): add my 2 cents on conventionals commits [JIRA-69420]Tôi đồng ý với gần như mọi thứ, nhưng có một điểm tôi nhìn khác đi: đoạn nói rằng “nó cho người đóng góp thấy một hồ sơ mang tính xét lại, làm giảm độ tin cậy của câu chuyện mà commit log kể lại”. Có vẻ tác giả chủ yếu đang nói về các nhánh công khai, và nếu là nhánh công khai thì đó là lời khuyên hợp lý. Nhưng điều đó không nên áp dụng cho nhánh riêng tư. Chỉ cần làm sao để người review thay đổi cuối cùng — tức maintainer hoặc chính tôi của 10 năm sau — có thể hiểu được là đủ; không cần giữ lại một dòng suy nghĩ trước sau không khớp, hoặc tệ hơn là cả một chùm commit
address reviewCâu trả lời cho “vì sao scope là tùy chọn?” là trong các dự án nhỏ thì toàn bộ dự án chính là scope
Tôi đồng ý rằng “type” của commit không quá hữu ích, nhưng cũng không chắc giữa scoped commits và conventional commits có khác biệt lớn đến vậy không. Scoped thực chất chỉ là conventional bỏ đi “type”, còn các phân loại như fix, feat, refactor, chore thì vẫn là những phân loại chấp nhận được
Nếu mọi người đều chỉ bê nguyên mặc định của commitlint vào dùng, thì chẳng phải chỉ cần làm cho người ta dùng nó tốt hơn hay sao?