Giao diện
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 branchesConventional 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ớifix(...)→ sửa lỗi hiện hữurefactor(...)→ tái cấu trúc nhưng không đổi behavior mong muốndocs(...)→ 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 tokenfeat(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
featvào phần tính năng mới - gom
fixvà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 ActionsTIP
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
| Type | Dùng khi nào | Không nên lạm dụng cho |
|---|---|---|
feat | Thêm hành vi hoặc capability mới cho user/system | Mọi thay đổi chỉ vì “nghe quan trọng” |
fix | Sửa lỗi behavior sai, race condition, edge case, regression | Refactor hoặc cleanup không đổi behavior |
docs | Thay đổi tài liệu, hướng dẫn, comment public-facing | Che giấu thay đổi code thật |
refactor | Đổi cấu trúc code mà không đổi behavior kỳ vọng | Bug fix trá hình |
test | Thêm/sửa test | Code production kèm theo nhưng không nói rõ |
chore | Việc bảo trì: dependency, config, tooling, scripts | Feature hoặc fix thật sự |
style | Format, whitespace, lint-only, không đổi logic | Bất kỳ thay đổi có ảnh hưởng runtime |
build / ci | Build pipeline, release pipeline, CI/CD | Công việc không liên quan build/CI |
perf | Tối ưu hiệu năng có chủ đích | Refactor 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:
authpaymentscheckoutsearchcigitdocs
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 deploymentKhi 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 versionKhông ổn:
bash
docs(everything): update docs
fix(misc): fix some issues
feat(final): finish featureNguyê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 statusCách 2: thêm footer BREAKING CHANGE:
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 codeChú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 deployment2. Fake scope
Ví dụ:
bash
fix(core): fix typo in button label
chore(auth): update all dependencies
feat(api): rename local variableScope 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 docsMộ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 flowVí 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 scenarioTạ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 assumptionsKhi 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 artifactLị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:
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.
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.
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.
- Một commit khổng lồ không trở nên tốt chỉ vì đặt tên
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 --amendHoặ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
chorethay 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~4Bạn có thể:
rewordđể đổi messagesquashđể gộp commit vụnfixupđể 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 noteChecklist trước khi commit
- Commit này làm một việc chưa?
typecó phản ánh đúng bản chất thay đổi không?scopecó 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 expectationsBạ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:
wipkhô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ẻ.