Open up git log and look at your last 100 commits. How many do you actually remember? Commits like “fix bug”, “update”, “wip”, “stuff” are lost history. Good commit messages are the memory of your codebase.
In 19 years as a developer, I’ve tried to install this discipline in plenty of teams. Here’s the practical approach I keep coming back to.
Why commit messages matter
1. Future debugging. Six months later, the answer to “why is this line like this?” lives in the commit message.
2. Code review context. A reviewer reads the diff, but also the commit message. It should answer “why did you make this change?”.
3. Release notes generation. Release notes can be auto-generated from commits.
4. Bisect debugging. When you’re using git bisect to find where a bug crept in, commit messages are the signposts.
5. Onboarding new developers. Git log is the evolution story of your codebase.
Bad commit messages throw away all five of these.
Conventional commits format
The most widespread standard: Conventional Commits.
<type>(<scope>): <subject>
<body>
<footer>Types:
– feat: new feature
– fix: bug fix
– docs: documentation change
– style: code style (whitespace, formatting, no logic change)
– refactor: code restructure, no behavior change
– perf: performance improvement
– test: adding or updating tests
– chore: maintenance task (dependency update, config)
Example:
feat(checkout): add Apple Pay option
Implement Apple Pay as alternative payment method alongside
credit card. Uses Stripe's PKPaymentRequest API.
Closes #234This format is machine-parseable. It unlocks changelog generation and semantic versioning automation.
Subject line rules
The first line (subject) is the most important part. Rules:
- Max 72 characters. Git tooling truncates beyond that.
- Imperative mood. “add feature”, not “added feature”.
- No period at the end.
- Lowercase after the type. Conventional commits prefers lowercase subjects.
- Concise but descriptive.
Good subjects:
feat(auth): add two-factor authentication
fix(api): prevent duplicate order creation on network retry
refactor(db): extract query builder into separate class
perf(list): memoize expensive filter computationBad subjects:
update (update what?)
fix bug (which bug?)
wip (who cares)
misc changes (vague)Body: why, not what
Writing “what I changed” in the body is redundant. The diff already shows that. The body exists to explain “why”.
Bad body:
Changed line 45 in UserService.swift.
Updated the validation logic.
Added a new parameter.Good body:
The existing validation allowed empty emails to pass through,
causing NULL values in the database. This led to the bug #412
where notification emails failed silently.
Now requires @ symbol and non-empty domain.With “why” and “impact”, the reviewer gets the context they need.
Atomic commits
One commit equals one logical change. Mixed commits are sloppy.
Bad (mixed):
feat: add user profile, fix login bug, update depsThree different things in one commit. Revert is painful (you can’t revert just the login fix). Review is painful.
Good (atomic):
commit 1: feat(profile): add user profile page
commit 2: fix(auth): prevent login loop after token expiry
commit 3: chore(deps): update lodash to 4.17.21Three separate commits. Each one is self-contained, revertable, reviewable.
Branch naming
Commit discipline goes hand in hand with branch naming:
feature/checkout-apple-pay
bugfix/email-validation-nil-check
hotfix/payment-timeout
chore/update-firebase-sdkBranch type as prefix, kebab-case descriptive name. For GitHub or GitLab issue integration, add the issue number:
feature/234-apple-pay-checkoutAmend and squash
git commit --amend fixes the most recent commit. Typo in the message, forgotten file.
git rebase -i HEAD~5 lets you interactively rewrite multiple commits:
– reword: change the commit message
– squash: combine multiple commits
– fixup: squash but drop the message
– drop: delete the commit
When to squash: during PR review you accumulate five commits like “fix typo”, “fix another typo”, “actually fix”. Before merge, squash:
git rebase -i main
# combine with the "squash" markerResult: one clean commit at PR merge time.
Enforcing with git hooks
For team-wide discipline, use git hooks:
commit-msg hook: checks commit message format.
#!/bin/sh
# .git/hooks/commit-msg
commit_regex='^(feat|fix|docs|style|refactor|perf|test|chore)(([a-z-]+))?: .{1,72}'
if ! grep -qE "$commit_regex" "$1"; then
echo "Commit message format invalid."
echo "Format: type(scope): subject"
exit 1
fiTools like Husky (Node.js projects) or pre-commit (Python) make hook management painless.
Team onboarding
How to explain commit discipline to a new developer:
- Commit convention in the README. Link to the Conventional Commits spec.
- PR template. The body asks “how should a reviewer judge this?”.
- Pair commit writing. For the first week, have a senior review commit messages.
- Reject PRs with bad commits. Leave feedback: “please squash into meaningful commits”.
Three or four weeks in, the pattern becomes second nature.
Multi-commit PR vs single-commit
Two PR approaches:
Option A: one commit per PR. Squash on merge. Clean final history.
Option B: multi-commit PR. Each commit is a logical unit. Merge commit or rebase merge.
I prefer Option B on large PRs. Five to ten commits with a logical breakdown is much easier to review. On small PRs (under 100 lines), Option A is fine.
Tools: git log visualization
Ways to read git log:
git log --oneline
git log --graph --oneline --all
git log --pretty=format:'%h %s (%an)' --abbrev-commitVisual tools: GitLens (VS Code), Fork, Tower. They visualize commit history nicely.
Wrap-up
Commit discipline is core to long-term team productivity. At first it feels like over-discipline; six months later, when you’re doing code archaeology, you understand its value.
Conventional Commits plus atomic commits plus a meaningful subject and body. Enforce with hooks, enforce in PR review, teach during onboarding. After a month of discipline, it becomes natural for everyone.