Stacked pull requests without a dedicated tool Jump to heading
Dependent work creates a choice nobody likes: bundle three changes into one enormous pull request, or wait for the first to merge before starting the second. Stacking is the third option β each change is its own pull request, based on the one before it, reviewed in parallel and merged in order. The reputation for complexity comes almost entirely from restacking after a rebase, and Git has had a flag for that since 2.38. This recipe uses plain Git throughout, within code review workflow engineering.
When to use this approach Jump to heading
- A change naturally decomposes into steps that build on each other.
- The first step is reviewable on its own and would be merged on its own merits.
- Waiting for each review before starting the next would cost days.
- Your forge lets a pull request target a branch other than the default.
- If the parts are independent rather than dependent, open them side by side instead; stacking adds ordering you do not need.
Step 1 β Build the stack, each branch on the one before Jump to heading
git switch -c refactor/extract-client origin/main
# ... work, commit ...
git switch -c feat/retry-policy # based on the branch above, not on main
# ... work, commit ...
git switch -c feat/retry-metrics # third layer
# ... work, commit ... # Verification: each branch contains the one below it
git log --oneline --graph origin/main..feat/retry-metrics | head -12
git merge-base --is-ancestor refactor/extract-client feat/retry-policy && echo "stacked correctly" Step 2 β Target the correct base on each pull request Jump to heading
This is the step that makes each diff small. A pull request targeting the default branch shows every commit in the stack; targeting its parent shows only its own.
gh pr create --base main --head refactor/extract-client --title 'refactor: extract the HTTP client'
gh pr create --base refactor/extract-client --head feat/retry-policy --title 'feat: retry policy'
gh pr create --base feat/retry-policy --head feat/retry-metrics --title 'feat: retry metrics' # Verification: each diff should be the size of one step
for b in refactor/extract-client feat/retry-policy feat/retry-metrics; do
printf '%-30s ' "$b"; gh pr view "$b" --json additions,deletions --jq '.additions + .deletions'
done Add one line to each description saying what it sits on and what sits on it. Reviewers who land on the middle of a stack with no context assume they are looking at a broken change.
Step 3 β Restack after the base moves Jump to heading
The default branch advances while the stack is in review. --update-refs rebases the whole stack and moves every branch pointer in one operation.
git fetch origin
git switch feat/retry-metrics # the top of the stack
git rebase --update-refs origin/main # Verification: every branch moved, and the stacking is intact
git log --oneline --graph origin/main..feat/retry-metrics | head
git merge-base --is-ancestor refactor/extract-client feat/retry-policy && echo "still stacked" # Make it the default, so a plain rebase restacks
git config rebase.updateRefs true Without --update-refs, rebasing the top branch leaves the lower branch pointers where they were, and the stack silently becomes three unrelated branches containing duplicated commits β the state that gives stacking its bad reputation.
Step 4 β Force-push the stack safely Jump to heading
Rewritten branches need force-pushing, and the lease option is what stops you overwriting a colleagueβs push.
git push --force-with-lease origin \
refactor/extract-client feat/retry-policy feat/retry-metrics # Verification: the remote branches now match, and reviews can resume
for b in refactor/extract-client feat/retry-policy feat/retry-metrics; do
printf '%-30s %s\n' "$b" "$(git rev-parse --short "origin/$b")"
done SAFETY WARNING β never use plain
--forceon a branch that is under review.--force-with-leaserefuses when the remote has moved since your last fetch, which is exactly the case where someone else pushed a change you have not seen. Plain force discards it silently, and the author finds out when their commit is missing.
Give reviewers a range-diff after any restack so they can see that only the rebase changed β the technique is in reviewing a force-push with git range-diff.
Step 5 β Merge from the bottom, then restack Jump to heading
# Merge the bottom pull request first
gh pr merge refactor/extract-client --squash --delete-branch
# Retarget the next one at the default branch and restack
gh pr edit feat/retry-policy --base main
git fetch origin && git switch feat/retry-metrics && git rebase --update-refs origin/main
git push --force-with-lease origin feat/retry-policy feat/retry-metrics # Verification: the remaining stack has no duplicated commits
git log --oneline origin/main..feat/retry-metrics Squash merging the bottom of a stack rewrites its commits, so the restack afterwards is not optional β without it the next branch still contains the pre-squash commits and its diff shows them as new work.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if a reviewer wants a change in the bottom branch? Jump to heading
Make it there, then restack. git commit --fixup on the bottom branch followed by git rebase -i --autosquash --update-refs origin/main from the top applies the change and carries the whole stack with it in one operation.
Can CI run on every branch in the stack? Jump to heading
It can and it should, though the middle branches are testing a state that will never exist on the default branch exactly as written. Treat their results as information about that step rather than as a guarantee about the final merge, and let the merge queue validate the real tree.
Is a dedicated stacking tool worth it? Jump to heading
For teams doing this constantly, yes β the tools automate retargeting and restacking across many branches. For occasional stacks of two or three, --update-refs covers the hard part, and plain Git keeps the workflow legible to everyone rather than to whoever installed the tool.
Related Jump to heading
- Code Review Workflow Engineering β the parent topic and where stacking fits.
- Splitting a Branch Into Reviewable Pull Requests β producing the stack from work already written.
- Reviewing a Force-Push With git range-diff β keeping reviews alive through restacks.