GitWriting Good Commit Messages

Writing Good Commit Messages

Commit messages are notes for the future — for your teammates, your reviewers, and yourself in six months. The diff already shows what changed; the commit message exists to explain why. A well-written commit message can save hours of detective work later. A bad one (“fix stuff”, “asdf”, “WIP”) is a permanent low-effort marker in the history of the project.

The widely accepted format

The 7 rules of a great commit message (Tim Pope, ca. 2008)

Text
Subject line ≤ 50 chars in the imperative mood

Blank line, then a longer body wrapped at 72 characters.
Explain WHAT changed and WHY — not how. The diff shows how.
Reference issues, design docs, links to context as needed.

- Bullet lists are fine.
- Use the present-tense imperative: "Add", "Fix", "Refactor".

Refs: #1234
The seven rules
  • 1. Separate subject from body with a blank line. This is the most-broken rule.

  • 2. Limit the subject line to 50 characters. Git and many tools truncate at 50–72.

  • 3. Capitalise the subject line. "Add", not "add".

  • 4. Do not end the subject line with a period. It is a title, not a sentence.

  • 5. Use the imperative mood. "Fix typo", not "Fixed typo" or "Fixes typo". Pretend you are giving an order to the codebase.

  • 6. Wrap the body at 72 characters. Long lines wrap badly in many tools.

  • 7. Use the body to explain WHAT and WHY, not HOW. The diff already shows how.

Good examples

A small, clear commit message

Text
Fix off-by-one error in pagination

Pages were rendering one item less than expected on the last
page. The cause was an inclusive comparison in calculatePages()
that should have been exclusive.

Closes #214

A medium-sized message with context

Text
Replace bcrypt with argon2id for password hashing

bcrypt is showing its age — it has a hard 72-byte input limit
and tooling around it is in maintenance mode. argon2id is the
current OWASP-recommended algorithm and is more resistant to
GPU-based attacks.

Migration plan:
- Existing hashes still verify (we keep the bcrypt fallback)
- New passwords are stored with argon2id
- A background job will gradually re-hash old passwords on
  successful login

Refs: SEC-37
Bad examples (and why)

Don't do these

Text
"fix"                   ← what did you fix?
"updated stuff"         ← which stuff?
"asdf"                  ← what does this mean?
"final changes"         ← they are never the final changes
"more fixes"            ← more of what?
"WIP"                   ← never commit "WIP" to a shared branch
"."                     ← please no
Imperative mood — the trick

A good test: the subject line should complete the sentence:

If applied, this commit will __________

Text
✓ "Add login form"             ← complete: "...add login form"
✓ "Fix race condition in queue" ← complete
✗ "Added login form"           ← past tense — wrong
✗ "Adds login form"            ← present tense — wrong
✗ "Adding login form"          ← gerund — wrong

Git itself uses imperative mood in messages it generates (like "Merge branch ..." and "Revert ..."). Matching that style keeps the log readable.

Multi-line messages

For larger changes, the body is where the real value lives. A typical structure:

  • Subject line — a 50-character summary.

  • Blank line.

  • Body — answer "Why did we do this?", "What problem did it solve?", "What alternatives did we consider?". Wrap at 72 chars.

  • Blank line.

  • Footer — references to issues, breaking changes, co-authors. Examples: Refs: #1234, BREAKING CHANGE: …, Co-authored-by: ….

Commit messages for fixes

A bug-fix commit benefits from describing: (1) what the user- visible symptom was, (2) the root cause, (3) the fix.

Text
Fix crash when search query contains a slash

User report: searching for "and/or" crashes the page.

Root cause: the search route used `encodeURI` instead of
`encodeURIComponent`, which left forward slashes unencoded.
The router then interpreted "and/or" as two path segments and
404’d, but the error path threw because the query parser
expected a string.

Fix: switch to encodeURIComponent and add a regression test.

Fixes #481
Conventional Commits

A popular convention (used by many open-source projects) puts a type prefix on every commit:

Text
feat:     a new feature
fix:      a bug fix
docs:     documentation only
style:    formatting, no logic change
refactor: code change that neither fixes a bug nor adds a feature
perf:     performance improvement
test:     adding or fixing tests
chore:    routine maintenance, build configs, etc.
ci:       CI/CD configuration

# With scope (optional):
feat(auth): add OAuth login

These prefixes are friendly to automated tooling — release- please, semantic-release, and similar tools auto-generate changelogs and version numbers from them.

References and footers

Common footers

Text
Refs: #1234, ABC-42       ← reference issues
Fixes: #1234              ← GitHub auto-closes the issue on merge
Closes: #1234             ← same
Co-authored-by: Name <email>   ← gives credit on GitHub
Signed-off-by: Name <email>    ← used by Linux kernel (-s flag)
BREAKING CHANGE: ...      ← signals semver-major change
Tips for writing better messages
  • Write the subject as if you were emailing a colleague: "what does this commit do?"

  • If you cannot summarise it in 50 chars, you probably committed too much. Use git add -p to split it.

  • Read your commit message back. Does it tell you anything the diff does not?

  • When in doubt, write a longer body. Future-you will thank present-you.

  • Use the team’s style — every team has slightly different conventions. Follow them.

A commit message template

~/.gitmessage

Bash
# Subject: imperative, ≤ 50 chars
#
# Body: explain WHAT changed and WHY, wrap at 72 chars.
# Reference issues at the bottom.
#
# Refs:

Bash
git config --global commit.template ~/.gitmessage
Commits as documentation
A great commit log is documentation that never goes out of date. You can search it (`git log --grep`), blame it (`git blame`), and pull individual commits into other branches. Treat each message as a tiny note to the future.
Tip
If you genuinely cannot think of a good message, that is a signal the commit is too big or unfocused. Reset, stage in smaller chunks with `git add -p`, and commit those individually.