All posts
gitbest practicesdeveloper tools

What Makes a Good Git Commit Message?

A practical guide to writing commit messages that are clear, specific, and useful — for your teammates, your future self, and your performance review.

21 March 20266 min read

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 loader

Good 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 feature
  • fix — a bug fix
  • perf — a performance improvement
  • refactor — code change that neither fixes a bug nor adds a feature
  • docs — documentation changes
  • test — adding or updating tests
  • chore — 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 commitlint can 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 .gitmessage template 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.

Stop writing your review from memory.

Gitsprout connects to your GitHub, GitLab, or Azure DevOps and turns your commit history into structured performance evidence in seconds.

Generate your review