A git commit message is a note to the future. It might be read by a colleague debugging a production issue at 2am, by you trying to remember why you made a change six months ago, or by an automated tool trying to generate a changelog.
Most commit messages fail all three audiences. Here's what separates good ones from bad ones — and how to write them consistently.
Why commit messages matter more than you think
Bad commit messages have real costs:
- Debugging takes longer. When you're bisecting a regression, "fix stuff" tells you nothing. "fix: clamp negative values in discount calculation" tells you exactly where to look.
- Code reviews suffer. Reviewers read the commit message before the diff. A clear message primes them to understand your intent; a vague one makes them work harder.
- Your career takes a hit. At performance review time, your commit history is one of the best records of your work. Vague commit messages make it harder to reconstruct what you actually built and why it mattered.
The anatomy of a great commit message
A well-formed commit message has two parts: a subject line and an optional body. Most commits only need the subject line.
The subject line
The subject line is what appears in git log --oneline, GitHub's commit list, and pull request timelines. It should:
- Be 72 characters or fewer
- Use the imperative mood ("add", "fix", "remove" — not "added", "fixing", "removed")
- Not end with a period
- Complete the sentence: "If applied, this commit will [your subject line]"
The body (optional)
Use a body when the why isn't obvious from the subject alone. Separate it from the subject with a blank line. Explain the motivation for the change, not what the diff shows — the diff shows the what.
Good subject:
perf: replace sequential DB calls with batch query in dashboard loaderGood body: "Dashboard was timing out for accounts with 500+ records. Profiling showed the loader was making one DB call per row. Batch query brings this down to a single round trip."
Conventional commits: a standard worth adopting
Conventional Commits is a widely-adopted specification that adds structure to the subject line:
<type>(<optional scope>): <description>The most common types:
feat— a new featurefix— a bug fixperf— a performance improvementrefactor— code change that neither fixes a bug nor adds a featuredocs— documentation changestest— adding or updating testschore— maintenance tasks (dependency updates, config changes)ci— CI/CD pipeline changes
Beyond readability, conventional commits enable tooling: automated changelogs, semantic versioning, and commit-based release notes all rely on this format.
Real examples: before and after
The difference between a weak and a strong commit message is almost always specificity. Here are common patterns and how to fix them:
"fix bug" → be specific about what broke and where
❌
fix bug✅
fix: prevent divide-by-zero in invoice total when quantity is 0
"wip" → never commit WIP to main
❌
wip✅
feat(auth): scaffold Google OAuth2 flow (token exchange pending)
"update stuff" → name the thing you updated
❌
update stuff✅
chore: bump Next.js to 15.2 and update app router config
"changes" → describe the change, not the act of changing
❌
changes✅
refactor: extract payment validation into reusable hook
The scope field: when to use it
The optional scope in feat(scope): description is useful in larger codebases where changes are clearly bounded to a module, service, or domain. Good scopes are short and consistent across the team:
feat(auth): ...fix(checkout): ...perf(api): ...
Don't use scope if your codebase is small or if the scope would always be the same — it adds noise without signal.
Breaking changes
If your commit introduces a breaking change, signal it with a ! after the type, or include a BREAKING CHANGE: footer in the body:
feat(api)!: remove deprecated /v1/users endpoint
BREAKING CHANGE: /v1/users has been removed. Migrate to /v2/users.Common mistakes to avoid
- Committing too much at once. If your message needs "and" in it, consider splitting into two commits. "fix login redirect and update user avatar upload" is two things.
- Referencing ticket numbers only. "JIRA-1234" means nothing without the JIRA context. Include a human-readable description alongside any ticket reference.
- Describing the diff instead of the intent. "Remove console.log statements" describes what you did. "chore: clean up debug logs before release" describes why.
- Inconsistent tense. Pick imperative mood and stick to it across the team. Linters like
commitlintcan enforce this automatically.
Tools that help
- Gitsprout Commit Grader — paste any commit message and get an instant quality score, specific issues, and a rewritten version. Free, no sign-up.
- commitlint — enforces conventional commit format in CI, so bad messages never reach main.
- Conventional Commits VSCode extension — adds a commit message builder to your editor.
- git commit template — set a
.gitmessagetemplate in your repo to prompt for type, scope, and description every time.
Why this pays off at performance review time
There's a less obvious benefit to writing good commit messages: they make your performance reviews dramatically easier to write.
When your commits clearly describe what you did and why, your git history becomes a structured record of your contributions. "feat(payments): add idempotency key support to prevent duplicate charges" is already most of an evidence item for your review — it has the domain (payments), the action (added idempotency), and an implicit outcome (preventing duplicate charges).
Engineers who write clear commits spend less time reconstructing their work at review time and write more compelling self-reviews as a result.
Gitsprout takes this further — it analyses your commit history across GitHub, GitLab, or Azure DevOps and generates structured performance review evidence automatically. The better your commit messages, the better the output.

