Splitting a large commit into reviewable pieces Jump to heading
A commit that renames an interface, adds a feature, fixes an unrelated bug and reformats two files is not reviewable β not because reviewers lack diligence, but because there is no order in which the diff makes sense. Splitting it into a sequence where each commit does one thing turns an unreadable change into four short ones, and takes about ten minutes. This recipe uses the machinery covered in Interactive Rebase Workflows.
When to use this approach Jump to heading
- A commitβs diff spans concerns a reviewer would want to evaluate separately.
- A refactor and a behaviour change landed together, so neither can be reverted alone.
- You want
git bisectto be able to land on a small commit rather than a large one. - The branch has not been shared yet, or you are the only person with it checked out.
- If the commit is already merged to a shared branch, do not split it β the history is public. Improve the next one instead.
Step 1 β Plan the slices before touching Git Jump to heading
# What is actually in this commit?
git show --stat HEAD
git show HEAD --name-only | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn What changed: nothing β but the directory histogram usually reveals the boundaries. Files under api/ moving together with files under db/migrations/ is two commits, not one.
Order matters. Put the mechanical changes first β formatting, renames, additive schema β so the final commit contains only the substance a reviewer needs to think hard about.
Step 2 β Stop at the commit with an interactive rebase Jump to heading
# Start a rebase covering the commit you want to split
git rebase -i HEAD~3
# In the editor, change 'pick' to 'edit' on the target commit:
# edit 9f8e7d6 feat: settlement export
# pick a1b2c3d docs: update readme # Git stops with the commit applied. Undo it, keeping the changes unstaged.
git reset HEAD^
git status --short # every file from the commit, now unstaged What changed: the commitβs content is back in the working tree and nothing is staged, so you can rebuild it in pieces. Nothing has been lost β git rebase --abort returns you to where you started at any point.
SAFETY WARNING β
git reset HEAD^during a rebase discards the commit object from the branch, and the content survives only in your working tree. Do not rungit checkout .orgit stashcarelessly at this point: the reflog can recover the commit, but only if you know its SHA. Note it first withgit rev-parse HEADbefore resetting, and keep the terminal open until the split is finished.
Step 3 β Stage each slice with add --patch Jump to heading
# Slice 1: the formatting-only changes
git add --patch src/settlement/format.ts
# y = stage this hunk, n = skip, s = split it smaller, e = edit by hand, ? = help
git commit -m "style: reformat the settlement module
No functional change; produced by the project formatter." # Confirm the slice is coherent before moving on
git show --stat HEAD
git diff --cached --stat # empty: everything intended is committed
npm test --silent # this commit must build and pass on its own Repeat for each slice. The four interactive commands do all the work:
# When every slice is committed, resume the rebase
git status --short # must be empty: nothing left uncommitted
git rebase --continue Step 4 β Verify the final tree is byte-identical Jump to heading
A split must not change what the branch produces. This is the check that proves it.
# The SHA you noted before resetting, in Step 2
original=9f8e7d6
# Compare the resulting tree against the original commit's tree
git diff "$original" HEAD --stat
# Expect: no output β the content is identical, only the commit boundaries moved # And confirm each intermediate commit builds
git rebase -i --exec 'npm test --silent' HEAD~5
# Expect: the rebase completes; any failing commit stops it with the SHA When a slice will not build on its own Jump to heading
Occasionally a boundary that looked clean produces a commit that does not compile β the rename slice changes a call site whose definition arrives in the next slice, or a schema migration references a column added later. Two responses are correct and one is not.
Move the boundary. If a definition and its first caller genuinely cannot be separated, they are one change and belong in one commit; forcing them apart produces a commit that exists only to satisfy a rule. Reorder instead. Dependency order is usually the fix: the additive change goes first, the change that consumes it second, and a broken intermediate state disappears. What is not correct is shipping a knowingly broken commit with a note in the message, because that is precisely the commit git bisect will land on during an incident, at which point the note is no help at all.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Must every intermediate commit build? Jump to heading
It should, and it is what makes the split worth doing. A sequence where each commit builds and passes tests can be bisected, reverted individually and reviewed independently. If a slice genuinely cannot stand alone, that is a signal the boundary is in the wrong place β usually the interface change and its first caller belong in the same commit.
What if the changes are interleaved in one file? Jump to heading
git add --patch splits by hunk, and the s command splits a hunk further. When two changes share adjacent lines, use e to edit the staged hunk by hand β you keep the lines belonging to this slice and drop the rest, and the remainder stays in the working tree for the next commit.
Is this safe on a pushed branch? Jump to heading
It rewrites commits, so it is safe only on a branch nobody else has checked out, and it requires a force push afterwards. On a branch under review where colleagues have pulled, splitting orphans their copies. Do it before requesting review, or coordinate explicitly if the branch is already shared.
Related Jump to heading
- Interactive Rebase Workflows β the parent guide: the todo list, its commands, and how to recover from a rebase gone wrong.
- Safe git rebase -i for Shared Branches β what changes when someone else has the branch.
- Using Fixup Commits and Autosquash During Review β the opposite operation: folding review corrections back into the commit they belong to.