🛠️DevKitPack
Back to blog
Dev Tools5 min read

Git Hygiene: Crafting a Clean, Reviewable History

Git history is documentation that writes itself — if you let it. A few habits around atomic commits, clear messages, and tidying before you push turn a messy log into a story your future team can actually read.

Most developers treat git history as a place to dump work, not as something anyone will read. The result is a log full of "wip," "fix," and "more changes," interspersed with commits that bundle three unrelated ideas together. It works, until the day someone — often you, six months later — needs to understand why a line changed, and the history offers no help at all. A clean history is documentation that writes itself, and it costs far less than people assume.

History is for humans

The point of commit history is not to record that work happened; the filesystem already knows that. The point is to explain, to a future reader, what changed and why. When you tools like git blame or git log to investigate a bug, you are not asking "what does this code do" — the code answers that — you are asking "why is it like this," and only the history can tell you.

Once you accept that history is written for a reader, the habits follow naturally. A reader needs commits that each express one coherent idea, messages that explain intent rather than mechanics, and a log that reads like a sequence of decisions rather than a pile of saves. Everything else is detail.

The atomic commit

An atomic commit is one self-contained change: one idea, complete, that could be reverted on its own without taking unrelated work with it. A commit that renames a variable, fixes a bug, and adds a feature all at once is impossible to review cleanly and impossible to revert surgically. Split it, and each piece becomes legible.

  • Each commit should do one thing and leave the codebase in a working state.
  • If a commit message needs the word "and," it is probably two commits.
  • Refactors and behavior changes belong in separate commits — never hide a rename inside a bug fix.
  • Stage selectively (git add -p) so you commit exactly one idea, not everything you happened to touch.

Atomic commits make review dramatically easier, because a reviewer can reason about one change at a time. They also make the history a safety tool: when something breaks, you can bisect to the exact commit and revert just that, instead of unpicking a tangle of mixed concerns.

Messages that help

A commit message has two jobs: a short summary line that says what the commit does, and an optional body that says why. The summary should be concise and written in the imperative — "add retry to the upload client," as if completing the sentence "this commit will…" The body is where the real value lives: the reasoning, the constraint, the thing that was not obvious.

Add retry with backoff to the upload client

Uploads were failing intermittently on flaky mobile connections
because a single network blip aborted the whole request. Retry up
to three times with exponential backoff so a transient failure no
longer surfaces as a lost upload.

Does not change behavior on stable connections.

Notice the body says nothing about which functions changed — the diff shows that. It explains the why the diff cannot: the symptom, the cause, the decision, and a note about scope. That is the message that saves someone an hour of confused investigation a year from now.

The diff tells you what changed. Only the commit message can tell you why — so write the why down.

Tidy before you share

Local history is a workspace; shared history is a record. While work lives only on your machine, you are free to commit messily, then clean it up before anyone sees it. Interactive rebase lets you squash the "wip" commits together, reorder them into a logical sequence, reword unclear messages, and split commits that grew too large. The branch you eventually push should tell the clean story, not the messy process behind it.

The one firm rule is to rewrite only history you have not yet shared. Rewriting commits that others have already pulled rewrites the ground under their feet and creates painful conflicts. The discipline is simple: rebase and tidy freely while a branch is yours alone, and treat history as immutable the moment it is shared.

Branches and merges

How you integrate branches shapes the readability of the main line. A merge commit preserves the fact that a set of changes came in together as a unit, which is useful for grouping a feature. A rebase or squash produces a linear history that is easier to read top to bottom. Neither is universally right; what matters is that the team agrees on one approach so the main branch has a consistent shape.

Whatever the strategy, aim for a main branch where each entry is a deliberate, reviewable unit of change. A history you can read like a changelog is one of the quiet markers of a healthy codebase, and it is built entirely from small habits applied consistently.

The habit that pays off

None of this requires advanced git wizardry — just the intention to write history for the next reader. Commit one idea at a time, explain the why in the message, and tidy your branch before you push it. The investment is small and the payoff is delayed but real: the day you are debugging a strange failure and the history hands you the exact reasoning behind the change, you will be glad someone — possibly you — took the few extra minutes to write it down.

Related posts