Skip to content

02. Conventional Commits

IMPORTANT

Tư duy đúng: Conventional Commits không tồn tại để “làm đẹp cú pháp”. Nó là chính sách workflow giúp team đọc lịch sử nhanh hơn, review PR dễ hơn, và tự động hóa release/changelog ít rủi ro hơn.

Vì sao đội kỹ thuật cần quy ước này?

Khi lịch sử commit đầy những dòng như update, fix bug, wip, reviewer không biết:

  • thay đổi đó là tính năng, bug fix, hay chỉ là refactor
  • commit đó có nên xuất hiện trong changelog hay không
  • có tín hiệu nào cho thấy đây là thay đổi breaking không
  • nếu production lỗi, commit nào đáng nghi nhất để rollback hoặc điều tra

Conventional Commits giải quyết đúng bài toán đó: biến lịch sử commit thành một log có cấu trúc, đọc được bởi cả con người lẫn tooling.

text
Không có quy ước:
update stuff
final fix
wip
change api

Có quy ước:
feat(auth): add refresh token rotation
fix(payments): prevent duplicate webhook processing
refactor(cache): isolate key builder for invoice queries
docs(git): clarify rebase policy for shared branches

Conventional Commits giải quyết vấn đề gì?

1. Tăng khả năng review

Reviewer nhìn commit message là biết ngay mục đích thay đổi:

  • feat(...) → có hành vi mới
  • fix(...) → sửa lỗi hiện hữu
  • refactor(...) → tái cấu trúc nhưng không đổi behavior mong muốn
  • docs(...) → tài liệu

Điều này làm review bớt mơ hồ. Khi một PR có 8 commit rõ ràng, reviewer không phải đoán “commit này để làm gì?”.

2. Làm lịch sử dễ điều tra

Khi cần dùng git log, git blame, git bisect, hoặc quyết định revert, commit message rõ ràng giúp bạn khoanh vùng nhanh hơn.

Ví dụ:

  • fix(auth): reject expired password reset token
  • feat(search): add Vietnamese synonym expansion

Chỉ nhìn lịch sử, bạn đã có ngữ cảnh kỹ thuật đủ tốt để điều tra.

3. Hỗ trợ release và changelog ở mức thực dụng

Bạn không cần hệ thống release quá phức tạp mới có lợi từ Conventional Commits.

Chỉ ở mức tối thiểu, team đã có thể:

  • gom feat vào phần tính năng mới
  • gom fix vào phần sửa lỗi
  • bỏ qua hoặc giảm trọng số các commit docs, test, chore
  • nhận biết thay đổi breaking để cảnh báo release

Nói ngắn gọn: lịch sử commit tốt giúp release note bớt viết tay và bớt sai.

Cú pháp chuẩn, nhưng đừng học nó như mẹo thuộc lòng

Format phổ biến:

text
<type>(<scope>): <description>

Hoặc khi không cần scope:

text
<type>: <description>

Ví dụ:

bash
feat(auth): add refresh token rotation
fix(payments): prevent duplicate webhook processing
docs(git): clarify commit policy for release branches
refactor(api): extract request signature validator
test(checkout): cover expired coupon scenario
chore(ci): cache pnpm store in GitHub Actions

TIP

Hãy đọc format này như một câu trả lời ngắn cho reviewer: “Loại thay đổi gì, ở khu vực nào, và đã làm điều gì?”

Các type dùng nhiều nhất trong engineering work

TypeDùng khi nàoKhông nên lạm dụng cho
featThêm hành vi hoặc capability mới cho user/systemMọi thay đổi chỉ vì “nghe quan trọng”
fixSửa lỗi behavior sai, race condition, edge case, regressionRefactor hoặc cleanup không đổi behavior
docsThay đổi tài liệu, hướng dẫn, comment public-facingChe giấu thay đổi code thật
refactorĐổi cấu trúc code mà không đổi behavior kỳ vọngBug fix trá hình
testThêm/sửa testCode production kèm theo nhưng không nói rõ
choreViệc bảo trì: dependency, config, tooling, scriptsFeature hoặc fix thật sự
styleFormat, whitespace, lint-only, không đổi logicBất kỳ thay đổi có ảnh hưởng runtime
build / ciBuild pipeline, release pipeline, CI/CDCông việc không liên quan build/CI
perfTối ưu hiệu năng có chủ đíchRefactor thông thường chưa đo được lợi ích

Scope: hữu ích khi nó phản ánh đúng boundary kỹ thuật

scope giúp trả lời câu hỏi: thay đổi này nằm ở khu vực nào của hệ thống?

Ví dụ scope tốt:

  • auth
  • payments
  • checkout
  • search
  • ci
  • git
  • docs

