Writing Commit Messages That Mean Something
Open a random project on GitHub and run git log --oneline. You’ll see something like this:
f3a91cc fix
e7b20d1 update
d4c38fa changes
9a1e77f wip
b832c10 asdf
a3f91d2 fix for real this time
This is not a commit history. It’s a list of timestamps with noise attached.
The problem isn’t that developers are careless. Most people who write “fix” as a commit message aren’t being lazy - they just haven’t thought about who reads commit messages and when.
Who reads commit messages
Commit messages aren’t written for the moment of the commit. They’re written for the moment someone runs git blame on a line that’s causing a production incident at 11pm and needs to understand, in thirty seconds, why that line exists.
That person might be a colleague. It might be you in eight months, with zero memory of writing the code. Either way, the diff tells them what changed. The commit message is the only place that explains why.
The diff is always available. The context - the reasoning, the tradeoff, the ticket, the constraint - disappears the moment you close the pull request.
What a commit message needs to do
A useful commit message answers one question: why did this change happen?
Not “what does this code do” - you can read the code for that. Not “what files were changed” - you can run git show. The why is the thing that doesn’t survive anywhere else.
Sometimes the why is simple: “fix a nil pointer dereference when the user has no billing address.” That’s enough. It tells you what was wrong, where, and under what condition. Anyone hitting a bug in that area now has a lead.
Sometimes the why is deeper: a constraint you worked around, a library bug you hit, a product decision that drove a technical choice. That context belongs in the message body, not in a Slack thread that disappears.
Here’s the difference between a message that helps and one that doesn’t:
# Not useful
fix null check
# Useful
Fix nil pointer when user has no billing address
Users with accounts created before 2023-06 may not have a billing
address record. The checkout flow assumed this always existed.
Added a guard and a fallback to the default address.
Fixes #4821.
The second version is not longer for the sake of it. Every sentence is load-bearing.
The format
There’s a convention that works well and that most tools understand:
Short subject line (50 chars or less)
Longer body if needed. Explain what changed and why.
Wrap lines at 72 characters.
Reference issues or tickets at the end.
The subject line is mandatory. The body is optional - use it when the why needs more than one sentence.
The 50-character limit for the subject isn’t arbitrary. git log --oneline, GitHub’s commit list, and most CI tools truncate around there. If your subject is 90 characters, it gets cut off in every place that matters.
The body is separated from the subject by a blank line. This is not a style preference - many tools (including git log --format, GitHub, and GitLab) rely on this to tell the two apart.
Subject line: imperative mood
Write the subject line as a command: Add, Fix, Remove, Refactor, Update - not Added, Fixes, Removed.
The reason is grammatical consistency with how Git itself phrases messages. When you run git revert, Git writes: “Revert ‘Add user authentication’”. When you run git merge, it writes: “Merge branch ‘feature/x’”. Your messages fit neatly into that pattern when they follow the same convention.
A useful test: the subject line should complete the sentence “If applied, this commit will ___.” Add rate limiting to the upload endpoint - yes. Added rate limiting - no.
What to put in the body
Not every commit needs a body. A straightforward bug fix with a clear subject line is complete. Add a body when:
- The change is a workaround for something external (a bug in a library, a platform limitation)
- You considered alternatives and chose this one for a reason
- There are non-obvious consequences or constraints
- The ticket system won’t capture the technical reasoning
When in doubt, write the body. The cost of an unnecessary paragraph is negligible. The cost of missing context at 11pm is not.
One thing that does not belong in the body: a description of what the code does. If you catch yourself writing “this function now checks if the user is active before calling the payment service,” stop and ask whether that should be a comment in the code instead.
Atomic commits
A commit message becomes much easier to write when the commit itself is coherent. If your commit contains a bug fix, a refactor, and the start of a new feature, you can’t write a single subject line that honestly describes it.
An atomic commit does one thing. That doesn’t mean one file or one line - it means one logical change. A refactor that touches twenty files is still one commit. A bug fix and an unrelated style cleanup are two commits.
Atomic commits also make history more useful. git bisect becomes viable. Reverting a specific change without rolling back unrelated work becomes possible. The log reads like a sequence of decisions, not a stream of saves.
A note on tooling
Conventional Commits is a popular convention that adds a type prefix to the subject: feat:, fix:, chore:, docs:, refactor:. It exists primarily to enable automated changelog generation and semantic versioning.
If your team uses it and has tooling around it, follow it. If you don’t, the type prefix adds structure without necessarily adding clarity - “fix: fix nil pointer” is not better than “Fix nil pointer when user has no billing address.” The format is a means to an end, not the end itself.
The actual test
When you’ve written a commit message, ask: if someone runs git log on this repo in a year, does this message give them anything useful?
Not everything will. Dependency bumps, typo fixes, minor style changes - sometimes the subject line is enough and the why is obvious. That’s fine.
What’s not fine is making a deliberate, non-trivial change to a codebase and leaving behind only the word “fix.” Someone will need to understand that change eventually. The two minutes you spend writing a real message will save someone an hour - and that someone is very often you.