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 can get messy when your team uses a --squash
merge strategy where each PR turns into a single commit when merged into main
.
Once you squash-merge first
into main
, all of those commits are replaced with a single commit on main
. This is great for historical context and readability of commit history in your main
branch, but your next PRs (second
and third
) are still branched off the old history, so they’ll suddenly show all of first
’s commits too. The diffs get noisy and reviewers can’t tell what’s actually new.
Here’s how to fix that without recreating branches or PRs.
Step 1: Rebase the next branch (second) onto main
This tells Git: “replay just the commits unique to this branch on top of main
, and drop everything from first
.”
git fetch origin
git checkout feature/second
git rebase --onto origin/main origin/feature/first
git push --force-with-lease
Then update the second
PR’s target branch to main
. Now you'll have a clean second
branch and PR with just its changes.
Step 2: Rebase deeper branches (third) with --fork-point
Once you’ve rebased second
, its history is now different. If third
was branched off that earlier version, it’ll now include all of first
and second
’s commits. The fix is to tell Git find where it originally forked during the rebase:
git fetch origin
git checkout feature/third
git rebase --onto origin/main --fork-point origin/feature/second
git push --force-with-lease
--fork-point
finds the right spot where third
branched from the old second
, even if the commit hashes changed during the rebase.
Why this works
A squash-merge creates a brand new commit on main.
Rebasing with --onto
(or --fork-point
) replays your branch’s commits on top of that new base, dropping any that already exist upstream.
You end up with:
- main → clean history, each squash commit representing a full PR.
- stacked branches → rebased to show only what’s new from that branch.
TL;DR
If you’re stacking PRs, every time you squash-merge one into main
, rebase the next one with:
git rebase --onto origin/main --fork-point origin/<previous-branch>
Then push --force-with-lease
and retarget the PR to main
.
It’s a small bit of cleanup that keeps each PR focused, readable, and mergeable.
Top comments (0)