TechEarl

Git Commit Message Best Practices

A good Git commit message uses a short imperative subject under 50 characters, a blank line, then a wrapped body that explains why. Here is the whole convention.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
Git commit message best practices: the 50/72 rule, imperative mood, and Conventional Commits explained for beginners

A good Git commit message has a short subject line in the imperative mood, kept under about 50 characters, then a blank line, then an optional body wrapped at around 72 characters that explains why the change was made. That is the whole convention, and it is the single cheapest habit you can pick up that makes your history readable to other people (and to future you).

If you want a one-line version to copy now:

bash
git commit -m "Fix login redirect after session timeout"

Short, imperative, capitalized, no trailing period. The rest of this page explains where that rule comes from, when to add a body, what the imperative mood actually means, and the Conventional Commits format you will see on a lot of team projects.

Why commit messages matter at all

When you work alone on a weekend project, you can get away with git commit -m "stuff" and nobody suffers but you. The moment another person reads your history, the message becomes the only explanation of why a line changed. The diff already tells anyone what changed; the message has to carry the reasoning the diff cannot. If you are just starting to track a project, the habit begins the moment you make your first commit, whether you are following how to add Git to an existing project or starting fresh.

The places a message gets read are exactly the places you are under pressure:

  • Running git log to find when a bug was introduced. Once you know how to read history with git log, good messages are what make that history worth reading.
  • Running git blame on a confusing line and landing on the commit that wrote it.
  • Reviewing a pull request where the commit messages are the table of contents.
  • Six months later, trying to remember why you deleted code that now looks important.

A message like fixed it helps in none of those. A message like Fix race condition in cache eviction under high write load answers the question before you even open the diff. This is the whole game with Git as a beginner: the tooling is there to let other people understand your changes without asking you, and the commit message is where that understanding lives.

The 50/72 rule

The convention almost everyone follows comes straight out of how Git itself displays commits. The format is:

text
Summarize the change in 50 characters or less

Add a more detailed explanation here if it is needed. Wrap the
body at about 72 characters. Explain what and why, not how (the
diff already shows how). Leave a blank line between the subject
and the body, because tools depend on it.

- Bullet points are fine in the body
- Use a hyphen or asterisk, with a blank line between paragraphs

Two numbers, two reasons:

Subject under ~50 characters. This is a soft target, not a hard error. Git does not reject a longer subject, but git log --oneline, GitHub's commit list, and most UIs truncate around 50 to 72 characters with an ellipsis. If your subject runs past that, the important words get cut off in exactly the views people scan fastest. Fifty characters forces you to name the change, not narrate it.

Body wrapped at ~72 characters. Git does not wrap text for you. When you run a plain git log, it indents the body by four spaces, so a body written as one long line either runs off the screen or wraps raggedly. Wrapping at 72 leaves room for that indent inside an 80-column terminal and keeps the body readable everywhere.

The blank line between subject and body is the one part that is not cosmetic. Git treats the first line as the subject and everything after the blank line as the body. Tools like git shortlog, git log --format=%s, and the GitHub UI all rely on it. Skip the blank line and your whole message becomes one mushed-together subject.

For anything but a trivial change, do not use -m. Run a bare git commit, which opens your editor, so you can write a real subject and body without fighting the shell:

bash
git commit

If your editor is not set, point Git at the one you want:

bash
git config --global core.editor "nano"

Use the imperative mood

This is the rule that feels strange at first and then becomes automatic. Write the subject as a command, as if you are telling the codebase what to do:

text
Add retry logic to the webhook sender
Remove the deprecated v1 auth endpoint
Fix off-by-one error in pagination

Not the past tense, not a description of what you did:

text
Added retry logic to the webhook sender
Removes the deprecated v1 auth endpoint
Fixing off-by-one error in pagination

The reason is not style police. Git's own generated messages are imperative: a merge says Merge branch 'feature', a revert says Revert "...". The clean mental test comes from the Git project's own guideline: a properly formed subject should complete the sentence "If applied, this commit will ___". "If applied, this commit will Add retry logic" reads correctly. "If applied, this commit will Added retry logic" does not. Matching that mood keeps your hand-written commits consistent with the ones Git writes for you.

A few more subject conventions that ride along with it:

  • Capitalize the first word.
  • Do not end the subject with a period. It is a title, not a sentence, and the period just wastes one of your ~50 characters.
  • Do not start with a vague verb like "Update" or "Change" with no object. "Update files" tells the reader nothing; "Update API base URL for the staging environment" tells them everything.