Ví dụ:

bash
fix(auth): invalidate stolen refresh token on reuse
feat(checkout): support bank transfer confirmation flow
docs(git): add warning about rebasing shared history
chore(ci): upload build artifact for preview deployment

Khi nào không cần scope?

Không bắt buộc phải gắn scope nếu nó chỉ làm commit dài và giả tạo.

Ổn:

bash
docs: fix typo in installation guide
chore: update prettier to latest stable version

Không ổn:

bash
docs(everything): update docs
fix(misc): fix some issues
feat(final): finish feature

Nguyên tắc: scope phải phản ánh boundary thật, không phải để “trông chuyên nghiệp”.

Breaking change: tín hiệu sống còn cho release

Nếu một commit làm thay đổi contract theo cách có thể phá consumer hiện tại, nó phải phát tín hiệu rõ.

Hai cách phổ biến:

Cách 1: dùng !

bash
feat(api)!: rename invoice status field from state to status
text
feat(api): rename invoice status field

BREAKING CHANGE: API response field `state` was removed and replaced by `status`.

Khi nào nên gắn breaking?

  • đổi contract API
  • đổi event name mà consumer đang dùng
  • đổi CLI flag hoặc config key
  • đổi migration theo cách không tương thích ngược

Khi nào không nên gắn breaking?

  • chỉ refactor nội bộ
  • rename biến nội bộ
  • cleanup code không ảnh hưởng contract

WARNING

Gắn BREAKING CHANGE sai làm release note gây hoảng loạn không cần thiết. Không gắn khi không có impact thật lên consumer.

Các anti-pattern phổ biến

1. wip, tmp, final, update

Đây là các commit message gần như vô nghĩa:

bash
wip
tmp fix
final update
update code

Chúng thất bại ở cả 3 mặt:

  • reviewer không hiểu mục đích
  • người viết changelog không phân loại được
  • người điều tra incident không đoán được commit nào đáng nghi

Thay bằng:

bash
fix(session): refresh csrf token after forced logout
refactor(editor): split markdown parsing from toolbar state
docs(release): add rollback note for hotfix deployment

2. Fake scope

Ví dụ:

bash
fix(core): fix typo in button label
chore(auth): update all dependencies
feat(api): rename local variable

Scope giả làm lịch sử “có vẻ chuẩn” nhưng lại gây nhiễu. Nếu thay đổi không thuộc core, đừng nhét vào core.

3. Dùng chore để che mọi thứ

chore là thùng rác phổ biến nhất trong repo kỷ luật kém.

Ví dụ xấu:

bash
chore: add coupon validation to checkout
chore: prevent duplicate payment retries

Đây không phải chore; đây là feat hoặc fix.

4. Một commit ôm nhiều loại thay đổi

Ví dụ:

bash
feat(auth): add refresh token rotation and fix logout bug and update docs

Một commit như vậy làm mất tính atomic. Nếu có thể, hãy tách ra:

bash
feat(auth): add refresh token rotation
fix(auth): clear device session on forced logout
docs(auth): document token rotation flow

Ví dụ thực tế từ engineering work

Ví dụ 1: Backend auth

bash
feat(auth): add refresh token rotation
fix(auth): reject reused refresh tokens
test(auth): cover stolen token replay scenario

Tại sao tốt:

  • mô tả đúng intent
  • dễ review theo từng bước
  • release note biết đâu là feature, đâu là bug fix

Ví dụ 2: Payments và production incident

bash
fix(payments): prevent duplicate webhook processing
perf(payments): reduce lock contention in reconciliation job
docs(payments): document retry idempotency assumptions

Khi production có duplicate charge, commit fix(payments) sẽ nổi bật hơn nhiều so với update payment logic.

Ví dụ 3: Frontend + CI

bash
feat(checkout): show bank transfer status timeline
fix(checkout): preserve selected coupon after page refresh
ci(preview): upload Lighthouse report artifact

Lịch sử này giúp team release biết:

  • có feature cho checkout
  • có bug fix user-facing
  • có thay đổi CI nhưng không phải product behavior

Khi nào không nên dùng hoặc không nên lạm dụng?

Conventional Commits rất hữu ích, nhưng đừng biến nó thành nghi thức máy móc.

Không nên lạm dụng khi:

  1. Commit chưa atomic

    • Nếu thay đổi đang trộn feature + refactor + debug log, vấn đề chính không phải là thiếu type.
    • Vấn đề là bạn cần quay lại staging và tách commit.
  2. Cố nhồi scope cho mọi commit

    • Scope không phải KPI.
    • Scope vô nghĩa còn tệ hơn không có scope.
  3. Dùng commit message để che giấu commit xấu

    • Một commit khổng lồ không trở nên tốt chỉ vì đặt tên feat(checkout): improve payment flow.
  4. Rewrite history đã chia sẻ

    • Đừng interactive rebase hoặc sửa hàng loạt commit trên nhánh public mà người khác đang dựa vào, chỉ để “đẹp lịch sử”.

