A git history is a log of decisions, not just a log of changes. When a bug appears six months from now and someone runs git log --oneline, they are not looking for a list of files changed — they are looking for the reasoning behind each change. A good commit message takes 60 seconds to write and saves hours of archaeological work. A bad one is permanently useless.
What changed in 2026
- Conventional Commits is the industry default. Libraries, CLI tools, and AI commit message generators all default to
feat:, fix:, docs:, chore: prefixes. It is not an opinion anymore — it is the standard.
- Automated changelogs from commits are mainstream. Tools like
release-please, semantic-release, and changesets generate changelogs and bump semver versions directly from commit messages. Your commit messages are now release notes.
- AI commit message generation is good enough to use as a draft. VS Code, JetBrains, and CLI tools can suggest a message from the diff — but they produce "what," not "why." Always add the why yourself.
- Co-author and signed commits are common. AI pair programming tools add
Co-Authored-By: trailers; regulated industries require GPG-signed commits.
The anatomy of a commit message
<type>(<scope>): <subject> ← subject line, max 72 chars
← blank line (required)
<body> ← why, context, tradeoffs (optional)
← blank line
<footer> ← issue refs, breaking changes (optional)
The subject line rules
- Use a type prefix —
feat, fix, docs, style, refactor, test, chore, perf, ci.
- Imperative mood — "Add", "Fix", "Remove", not "Added", "Fixes", "Removing".
- No full stop at the end.
- Maximum 72 characters — many tools truncate at 72; keep it under that.
- Lowercase after the colon (Conventional Commits convention).
feat(auth): add rate limiting to login endpoint
fix(payments): prevent duplicate charge on network timeout
refactor(users): extract email validation into shared util
docs(api): add pagination examples to REST guide
chore(deps): bump express from 4.18.3 to 4.19.2
The body
The body is optional but valuable for anything non-obvious. Answer:
- Why was this change made?
- What alternative was considered and why was it rejected?
- What is the tradeoff or known limitation?
feat(billing): switch to idempotency keys for Stripe charges
Previously we retried failed charges by re-submitting the request.
This caused duplicate charges when the network failed after Stripe
had processed the payment but before we received the 200 response.
Idempotency keys (scoped to order ID + attempt number) let Stripe
return the same result for duplicate requests within 24 hours,
eliminating the duplicate-charge race condition entirely.
The footer
feat(auth): add TOTP two-factor authentication
Implements RFC 6238 TOTP using the otpauth library.
Backup codes are hashed with bcrypt before storage.
BREAKING CHANGE: /auth/login now returns 202 Accepted with a
session_token when 2FA is enabled, instead of 200 with a full JWT.
Clients must handle the 2FA challenge flow.
Closes #847
Refs JIRA-1234
Co-Authored-By: Alice <alice@example.com>
Conventional Commits type reference
| Type |
When to use |
feat |
New feature for the user |
fix |
Bug fix for the user |
docs |
Documentation only |
style |
Formatting, whitespace — no logic change |
refactor |
Code restructure — no feature or fix |
test |
Adding or correcting tests |
chore |
Maintenance — build system, deps, tooling |
perf |
Performance improvement |
ci |
CI/CD configuration changes |
revert |
Reverting a previous commit |
Bad vs good examples
| Bad |
Good |
fix bug |
fix(orders): correct off-by-one in pagination limit |
WIP |
feat(search): add partial match for product names (draft) |
changes |
refactor(db): replace raw SQL with typed query builder |
update deps |
chore(deps): upgrade to Node 22 LTS, drop Node 20 |
added tests |
test(auth): add coverage for expired token refresh path |
Setting up a commit template
Store a template so your team uses it consistently:
# Create a template
cat > ~/.gitmessage << 'EOF'
# <type>(<scope>): <subject> — max 72 chars, imperative, no period
# Types: feat fix docs style refactor test chore perf ci revert
# Body: explain WHY, not WHAT. What alternatives were considered?
# Footer: Closes #<issue> | BREAKING CHANGE: <desc>
EOF
# Tell git to use it
git config --global commit.template ~/.gitmessage
For a team-wide template, commit it to the repo and set up a git hook:
# .git/hooks/commit-msg (or via Husky)
#!/bin/sh
# Enforce Conventional Commits subject line
pattern='^(feat|fix|docs|style|refactor|test|chore|perf|ci|revert)(\(.+\))?: .{1,72}#39;
subject=$(head -1 "$1")
if ! echo "$subject" | grep -qE "$pattern"; then
echo "ERROR: Commit message does not follow Conventional Commits."
echo "Expected: <type>(<scope>): <subject>"
exit 1
fi
Husky + commitlint is the standard automated enforcement in 2026:
// package.json
{
"devDependencies": {
"@commitlint/cli": "^19",
"@commitlint/config-conventional": "^19",
"husky": "^9"
}
}
// commitlint.config.js
export default { extends: ['@commitlint/config-conventional'] };
How to pick what to put in the body
Ask yourself: "Would a developer 12 months from now, seeing this commit in git blame, understand why this code exists the way it does?"
If the subject line alone answers that — no body needed. If the change involved a non-obvious decision, a rejected alternative, a security tradeoff, or a workaround for a third-party bug — write a body.
Common mistakes
Past tense subject lines. Added validation — make it add validation. The imperative reads as "if you apply this commit, it will add validation."
Padding the subject. feat: various improvements and bug fixes — useless. Split into separate commits.
No reference to the issue. A commit that closes a bug without Closes #123 breaks the traceability chain.
Committing unrelated changes together. A commit that refactors AND adds a feature creates noise in the changelog and makes rollback painful. One logical change per commit.
What to skip
- Emoji-first commit messages (
✨ feat: ..., 🐛 fix: ...) unless your team has explicitly agreed. They are unsearchable and cluttered in many log views.
- Vague scopes —
(misc), (various), (other) are not useful. Name the specific module or service.
- Closing multiple unrelated issues in one commit. It signals the commit is doing too much.
FAQ
How granular should commits be?
One logical unit of work. You should be able to describe the commit in one subject line without "and." If you need "and," split it.
What about merge commits vs squash-merge?
Squash-merge is popular for keeping main history clean; but you lose the commit-level context of the PR. Use squash for small PRs, preserve commits for large, multi-commit PRs where individual commits add context.
Should I rewrite commit messages before merging?
Yes — rebase or amend to clean up "WIP" and fixup commits before they land on main. On main, never rewrite history.
Does this matter for solo projects?
Yes, because your future self is the audience. Six-month-old-you will be very grateful for a descriptive message.
Where to go next