open atlas
↑ Back to track
Git, from zero to senior GIT · 02 · 04

What makes a good commit

A good commit is atomic — one logical change that stands on its own — with a clear message: an imperative-mood subject under ~50 chars, a blank line, then a body explaining why. Learn Conventional Commits (type(scope): subject) and how to split noisy work into clean commits.

GIT Junior ◷ 17 min
Level
FoundationsJuniorMiddleSenior

Two developers ship the same feature. One leaves a single commit: git commit -m "stuff" bundling a bug fix, a rename, and a new endpoint. The other leaves three commits, each one thing, each with a sentence saying why. Six months later someone hits a regression and runs git bisect or git log. The first history is useless — every commit is a tangled blob. The second reads like a changelog. Same code, wildly different maintainability.

By the end of this lesson you will be able to write atomic commits with messages a future engineer can actually use, and split a messy editing session into clean commits.

Goal

After this lesson you can explain what makes a commit atomic, write a commit message with an imperative-mood subject and a why-focused body, apply the Conventional Commits format, and split one noisy change into several clean commits.

1

A good commit is atomic: it captures exactly one logical change, and the project still builds and passes tests at that commit. Atomic does not mean “small” — it means self-contained. A bug fix is one commit; a variable rename is another; a new feature is a third. The test is: could you revert this single commit cleanly without dragging unrelated work with it? If reverting “fix login” also undoes a rename, the commit was not atomic. Atomic commits make git revert, git bisect, git cherry-pick, and code review all work the way they are designed to.

2

The subject line is a one-line summary in the imperative mood, under about 50 characters. Write it as a command — what applying the commit will do — not as a past-tense report:

Add rate limiting to login endpoint     ← imperative, good
Added rate limiting                      ← past tense, avoid
Fixes the thing where login breaks       ← vague, avoid

The imperative reads naturally with git’s own phrasing: “if applied, this commit will Add rate limiting…”. Keep it under ~50 chars so git log --oneline and GitHub do not truncate it. Capitalise the first word, no trailing period — it is a title, not a sentence.

3

After the subject, leave a blank line, then a body that explains WHY — not what. The diff already shows what changed; the body’s job is the reasoning the diff cannot convey:

Add rate limiting to login endpoint

Brute-force attempts were spiking auth CPU and occasionally
locking out real users. Cap to 5 attempts per IP per minute;
chose a sliding window over a fixed one to avoid burst gaming
at minute boundaries.

Closes #482

Wrap the body at about 72 characters so it reads well in terminals and git log. The blank line between subject and body is mandatory — git and its tools rely on it to separate the title from the description. Months later, the “why” is the only thing that explains a non-obvious line.

4

Conventional Commits give the subject a machine-readable structure: type(scope): subject. A small, fixed vocabulary of types prefixes the message:

feat(auth): add rate limiting to login
fix(api): correct off-by-one in pagination
refactor(db): extract query builder
docs(readme): document env vars
test(cart): cover empty-basket checkout
chore(deps): bump eslint to 9.x

Common types are feat, fix, refactor, docs, test, and chore; the optional (scope) names the affected area. Teams adopt this because it is parseable: tools auto-generate changelogs, and a feat vs fix vs a !/BREAKING CHANGE marker drives semantic-versioning bumps automatically. The format turns commit messages into release infrastructure.

Why this works

Why does atomicity make git bisect magical? Bisect binary-searches your history to find the commit that introduced a bug — it checks out a commit, you mark it good or bad, and it halves the range. This only works if each commit builds and represents one change: a “bad” commit then points at one logical change, not a tangle of five. A single mega-commit that mixes a fix, a rename, and a feature makes bisect land on a blob you still have to untangle by hand — you have thrown away the very precision the tool exists to give you.

Worked example

Splitting a tangled afternoon into clean history.

You spent an afternoon and your working tree now has three unrelated things: a fix to auth.js, a rename of util.jshelpers.js across the codebase, and a new /health endpoint. Committing all at once would produce one un-revertable blob. Instead you stage by purpose:

git add auth.js
git commit -m "fix(auth): reject expired session tokens"

git add util.js helpers.js src/   # the rename and its call sites
git commit -m "refactor: rename util to helpers"

git add routes/health.js app.js
git commit -m "feat(api): add /health liveness endpoint"

For the bug fix you add a body explaining the security reasoning (git commit with no -m opens your editor). Now git log --oneline reads as three intentional steps. If the /health endpoint later causes trouble, git revert removes just that commit — the fix and the rename are untouched. The afternoon’s mess became history a teammate can read and bisect.

Common mistake

A common anti-pattern is the “WIP dump”: coding all day, then git add . && git commit -m "wip" at the end. It is fast, but it destroys every benefit of history — you cannot revert one part, bisect lands on a wall of unrelated changes, and review is hopeless. If you genuinely committed messy work-in-progress locally, clean it up before sharing it (with interactive rebase, covered later) so the published history is atomic. The rule of thumb: commit messages are written for the person debugging your code at 2 a.m. six months from now — often you.

Check yourself
Quiz

Which commit best follows the conventions for an atomic commit with a good message?

Recap

A good commit is atomic — one logical change that builds and passes on its own, so git revert, git bisect, and review work cleanly. Its message has an imperative-mood subject under ~50 chars (“Add”, not “Added”), a mandatory blank line, then a body explaining why wrapped at ~72 chars. Conventional Commits structure the subject as type(scope): subject using feat, fix, refactor, docs, test, chore, which lets tooling auto-generate changelogs and drive semantic-version bumps. Split noisy work into single-purpose commits by staging subsets — avoid the “wip” dump that throws away every benefit of history. You have now completed the recording-changes loop: stage precisely, read what you changed, ignore what does not belong, and record it as clean, atomic history.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.