Making a Commit Worth Reading
git commit -am is fast and it is why so many histories are useless six months later. Stage deliberately, split one messy file into two honest commits, and write the message the way the people reading git log need it.
Worth reading first: The Working Tree, the Index, and the Repository
git commit -am is fast and it is why so many histories are useless six months later. Stage deliberately, split one messy file into two honest commits, and write the message the way the people reading git log need it.
git add is a choice, not a formality
There are four ways to stage, and they are not interchangeable.
$ git add src/auth/redirect.ts # one file$ git add src/auth/ # a directory and everything under it$ git add . # everything under the current directory$ git add -A # everything in the repository, wherever you are standinggit add . is the one everybody types and the one worth being slightly suspicious of. It stages every changed and every new file below you, including the ones you forgot about: the scratch file, the screenshot, the .env you created while debugging.
Staging can also be undone freely. Nothing about git add is committal — it is a draft, and changing your mind costs nothing.
$ git restore --staged notes.md # unstage one file, keeping the edit$ git restore --staged . # unstage everything, keeping every editadd --patch stages part of a file
This is the command that turns staging from a formality into a tool, and most people go years without meeting it.
$ git add -pGit walks you through your changes one hunk at a time — a hunk being a contiguous run of changed lines — and asks what to do with each. You answer with a single key.
@@ -12,6 +12,9 @@ export async function loadUser(id) { const res = await fetch(`/api/users/${id}`);+ if (!res.ok) throw new Error("loadUser failed"); return res.json(); }++console.log("DEBUG: got here"); (1/1) Stage this hunk [y,n,q,a,d,s,e,?]?y / n
Stage this hunk, or do not. The two you will use ninety per cent of the time.
s
Split this hunk into smaller ones. This is how you separate the fix from the debug line above.
e
Edit the hunk by hand, line by line, when even a split will not separate them.
Press s on that hunk and Git offers the error check and the console.log separately. Stage the first, skip the second, and you have committed the fix without the debug statement — while the debug statement is still in your working tree, still useful, still there when you need it.
A message has a subject and, usually, a body
A commit message has a shape, and it is not arbitrary. Git, GitHub, and every tool built on top of them treat the first line specially.
Fix the redirect when the next param is empty<-- blank line, and it is load-bearing -->Arriving from /pricing set next="" rather than omitting it,so the guard treated it as a valid destination and senteveryone to the dashboard instead of back to the page theycame from. Checks for a non-empty string rather than a defined one. Fixes #482The first line is the subject. It shows up in git log --oneline, in the GitHub commits list, in blame annotations, in bisect output, and in the sidebar of every code review tool ever made. Keep it under about fifty characters so it is not truncated.
The blank line is not decoration. Git uses it to tell subject from body; without it the entire message is treated as one long subject and every tool that shows a summary shows the whole paragraph.
The body explains why. The diff already shows what changed — nobody needs a prose translation of it. What the diff cannot show is the reasoning, the alternative you rejected, and the thing that will look wrong to whoever reads it next.
Cache the user lookup for 60 seconds. The dashboard was calling loadUser four times per render because three separate components each needed the display name. Caching in the service was chosen over lifting state so the components stay independent.
Same commit, same diff. One of them answers the question somebody will ask in March.
The imperative mood is a convention with a reason
Write "Fix the redirect", not "Fixed the redirect" or "Fixes the redirect" or "Fixing the redirect".
This looks like arbitrary pedantry and it is not. Git itself writes messages in the imperative when it generates them — "Merge branch feature into main", "Revert Add the caching layer". Your messages sit in the same list, and matching the mood makes the log read as one document rather than a pile of styles.
The test that makes it stick: a commit message completes the sentence "If applied, this commit will…". If applied, this commit will fix the redirect. If applied, this commit will fixed the redirect — which is not a sentence.
Reads well in a log
Add rate limiting to the search endpoint
Remove the deprecated export helper
Fix off-by-one in the pagination footer
Costs the reader something
updates
fixed bug
asdf
final commit (please work)
One commit should do exactly one thing
Everything above is in service of this. A commit that does one thing can be described in one line, reviewed on its own, reverted without collateral damage, and found by bisect. A commit that does four things can do none of those.
The test for whether a commit is one thing
- →Can you describe it in one line without using the word "and"? If not, it is at least two commits.
- →Would reverting it undo exactly one decision? If reverting the rename also reverts a bug fix, they should not have been together.
- →Does the project still build and pass its tests at this commit? Bisect depends on this being true of every commit, not just the last one.
- →Would a reviewer looking at only this diff have everything they need to judge it?
The corresponding failure is the opposite extreme — twenty commits called "wip", "wip 2", "fix lint". That is a normal way to work and a bad thing to merge, and there are two good answers to it. Tidy them up before opening the pull request with an interactive rebase, which is a later chapter. Or let GitHub squash them on merge, which is also a later chapter. Both are fine; committing rarely while you work is not the fix.
Key takeaways
- git add . stages everything below you, including files you forgot about. Run git status first.
- Staging is reversible with git restore --staged, so nothing about git add is a commitment.
- git add -p stages one hunk at a time, and s splits a hunk further — this is how the fix gets committed without the debug line.
- A message is a subject line under fifty characters, a blank line, and a body explaining why.
- The blank line is load-bearing: without it every tool shows your whole paragraph as the summary.
- The diff shows what changed. The body should carry the reasoning the diff cannot.
- Use the imperative: "If applied, this commit will fix the redirect." It matches the messages Git writes itself.
- One commit, one thing — testable by whether you can describe it without the word "and".
- Commit often while working and tidy the history afterwards. The reflog can only save what was committed.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.