Git Commit Best Practices: Write History, Not Just Code
Your Git history is the only documentation that actually updates itself as you work. The catch is it only does that if you write it right.
When something breaks three months from now, you’ll run git log. If what
you find is “fix stuff”, “updates”, “wip”, and “misc” - you’re going to spend
the next hour reading code you have no memory of writing. Good commits answer
three questions before you even ask them: what changed, why it changed, and
who made the call.
1. Write a Clear Subject Line
The subject line is all most people will ever read. Make it useful.
Rules:
- Under 50 characters
- Imperative mood: “Add”, not “Added” or “Adding”
- Describe the result, not the activity
One quick test: does it finish the sentence “If applied, this commit will…”?
Not this:
fixed bug
changes
updated stuff
This:
Fix login crash on mobile Safari
Improve navbar contrast for WCAG AA
Add rate limiting to the auth endpoint
The bad examples aren’t stylistically wrong. They’re information-wrong. Someone reading the log shouldn’t need to open the diff to understand what happened.
2. Use the Body to Explain Why
The diff already shows what changed. The body is for everything the diff can’t tell you.
Most developers skip this. It’s also the part that saves you when something breaks eight months later and you have no memory of touching it.
Write down:
- What problem triggered this
- Any constraints you were working around
- What you considered but didn’t do
Example:
Users were getting logged out mid-session. The token expiry on the
frontend was calculated from the client clock, which was drifting
under load. Switched to server-side timestamps. Removed the
client-side calculation entirely - didn't want two sources of truth.
Forty words. Potentially hours saved.
3. Keep Commits Atomic
One commit, one thing.
If you fixed a bug, refactored a module, and added a feature in the same session, those need to be three commits. Not one “misc cleanup” blob you push before heading out.
Atomic commits let you revert one change without touching everything else.
They make git bisect useful when you’re hunting a regression. They make
code review faster because the diff tells a clear story.
Practical test before committing: if you had to undo only this change tomorrow, could you?
4. Use Conventional Commits
Structured prefixes make history parseable - by tools and by people skimming a log.
| Prefix | Use case |
|---|---|
feat: | New feature |
fix: | Bug fix |
docs: | Documentation only |
style: | Formatting, no logic changes |
refactor: | Internal restructure, no behavior change |
test: | Tests added or updated |
chore: | Maintenance, dependency bumps |
Tools like semantic-release and standard-version can generate changelogs and version bumps automatically from this format. Even without those tools, the prefixes make a long log much easier to scan.
Example:
feat(auth): add Google OAuth login
Integrates Google provider via NextAuth.js. Users can now sign in
without creating a password. Existing email/password accounts
are unaffected.
Closes #123
5. Reference Issues
When a commit connects to a tracked issue, link it.
Closes #45
Fixes the API timeout in #78
This connects code to the conversation that produced it. When someone asks why something was built a certain way, they can follow the thread instead of digging through Slack or pinging you directly.
6. Flag Breaking Changes
If you’re removing a field, changing a function signature, or altering a config format - make it impossible to miss.
feat(api)!: redesign user response payload
BREAKING CHANGE:
Removed "username". Clients must update to "displayName".
No migration path.
The ! and BREAKING CHANGE: label are part of the Conventional Commits
spec. Tools that parse the format will catch it. More importantly, whoever
merges your PR will catch it before it hits production.
7. Write for the Person Debugging at 2 AM
Commit messages aren’t for the machine. The machine doesn’t read them. They’re for whoever has to understand a decision made under time pressure, with context that no longer exists.
Sometimes that’s a teammate. Sometimes it’s someone who joined after you left. Honestly, most of the time it’s just you, several months later, completely stumped by your own code.
The commits that help most aren’t the polished ones. They’re the honest ones - the ones that say “we went with this because X, even though Y was tempting, and here’s the tradeoff we accepted.”
Quick Checklist
Before committing:
- Does the subject line say what actually happened?
- Does the body explain why?
- Is this one logical change, not three bundled together?
- Could someone read this with zero other context and understand it?
If all four are yes, ship it.
Good commits document intent, not just change