How do you split a large Git change into a series of commits a maintainer can review?
answer
- the commit is the unit of review
- noise versus meaning in one diff
- every commit should build
- separate renames from behaviour
- each commit one logical, testable step
basics
~20 sMake each commit one self-contained step that builds and passes tests on its own: preparation and refactoring first, behaviour change second, then tests and docs. Order them so a reviewer reading top to bottom sees the change being argued, not assembled.
solid answer
~50 sThe unit of review is the commit, so each one should be a single logical change that compiles and passes its tests in isolation — that keeps `git bisect` meaningful and lets a reviewer approve early commits while arguing about later ones. In practice that means separating mechanical work from semantic work: pure renames, moves and formatting go in their own commits, so the commit that actually changes behaviour has a small readable diff. Order the series as a narrative — add the new helper, switch callers over, delete the old path — and write each message explaining why, not what. Before sending, verify the series rather than just the tip: `git rebase -i --exec 'make test' <base>` runs the build at every commit, and `git log --oneline <base>..HEAD` is a fast sanity check that the story reads well. Rebase onto the current upstream tip last, so it applies cleanly.
code
bash · 10 lines# read the series the way a reviewer will
git log --oneline origin/main..HEAD
git log -p origin/main..HEAD
# prove every commit in the range builds, not just the tip
git rebase -i --exec 'make test' origin/main
# fold a review fix into the commit it belongs to
git commit --fixup 4f2ab19
git rebase -i --autosquash origin/maingo deeper
Remember the two rules you can apply immediately: one logical change per commit, and never mix a large mechanical rename with the real change. Write a summary line that says what the commit does.
Explain why each commit must build and pass tests — bisect, partial approval, clean revert — and describe reshaping a messy branch into a series before sending it rather than committing perfectly the first time.
Show judgment about ordering: preparatory refactor first, behaviour second, small backportable fix ahead of the big cleanup, and folding review feedback into the commit it belongs to instead of appending corrections.
Be ready to set the norm for a project: what series size you expect from contributors, how you keep history bisectable across many contributors, and the tradeoff between demanding clean series and losing occasional contributors to the friction.
## Why the shape of the series matters When you cannot push to a project, your change is judged as text by someone who did not write it. Reviewers read commits, not branches. A single 2,000-line commit forces an all-or-nothing verdict and hides the interesting change inside mechanical noise; the same work as eight focused commits can be reviewed incrementally, partially approved, and — crucially — bisected later when something breaks in production a year on. ## The two invariants Two properties do most of the work. **Each commit is one logical change.** If the message needs the word "and", or a bulleted list of unrelated items, the commit is probably two commits. A useful test: could this commit be reverted on its own without breaking something unrelated? **Each commit is good on its own.** The tree at every commit should build and pass tests. This is what makes `git bisect` usable: bisect assumes intermediate commits are testable, and a series with broken middles turns a bisect run into a guessing game. It also means you never introduce a call to a function that only appears three commits later. ## Separating mechanical from semantic The highest-leverage habit is isolating changes that a reviewer can skim from changes they must think about. A rename touching 60 files, an import reordering, a formatter run, a file move — each belongs in its own commit whose message says exactly that ("rename Foo to Bar, no behaviour change"). Then the commit that alters behaviour is twenty lines and the reviewer can actually concentrate on it. Mixing the two is the most common reason a review stalls: nobody can tell which of the 60 changed files matter. ## Ordering as an argument A good series reads like a proof. Typical shapes: - **Prepare, then change.** Extract a seam or add a parameter with no callers using it; then switch the callers; then remove the old path. - **Test-first.** Add a failing test that demonstrates the bug in one commit, fix it in the next — the reviewer sees the bug is real before seeing the fix. Some projects prefer the fix and its test together so no commit is red; follow the project's convention. - **Fix, then extend.** Land the small correct fix early so it can be backported, and put the larger refactor behind it. Documentation and changelog entries usually ride with the commit they describe rather than being swept into a trailing "docs" commit. ## Messages carry the reasoning Each commit message should make the commit reviewable without the pull request or mail thread. A short imperative summary line, a blank line, then the *why*: what was wrong, what alternatives you rejected, what the reader should watch out for. The diff already says what changed; the message is where the intent lives, and it is the only part that survives into `git log` years later. ## Reshaping what you actually wrote Nobody writes a clean series on the first try — real work is committed messily and reshaped afterwards. Interactive rebase reorders, squashes and edits commits; `git add -p` splits a dirty working tree into staged pieces; `git commit --fixup` plus an autosquash rebase folds review fixes into the commit they belong to instead of appending "address review comments" commits. The important discipline is to do this reshaping *before* you send, and to fold later review fixes into the original commit rather than stacking corrections on top, so the merged history contains only the version that was agreed. ## Verifying the whole series, not the tip It is easy to test only `HEAD`. To check every commit, `git rebase -i --exec 'make test' <base>` replays the range and runs the command after each commit, stopping where it fails. `git log --oneline <base>..HEAD` and `git log -p <base>..HEAD` let you read the series the way a reviewer will. Finally, rebase onto the current upstream tip immediately before sending: a series that does not apply is a series that does not get reviewed. ## How big is too big There is no universal number, but the practical guide is reviewer attention: a commit a reviewer can hold in their head, and a series short enough to review in one sitting. When a change genuinely cannot be made small, split it by *stage* rather than by *file* — a series of ten commits each spanning the same three files but each doing one step is far more reviewable than ten commits each rewriting one file completely.
- Why does it matter that intermediate commits build and pass tests?Because `git bisect` walks intermediate commits to find where a bug appeared. If some of them do not compile, every probe returns an inconclusive result and the search degenerates. It also lets a maintainer take the first few commits of a series while later ones are still under discussion.
- A reviewer asks for a change to your third commit — where does the fix go?Into that commit, not on top. Amend it during an interactive rebase, or commit with `git commit --fixup <sha>` and let an autosquash rebase fold it in. The series that finally lands should read as if you wrote it correctly the first time, without "fix review comment" commits.
- When is a single large commit actually the right call?When the change genuinely has no smaller correct state — a generated file regenerated wholesale, or an atomic interface change where any split leaves the tree broken. Say so in the message, and still split any mechanical churn such as a rename out into its own commit.
saying these in an interview costs you the question
- Says one big commit per feature is cleaner history
- Mixes a rename across 50 files with the behaviour change
- Adds fix review comments commits on top of the series
- Claims only the branch tip needs to build
- Splits by file rather than by logical step