How to Write Good Commit Messages

A commit message is documentation that ships with your code forever. Months from now, when you run git log or git blame to understand why a line exists, the message is often the only context you get. This guide covers the conventions that make commit history readable, searchable, and genuinely useful to your future self and your teammates.

Why commit messages matter

Code tells you what changed; a good commit message tells you why. The diff is already in the repository, so re-describing it adds nothing. The valuable information is the reasoning: the bug being fixed, the constraint that forced an unusual approach, the decision that was made and rejected alternatives.

Clear history pays off in concrete ways. It speeds up code review because reviewers understand intent before reading the diff. It makes git bisect and git revert safe, because each commit is a self-contained unit. And it lets you generate release notes and changelogs directly from the log instead of reconstructing them by hand.

The structure of a commit message

A commit message has three parts, separated by blank lines: a short subject line, an optional body, and an optional footer. Git treats the first line specially, so the structure is not arbitrary.

Fix race condition in session cache eviction

The eviction timer and the request handler both mutated the
cache map without a lock, so a request could read a half-evicted
entry and return stale auth data. Wrap both paths in the existing
cacheMutex.

Fixes #1423

The blank line between subject and body is mandatory. Tools like git log --oneline, GitHub, and most editors show only the subject, and they rely on that blank line to know where the subject ends.

Write a clear subject line

The subject line is the part everyone reads, so it carries the most weight. Follow these rules:

  1. Keep it to about 50 characters. This is a soft target, not a hard limit, but it forces you to summarize tightly. Git and GitHub truncate long subjects in many views, and 50 characters keeps the whole line visible.
  2. Use the imperative mood. Write Add retry logic, not Added retry logic or Adds retry logic. The convention reads as a command: this commit will "Add retry logic" when applied. It also matches the messages Git generates itself, such as "Merge branch" and "Revert".
  3. Capitalize the first word and omit the trailing period. A subject is a title, not a sentence.
  4. Be specific. "Fix bug" or "Update code" tells a future reader nothing. Name the component and the behavior: "Fix off-by-one in pagination offset".

A useful test: your subject should complete the sentence "If applied, this commit will ___." If "Update files" sounds wrong there, it is a poor subject.

Use the body to explain why

Not every commit needs a body. A one-line typo fix is fine on its own. But when a change involves a non-obvious decision, the body is where you earn your keep.

Wrap body text at around 72 characters per line. Git does not wrap text automatically, so without manual wrapping your message becomes one long line that displays awkwardly in the terminal. The 50/72 convention — 50 for the subject, 72 for the body — is the most widely followed standard in the Git community.

Focus the body on context the diff cannot show:

  • The problem this commit solves, and how it manifested.
  • Why you chose this approach over an obvious alternative.
  • Side effects, migration steps, or follow-up work that is now required.
  • Links to issues, tickets, or discussions. Use a footer line such as Fixes #1423 or Refs: JIRA-88, which many trackers auto-link.

Conventional Commits: a machine-readable format

Conventional Commits is a popular specification that adds a structured prefix to the subject line so that tooling can parse it. The format is type(scope): description, for example:

feat(auth): add password reset endpoint
fix(api): handle empty request body
docs: clarify rate-limit headers
refactor(db): extract query builder

The common types are feat (a new feature), fix (a bug fix), docs, refactor, test, chore, build, and perf. A ! after the type or scope, or a BREAKING CHANGE: footer, signals an incompatible change.

The payoff is automation. Because feat, fix, and breaking changes map directly onto semantic versioning rules, tools can pick the next version number and assemble a changelog straight from your history. When you need to confirm how two versions relate, a SemVer comparator makes the precedence rules explicit. Conventional Commits is optional, but if your team wants automated releases, adopt it consistently — partial adoption defeats the parsing.

Make atomic, reviewable commits

A good message starts with a good commit. Each commit should represent one logical change. Mixing a bug fix, a refactor, and a formatting pass into a single commit makes the message impossible to write honestly and makes the change hard to revert.

Before you commit, review exactly what you are about to record. Run git diff --staged, or paste your changes into a diff viewer to confirm the staged set is coherent. Use git add -p to stage selected hunks when a working tree has accumulated unrelated edits. Keeping commits small and focused is the single biggest lever for a clean history — see our Git workflow best practices for more on structuring branches and commits.

Common mistakes to avoid

  • Restating the diff. "Change line 42" or "Edit config" duplicates what Git already shows. Explain the reason instead.
  • Vague catch-alls. "Fixes", "WIP", "stuff", and "misc updates" make history unsearchable. If you must commit work in progress, squash it before merging.
  • Bundling unrelated changes. One commit, one concern. This keeps git revert and git bisect useful.
  • Past or present tense. Stick to the imperative ("Add", not "Added"/"Adds") so the whole log reads consistently.
  • No body when one is needed. If a reviewer would reasonably ask "why?", answer it in the commit, not in a chat thread that disappears.
  • Putting everything in the subject. A 200-character subject defeats the format. Summarize in 50, elaborate in the body.

If you forget a detail, you can amend the most recent commit with git commit --amend before you push. For commits that summarize many others, keep a clean reference of common operations handy with a Git cheat sheet. The habit is simple: write each message as if the person reading it has no memory of today — because in six months, that person is you.

Frequently Asked Questions

It is a widely followed convention: keep the subject line to about 50 characters and wrap body lines at about 72 characters. The 50-character target keeps subjects fully visible in tools like git log and GitHub, while 72-character wrapping ensures the body displays cleanly in a terminal, since Git does not wrap text automatically.

Use the imperative mood, which reads like a command: "Add retry logic" rather than "Added" or "Adds". This matches the messages Git generates itself (such as "Merge" and "Revert") and completes the sentence "If applied, this commit will...". Keeping every message in the imperative makes the log consistent and easy to scan.

No, it is optional. Conventional Commits adds a structured type(scope): description prefix so tools can parse your history to pick version numbers and generate changelogs automatically. Adopt it if you want automated releases tied to semantic versioning, but if you do, apply it consistently - partial use breaks the parsing that makes it valuable.

The subject is a short summary of what changed (about 50 characters, imperative mood). The body explains why: the problem being solved, why you chose this approach, side effects, and links to issues. A one-line fix may not need a body at all, but any non-obvious decision belongs there so future readers do not have to guess.

If you have not pushed yet, run git commit --amend to edit the most recent message, or git rebase -i to edit older ones. Once a commit is pushed and shared, avoid rewriting it, since that changes history others may have pulled. The safer habit is to review your staged diff and write the message carefully before committing.