Good and bad messages, side by side

Bad messageWhy it failsBetter version
fixNo subject, no context, no idea what was fixedFix crash when uploading a zero-byte file
stuffSays nothingAdd rate limiting to the public search endpoint
Fixed the bug.Past tense, trailing period, "the bug" is unknowable laterFix double-charge on retried checkout requests
WIPFine on a private branch, never in shared historySquash WIP commits before merging (see below)
Update user.jsNames the file (the diff already does) not the changeValidate email format before saving the user
asdkjfhA keyboard mash from committing in a hurryRevert accidental commit of debug logging

The pattern in every "better" version is the same: name the behavior that changed, in the imperative, specifically enough that someone reading git log a year from now does not have to open the diff to understand the gist.

Conventional Commits

On a lot of team projects you will see commit subjects that look like this:

text
feat: add password reset flow
fix: prevent duplicate orders on network retry
docs: clarify the rate-limit headers in the API guide
refactor: extract the pricing calculator into its own module
chore: bump the linter to the latest minor

That is the Conventional Commits format. It is the 50/72 imperative convention with one extra rule bolted on: the subject starts with a type, an optional scope in parentheses, a colon, and then the imperative description. The structure is:

text
type(optional scope): description

[optional body]

[optional footer]

The common types are:

TypeMeaning
featA new feature
fixA bug fix
docsDocumentation only
styleFormatting, whitespace, no code-behavior change
refactorCode change that neither fixes a bug nor adds a feature
testAdding or fixing tests
choreBuild, tooling, dependencies, housekeeping

A scope narrows it down, for example fix(auth): handle expired refresh tokens. A breaking change is flagged either with a ! after the type, like feat!: drop support for the legacy v1 config format, or with a BREAKING CHANGE: line in the footer.

The reason teams adopt it is not tidiness for its own sake: the type prefix is machine-readable. Tools can read the log and bump the version automatically (a fix: is a patch, a feat: is a minor, a breaking change is a major), generate a changelog grouped by type, and trigger CI behavior off the type. If your project runs that kind of release automation, often through a GitHub Actions pipeline, the convention is what feeds it.

You do not have to use Conventional Commits on a personal project. But if a repo already uses it, match it, because the tooling on the other end is parsing your subjects and will silently do the wrong thing if you do not.

Fixing messy messages before they ship

Beginners often worry that a bad message is permanent. It is not, as long as you have not shared the commit yet. While work sits on your local branch, you can rewrite it freely:

  • The very last commit's message is one command away with git commit --amend. See how to undo or rewrite the last commit for the full walk-through, including amending the message without changing the files.
  • A messy string of WIP commits can be collapsed into one clean commit with a clear message using interactive rebase to squash before you open a pull request.

The one boundary to respect is the same one that governs all history rewriting: rewrite freely while the commits are private, but once you have pushed and shared them, treat the history as fixed. For an already-pushed commit you would not amend the message in place; you would correct it with a new commit, which is the reset versus revert distinction. If you need a refresher on why, the trade-offs are spelled out in merge versus rebase. The practical upside is that you can commit sloppily while you work (commit early, commit often) and clean the messages up in one pass right before the work goes out for review.

A workflow that makes good messages easy

The convention is simpler to keep than to read about. In practice:

  1. Stage only the changes that belong in one logical commit, not the whole working tree. Keeping commits focused is what makes a one-line subject possible in the first place; this is the real payoff of understanding the staging area. If you are starting from scratch, set up Git for a new project first so you have a clean repo to commit into.
  2. Run a bare git commit (no -m) for anything non-trivial so your editor opens.
  3. Write an imperative subject under ~50 characters, then a blank line, then a body that explains why if the change is not self-explanatory.
  4. Before sharing, amend or squash to clean up anything sloppy.

Small, focused commits with honest messages turn your history into documentation you get for free. On a team workflow, that history is what code review, debugging, and onboarding all run on.

Sources

Authoritative references this article was fact-checked against.

Tagsgit commit message best practicesGitVersion Controlconventional commitscommit messageimperative mood

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

.dockerignore Best Practices

The .dockerignore file controls what COPY . . actually ships into your image. Without it, your image gets node_modules, .git, .env, and every other thing in the project. With it, builds are faster, images smaller, and secrets stay out.

Kinsta for WordPress Agencies: Honest Review

Kinsta is the premium managed WordPress host most agencies eventually consider. The honest take: where the value justifies the price, where it does not, the agency partner program math, and the alternatives at each tier.