Stacking PRs & Squashing Merges
Sometimes you're working on a feature that's too big for one PR and splitting it up makes it easier to review. The way I handle this is by creating a first feature branch, then branch second off of it, then third off of second. As each branch is ready for review, I open a PR and each subsequent PR stacks on the last, so reviewers can focus on one layer at a time.
That setup works really well, but it gets messy when your team uses a squash merge strategy where each PR turns into a single commit when merged into main.
Here's what happens: when you squash-merge first into main, Git collapses all of those commits into a single new commit. This is great for keeping main's history clean and readable. But your second and third branches are still branched off the old first—the one with all those individual commits. When they compare against the new main, they'll suddenly include all of first's commits in their diff. The diff becomes noisy and reviewers can't tell what's actually new in second or third.
The fix is to rebase your next branch back onto main. When you do that with the right flags, Git will replay just the commits unique to that branch on top of the new history, dropping anything that already exists upstream. Here's how.
Rebase the next branch (second) onto main
git fetch origin
git checkout feature/second
git rebase --onto origin/main origin/feature/first
git push --force-with-lease
The --onto flag tells Git: "replay the commits from second that don't exist in first, putting them on top of main instead." Then update the second PR's target branch to main. Now when reviewers look at the PR, they'll see only the changes second introduced—no noise from the merged first.
Rebase deeper branches (third) with --fork-point
Once you've rebased second, its commit history changes. If third was branched from the old version of second, it's still holding onto all of first and second's commits. You can't just use --onto again because you need Git to figure out where third originally forked from the old second, even though those commit hashes no longer exist. That's where --fork-point comes in:
git fetch origin
git checkout feature/third
git rebase --onto origin/main --fork-point origin/feature/second
git push --force-with-lease
The --fork-point flag does the heavy lifting: it finds the point where third originally branched from second, traces through the rebasing that happened to second, and figures out which commits in third are actually new. Then it replays just those commits on top of main. The result is what you want—third now shows only the changes it introduced, not everything from first and second.
Why this matters
The real value here is keeping each PR's diff clean and focused. When reviewers look at a stacked PR, they can actually see what that specific branch contributed without wading through noise from previous merges. It's a small bit of bookkeeping, but it makes the difference between a review that's clear and one that's confusing.
The technique is straightforward, but it's worth understanding what's actually happening. A squash-merge creates a new commit that replaces an entire branch's history. Your stacked branches are still pointing at the old history. Rebasing with the right flags replays your branch's work on top of the new baseline, so everything stays clean and readable. It's one of those moments where knowing how Git thinks about commits—not branches or PRs, but commits—suddenly makes a workflow problem disappear.