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
Which splitting technique the branch needsIf the commits already separate the concerns, cherry-picking a prefix is enough. If one commit mixes concerns but they live in different files, splitting by path works. If the concerns are interleaved inside the same files, the split has to happen hunk by hunk.How are the concerns distributed across the commits?separate commitsCherry-pick a prefixminutesone commit, different filesSplit by pathstraightforwardinterleaved in one fileSplit by hunkslowest, still worth itcheck this before starting — the three techniques have very different costs

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 --exec before pushing; the technique matters most for the repositories where automating git bisect with a test script is part of the routine.

The split, with the verification that makes it safeWork is separated into parts, each part is checked to build on its own, and the final tree is compared against the original branch. The comparison is what proves nothing was dropped between the parts.Find the seamcommits or pathsSplitcherry-pick, add -pBuild each commitrebase --execCompare treesdiff --exit-codeagainst the originalthe last box takes one second and removes the only serious risk

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.

What the split does to review timeOne nine-hundred-line change waits for a slot large enough to read it. Two smaller changes are each picked up within hours, and the first merges while the second is still being reviewed — so the total elapsed time falls even though there are two reviews.hours from opened to mergedone 900-line change52 hpart 1 of 29 hpart 2 of 226 hthe second part starts its review before the first has merged, so these overlap

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.