Recovery: nếu bạn viết sai commit message thì làm sao?

Tin tốt: lỗi commit message thường phục hồi được, miễn là bạn hiểu commit đó đã đi xa tới đâu.

Trường hợp 1: Commit cuối cùng, chưa push

Dùng:

bash
git commit --amend

Hoặc sửa nhanh chỉ message:

bash
git commit --amend -m "fix(payments): prevent duplicate webhook processing"

Nên dùng khi:

  • viết nhầm chore thay vì fix
  • quên scope
  • description quá mơ hồ

Trường hợp 2: Nhiều commit local đều viết chưa tốt

Dùng interactive rebase:

bash
git rebase -i HEAD~4

Bạn có thể:

  • reword để đổi message
  • squash để gộp commit vụn
  • fixup để nhập commit cleanup vào commit chính

WARNING

Chỉ làm điều này trên private history — tức là commit chưa được push lên nhánh shared, hoặc bạn chắc chắn không ai khác đang dựa vào chúng. Rewriting shared history chỉ để “dọn đẹp” là cách tạo ra incident cộng tác.

Trường hợp 3: Đã push lên nhánh dùng chung

Ưu tiên an toàn hơn thẩm mỹ:

  • đừng rebase lại lịch sử chỉ để đổi message
  • giữ nguyên commit nếu impact nhỏ
  • nếu commit sai bản chất nghiêm trọng, tạo commit mới để sửa phần nội dung thật
  • nếu team cho phép force push trên branch cá nhân, chỉ dùng --force-with-lease, không dùng force mù quáng

Thực tế production-first: lịch sử hơi xấu còn tốt hơn lịch sử shared bị bẻ gãy.

Quy trình đề xuất cho team

text
Code thay đổi
  -> tách thành atomic commit
  -> đặt Conventional Commit phản ánh đúng intent
  -> review lại bằng git log --oneline
  -> push branch
  -> PR / review / merge
  -> dùng lịch sử để hỗ trợ changelog và release note

Checklist trước khi commit

  • Commit này làm một việc chưa?
  • type có phản ánh đúng bản chất thay đổi không?
  • scope có thật sự giúp người đọc hiểu hơn không?
  • Có phải breaking change không?
  • Nếu nhìn lại sau 3 tháng, message này có còn hữu ích không?

Thực hành đọc lịch sử như một kỹ sư release

Giả sử bạn thấy lịch sử:

text
feat(search): add typo-tolerant product lookup
fix(search): ignore archived products in autocomplete
refactor(search): extract ranking strategy interface
docs(search): document indexing delay expectations

Bạn có thể suy ra:

  • release này có thêm capability tìm kiếm
  • có một bug fix user-facing
  • có refactor nội bộ cần review nhưng không nên xuất hiện đậm trong changelog
  • docs đã được cập nhật cho behavior vận hành

Đó chính là giá trị thực dụng của Conventional Commits.

🧠 Quiz

Câu 1: Conventional Commits giải quyết vấn đề chính nào?

  • [ ] A. Làm Git chạy nhanh hơn
  • [x] B. Làm lịch sử dễ review, dễ phân loại và hỗ trợ automation cho release/changelog
  • [ ] C. Tự động sửa conflict khi merge
  • [ ] D. Thay thế hoàn toàn code review

💡 Giải thích: Giá trị thật của Conventional Commits nằm ở workflow và automation, không phải ở việc “đúng cú pháp cho vui”.

Câu 2: Trường hợp nào là anti-pattern?

  • [ ] A. fix(auth): reject expired reset token
  • [x] B. wip
  • [ ] C. docs(git): clarify rebase policy
  • [ ] D. feat(checkout): support invoice download

💡 Giải thích: wip không cho reviewer, releaser, hay người điều tra incident biết commit đó thực chất làm gì.

Câu 3: Khi nào được dùng interactive rebase để sửa commit message?

  • [ ] A. Luôn luôn, kể cả trên main
  • [x] B. Khi đang làm trên private history, chưa ảnh hưởng tới người khác
  • [ ] C. Chỉ khi commit là feat
  • [ ] D. Chỉ khi có hơn 10 commit

💡 Giải thích: Rewriting shared history chỉ để làm đẹp là thói quen nguy hiểm. Rebase tương tác phù hợp cho lịch sử local hoặc branch riêng chưa chia sẻ.