Splitting a branch into reviewable pull requests Jump to heading
The change is finished, it works, and it is nine hundred lines across four concerns. Reviewers will approve it without reading it, which is the outcome everyone involved would rather avoid. Splitting after the fact is less painful than its reputation suggests, and the verification step at the end — proving that the parts produce exactly the original tree — is what makes it safe to do quickly. This recipe covers the three cases, within code review workflow engineering.
When to use this approach Jump to heading
- A branch has grown past what a reviewer can read in one sitting.
- The change contains a refactor and a feature that could be reviewed separately.
- A reviewer has asked to see it as smaller pieces.
- Part of the work is uncontroversial and could merge today.
- If the change is genuinely one indivisible decision, do not split it — book a longer review slot instead.
Step 1 — Find the seams Jump to heading
The seams are usually visible in the commit history or in which files change together.
# The branch as a sequence, with sizes
git log --oneline --stat origin/main..HEAD | head -40 # Files grouped by the commits that touched them
git log --format='---%h' --name-only origin/main..HEAD \
| awk '/^---/{c=substr($0,4);next}{if($0!="")print c, $0}' \
| awk '{a[$2]=a[$2]" "$1} END{for(f in a) print a[f], f}' | sort | head -20 # Verification: a candidate prefix should build and test on its own
git switch -c probe "$(git rev-list --reverse origin/main..HEAD | sed -n '3p')"
npm test >/dev/null 2>&1 && echo "prefix is independently valid"
git switch - && git branch -D probe Step 2 — The easy case: cherry-pick a prefix Jump to heading
When the commits already separate the concerns, the split is two commands.
seam=$(git rev-list --reverse origin/main..HEAD | sed -n '3p')
git switch -c part-1/extract-client origin/main
git cherry-pick "$(git rev-list --reverse origin/main..HEAD | head -1)".."$seam" # The remainder becomes the second change, based on the first
git switch -c part-2/retry-policy part-1/extract-client
git cherry-pick "$seam"..original-branch # Verification: the two parts together equal the original tree
git diff --exit-code part-2/retry-policy original-branch && echo "trees identical" That last check is the one to keep. It proves the split lost nothing, which is the only real risk of the operation.
Step 3 — Splitting one commit by path Jump to heading
# Start from the commit's parent with its changes staged
git switch -c split-work "$(git rev-parse HEAD~1)"
git cherry-pick -n HEAD@{1} # apply without committing
# First part: only the paths belonging to one concern
git reset
git add src/http/ && git commit -m 'refactor: extract the HTTP client'
# Second part: whatever is left
git add -A && git commit -m 'feat: add the retry policy' # Verification: each commit should build on its own
git rebase --exec 'npm run build --silent' origin/main git rebase --exec running the build over every commit is the cheapest guard against a split that produces an intermediate state that does not compile — which is the second real risk, and the one reviewers notice.
Step 4 — Splitting inside a file, hunk by hunk Jump to heading
# Interactive staging: choose hunks, or split them further with 's'
git add -p src/client.ts
git commit -m 'refactor: extract the request builder'
# Then the rest
git add -p src/client.ts
git commit -m 'feat: retry on 5xx responses' # Verification: nothing is left unstaged by accident
git status --short
git diff --exit-code && echo "working tree clean" SAFETY WARNING — a hunk-level split can produce a commit that does not compile, because half of a change was left in the next commit. That is invisible until someone bisects through the range months later and hits a build failure that has nothing to do with what they were hunting. Run the build over every commit with
git rebase --execbefore pushing; the technique matters most for the repositories where automating git bisect with a test script is part of the routine.
Step 5 — Open them as a stack, in order Jump to heading
gh pr create --base main --head part-1/extract-client \
--title 'refactor: extract the HTTP client' \
--body 'First of two. The retry policy follows in a stacked change.'
gh pr create --base part-1/extract-client --head part-2/retry-policy \
--title 'feat: retry policy' \
--body 'Stacked on the client extraction.' # Verification: the second diff contains only its own step
gh pr view part-2/retry-policy --json additions,deletions --jq '.additions + .deletions' The mechanics of keeping the stack current while both are in review are in stacked pull requests without a dedicated tool.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is it worth splitting once the work is already done? Jump to heading
When the change is large enough that review would otherwise be a formality, yes — the split takes under an hour and the review it enables is the point of having review at all. When the change is moderately large and coherent, the honest answer is often no.
What if the parts cannot be ordered — each needs the other? Jump to heading
Then they are one change, and splitting will produce two commits that neither build nor make sense alone. That situation is rarer than it feels; the usual case is that an interface can be introduced first and its caller second.
How do I keep the original branch as a safety net? Jump to heading
Leave it alone and work on new branches, which is what the recipe above does. Once both parts have merged, compare the default branch against the original one final time and then delete it — or archive it as a tag if you want a permanent record.
Related Jump to heading
- Code Review Workflow Engineering — the parent topic and why size dominates review latency.
- Stacked Pull Requests Without a Dedicated Tool — running the parts in parallel.
- Splitting a Large Commit Into Reviewable Pieces — the same operation at the level of a single commit.