Tại sao bạn nên ngừng sử dụng Conventional Commits ngay lập tức
Bài viết phân tích sâu lý do tại sao tiêu chuẩn Conventional Commits thực sự gây trở ngại cho quy trình phát triển phần mềm, đặc biệt là việc ưu tiên "type" (loại thay đổi) sai chỗ so với "scope" (phạm vi thay đổi). Tác giả chỉ ra những thất bại trong việc tự động tạo changelog và quản lý phiên bản, đồng thời đề xuất phương án "Scoped Commits" hiệu quả hơn được các dự án lớn như Linux hay Go sử dụng.

Bạn chắc hẳn đã từng gặp phải Conventional Commits ít nhất một lần. Có thể bạn đã thấy nó xuất hiện trong nhật ký thay đổi (changelog) của một dự án mã nguồn mở mà bạn đang sử dụng, hoặc nó là định dạng bắt buộc khi bạn đóng góp cho một dự án nào đó. Rất nhiều người coi đây là nguyên tắc bất di bất dịch, nhưng tôi lại cho rằng đây là một tiêu chuẩn tồi tệ cần được loại bỏ.
Mặc dù được một lượng lớn các dự án mã nguồn mở phổ biến sử dụng, Conventional Commits thực chất là một tiêu chuẩn tồi. Nó khuyến khích các nhà phát triển tập trung vào những điều sai lầm và thất bại trong việc thực hiện những lời hứa hẹn của mình.
Thất bại trong trọng tâm
Conventional Commits hứa hẹn sẽ thêm ý nghĩa ngữ nghĩa cho các thông điệp commit để giúp các nhà phát triển và người dùng cuối hiểu rõ hơn về các thay đổi. Tuy nhiên, nó đã thất bại một cách thảm hại.
Để chứng minh điều này, hãy xem xét cấu trúc của một Conventional Commit. Theo trang web chính thức, thông điệp commit nên được định dạng như sau:
type(scope): description
Chủ đề của commit chứa một loại (type) (như fix, feat, chore, docs, hoặc refactor) mô tả loại thay đổi. Theo sau đó là một phạm vi (scope) tùy chọn, và sau đó là phần mô tả.
Định dạng này có một sai lầm lớn: nó ưu tiên type (loại) hơn scope (phạm vi). Điều này hoàn toàn ngược lại.
Phạm vi quan trọng hơn Loại
Phạm vi của một thay đổi (chủ đề của thay đổi) mới là phần quan trọng nhất của một commit. Hãy xem xét lý do tại sao các bên liên quan dưới đây lại quan tâm đến phạm vi thay đổi nhiều hơn là loại thay đổi:
- Người đóng góp: Khi bạn đóng góp cho một dự án, bạn thường cần đọc nhật ký commit để xác định các thay đổi trong cơ sở mã liên quan đến một khu vực mã cụ thể. Bạn quan tâm đến những khu vực nào đã được tác động, chứ không thực sự quan tâm đến loại thay đổi là gì.
- Người gỡ lỗi: Khi điều tra một lỗi (bug), bạn thường muốn xem qua nhật ký commit để xem những thay đổi nào đã tác động đến các khu vực liên quan đến thành phần nơi lỗi xuất hiện. Một lần nữa, phạm vi là thông tin quan trọng nhất. Loại thay đổi hoàn toàn vô dụng vì lỗi có thể được đưa vào bởi bất kỳ thay đổi nào.
- Người phản ứng sự cố: Khi hệ thống gặp sự cố, việc quét nhật ký commit để tìm các thay đổi được thực hiện quanh thời điểm sự cố là một cách hiệu quả để xác định nguyên nhân. Ví dụ, nếu bạn thấy một commit liên quan đến phạm vi
auth(xác thực) tại thời điểm lỗi tăng vọt, đó có thể là thủ phạm. Loại thay đổi một lần nữa lại không liên quan.
Vậy Conventional Commits làm gì? Nó hạ thấp tầm quan trọng của phạm vi đến mức nó trở thành một tùy chọn! Tại sao phạm vi lại có thể tùy chọn được? Có một commit không có phạm vi giống như có một câu không có chủ thể! Sau đó, để thêm vào sự sỉ nhục, Conventional Commits nâng loại thay đổi lên phía trước thông điệp commit. Nó đã sai hoàn toàn về thứ tự ưu tiên.
Type là thừa thãi và hạn chế
Bạn có thể nghĩ rằng "có thể nó ngược, nhưng ít nhất loại commit vẫn quan trọng, đúng không?". Và tôi nói rằng "Không". Phần mô tả của commit hầu như luôn cho bạn biết loại thay đổi đó!
Hãy xem xét thông điệp commit này làm ví dụ:
fix(compiler): prevent namespaced SVG elements from being stripped
Ngay cả khi bạn chỉ có phần mô tả, rõ ràng đó là một bản sửa lỗi (bugfix)! Không gian trên dòng chủ đề của commit đã rất khan hiếm, việc lãng phí ký tự cho loại thay đổi là không hữu ích. Thậm chí, nó thường gây hạn chế. Hãy xem xét ví dụ này:
refactor(core): Update webmcp support to use document.modelContext
Commit này đã cập nhật chức năng webmcp trong thành phần cốt lõi để hỗ trợ cả document.modelContext và navigator.modelContext. Vậy đó là sửa lỗi, tái cấu trúc (refactor), hay tính năng mới? Tôi lập luận rằng nó là tất cả các thứ đó! Nhưng một lần nữa, điều thực sự quan trọng là đó là một thay đổi đối với thành phần core/webmcp.
Conventional Commits về cơ bản tập trung vào sai thứ (loại commit) và hạ thấp giá trị của phạm vi (điều mà mọi người thực sự quan tâm).
Những lời hứa bị phá vỡ
Chúng ta đã xác định rằng định dạng của Conventional Commits rất tệ, nhưng nó phải cung cấp một số lợi ích nào đó. Hãy xem xét các lý do được đưa ra để xem chúng có hợp lý không.
Tự động tạo CHANGELOG
Đây là lời hứa lớn nhất của Conventional Commits: bạn có thể chạy một công cụ để tạo changelog từ các commit kể từ lần phát hành cuối cùng của bạn. Đây có phải là một ý tưởng tốt không? Không! Khán giả của changelog hoàn toàn khác với khán giả của nhật ký commit!
- Một changelog dành cho người dùng cuối, họ quan tâm đến việc hiểu các khác biệt về chức năng giữa các phiên bản.
- Một nhật ký commit dành cho nhà phát triển, họ quan tâm đến việc đọc câu chuyện về cách cơ sở mã đã thay đổi theo thời gian.
Bạn có thể thấy, đây là hai góc nhìn hoàn toàn khác nhau và bất kỳ nỗ lực nào để kết hợp chúng đều dẫn đến kết quả kém. Trong bất kỳ dự án phức tạp nào, cần nhiều commit để hoàn thành một tính năng nổi bật. Quy trình hoàn thiện tính năng có giá trị cho nhà phát triển nhưng vô dụng cho người dùng cuối.
Tự động xác định tăng phiên bản ngữ nghĩa (Semantic Versioning)
Nghe có vẻ hay, nhưng thực tế của kỹ thuật phần mềm thường làm gián đoạn khả năng thực hiện chính xác nhiệm vụ này. Hãy xem xét các tình huống sau:
- Revert (Hoàn tác): Imagine bạn đã giới thiệu một thay đổi phá vỡ (breaking change) nghiêm trọng đến mức bạn phải hoàn tác nó? Công cụ của bạn sẽ phát hiện ra một thay đổi phá vỡ và tăng phiên bản chính (major), mặc dù sự cố phá vỡ thực sự đã được hoàn tác và không còn thay đổi phá vỡ nào tồn tại.
- Sự cố phá vỡ vô tình: Có thể sự cố phá vỡ rất tinh tế và bạn không nhận ra đó là thay đổi phá vỡ khi bạn thực hiện nó. Bạn sẽ tăng sai phiên bản.
Trong những tình huống như vậy, bạn có thể viết lại lịch sử bằng rebase, nhưng điều đó thường làm hỏng quy trình làm việc.
Kích hoạt quy trình xây dựng và xuất bản
Đây chỉ là một ý tưởng tồi. Giả sử bạn chỉ chạy các kiểm tra bảo mật tự động trên các commit chạm vào mã, sau đó ai đó tạo một commit Trojan ngựa có tiêu đề docs: fix typos nhưng thực sự lại đưa ra lỗ hổng vào hệ thống con xác thực? Rõ ràng, công cụ tự động đã bị bỏ qua.
Tính toán rẻ hơn, chỉ cần sử dụng git diff để xác định các tệp đã thay đổi (phạm vi, một lần nữa) và chạy quy trình xây dựng/xuất bản dựa trên đó.
Không một trong các "điểm bán" của Conventional Commits thực sự đứng vững.
Một cách tốt hơn
Vậy bạn nên làm gì thay thế? Hãy làm theo hướng dẫn của các dự án phần mềm thực sự thành công như Linux, FreeBSD, Git, Go và NixOS! Những dự án này có điểm chung gì? Họ đều sử dụng thông điệp commit có tiền tố phạm vi (trong đó "phạm vi" được định nghĩa là phù hợp với dự án thực tế).
Dưới đây là một số ví dụ về các dự án và hướng dẫn định dạng commit của họ:
- Linux:
subsystem: description(Ví dụ:i2c: virtio: mark device ready before registering the adapter) - FreeBSD:
prefix: Description(Ví dụ:linuxulator: Return EINVAL for invalid inotify flags) - Git:
area: description(Ví dụ:gitlab-ci: update macOS image) - Go:
package: description(Ví dụ:net/http/cookiejar: add godoc links)
Thật không may, mặc dù được sử dụng bởi một số dự án mã nguồn mở thành công nhất từng được tạo ra, phong cách commit này dường như đã thua trong cuộc chiến thương hiệu. Tôi định thay đổi điều đó bằng cách ủng hộ việc quay lại với lý trí trong thông điệp commit, và tách biệt mối quan tâm của việc tạo changelog khỏi việc quản lý nhật ký commit.
Kết luận
Những lợi ích được cho là của Conventional Commits thực ra chỉ là ảo ảnh và ngành công nghiệp chưa thấy được lợi ích thực tế nào từ việc sử dụng nó như một tiêu chuẩn. Tuy nhiên, Conventional Commits đáng tiếc đã trở nên khá phổ biến trong các dự án mã nguồn mở, và do đó, các AI dường như có thói quen mặc định sử dụng nó cho các thông điệp commit. Điều này đã gây ra sự lan truyền của các thông điệp commit chứa đầy anti-pattern (mẫu chống) trên các dự án.
Mục tiêu của tôi trong bài viết này là chống lại sự thống trị của Conventional Commits và chứng minh rằng có những cách tốt hơn để cấu trúc thông điệp commit. Hãy ưu tiên phạm vi (scope) thay vì loại (type) để lịch sử commit của bạn thực sự có ý nghĩa.
