Commit messages with Conventional Commits, and atomic commits Intermediate
Estimated reading time: 3 min read
This page answers: how to write commit messages that both people and tools can read, and how much change one commit should contain.
The Conventional Commits structure
Section titled “The Conventional Commits structure”Per Conventional Commits 1.0.0:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]The spec’s main rules:
featis required for a new feature,fixis required for a bug fix. Other types (docs,refactor,test,chore,perf,ci…) are allowed, but the spec does not define their meaning.- Breaking change:
!right before the:(feat(api)!: ...), or aBREAKING CHANGE: <description>footer. That token must be uppercase;BREAKING-CHANGEis a synonym. - Footers take the form
Token: valueorToken #value; tokens use-instead of spaces (Reviewed-by,Refs).
From commit to SemVer
Section titled “From commit to SemVer”Loading diagram…
A breaking change bumps MAJOR regardless of type: fix!: is also MAJOR.
Before and after
Section titled “Before and after”Before — the log tells you nothing:
a1b2c3d fixd4e5f6a update order service9f8e7d6 wipAfter — each line answers “what changed, where”:
fix(order): round the discounted total to whole VND
Previously a 10% discount on a 199,999 VND order returned 179,999.1 andstored a decimal in a bigint column, making the reconciliation job off by 1 VND.Round down at the final step, keep integers in the intermediate steps.
Refs: PAY-482Why the subject should be ≤ 50 characters
Section titled “Why the subject should be ≤ 50 characters”| Source | Recommendation |
|---|---|
git commit docs (DISCUSSION) |
First line “no more than 50 characters”, then a blank line, then the description |
| Pro Git | Wrap the body at about 72 characters; imperative mood (“Fix bug”, not “Fixed bug”) to match the messages git merge and git revert generate |
The first line is a title used everywhere: git log --oneline, PR titles, format-patch emails, the GitHub UI (truncated when too long). The body answers why; the diff already says what.
Atomic commits: separate refactors from behavior changes
Section titled “Atomic commits: separate refactors from behavior changes”Atomic commit = one logical change that builds and passes tests on its own. The most important rule: refactors (no behavior change) and behavior changes go in separate commits.
Before — one commit doing three things:
feat(order): support percentage vouchers src/order/pricing.ts | 180 +++++---- (rename variables, extract functions, change logic) src/order/order.module.ts | 12 +- (change DI) package.json | 2 +- (upgrade lodash)After — three commits, each reviewable and revertable on its own:
chore(deps): upgrade lodash 4.17.20 → 4.17.21refactor(order): extract pricing into PricingService # old tests pass, no behavior changefeat(order): support percentage vouchers # small diff, only the new logic| Task | Combined commit | Atomic commits |
|---|---|---|
| Review | Renames and logic are mixed together | The refactor commit only needs “does behavior change?” — passing old tests is the evidence |
| Voucher causes a bug | revert also removes the refactor and the lodash upgrade |
git revert exactly the feat commit |
git bisect |
Points at a 200-line commit | Points at a 20-line commit |
Tools:
git add -p: stage hunk by hunk, splitting one file into several commits.git commit --fixup=<sha>+git rebase -i --autosquash: fix an old commit in a PR (see Branch, merge, rebase).
Next: Branch, merge, rebase.