conventional-commits
Write the message from the diff, not from the conversation. The reader is a
stranger running git log in two years.
Procedure
- Read what is actually staged:
git diff --cached --statthen the diff itself. - Classify the change — pick ONE primary type:
featnew user-visible capability ·fixcorrects wrong behaviorrefactorno behavior change ·perffaster, same behaviordocs·test·build·ci·chore(only when nothing else fits)
- Scope = the subsystem a reader would filter by:
feat(parser): …. Omit if the repo doesn't use scopes (checkgit log --oneline -20and match style). - Subject line: imperative, lowercase, no period, ≤ 72 chars, and it must state the effect, not the activity — "fix: reject expired tokens" not "fix: update token check".
- Body (when the why isn't obvious): 1–3 sentences on why the change was needed and any non-obvious consequence. Wrap at 72. Never narrate the diff.
- Breaking change → footer
BREAKING CHANGE: <what breaks and the migration>, and add!after the type:feat(api)!: ….
Anti-patterns to reject
- One commit doing two unrelated things — split it (
git add -p). - "update", "misc fixes", "address comments" — say what changed.
- Restating the conversation ("as discussed…") — the log has no conversation.
- Explaining that the change is correct — that belongs in the PR, not the log.
Example
fix(ingest): treat mtime regressions as edits, not new files
Restored backups can move mtime backwards; we indexed those as fresh
documents, duplicating every chunk. Key on inode+path instead.