Rebasing Stacked Branches Without the Squash-Merge Pain

A dependent branch, a squashed parent, and the Git flag most developers never reach for until it saves them.

You're on a branch that depends on another branch. Not a library dependency — a code dependency. The function you need, the schema change you're building on top of, the shared fix your feature assumes exists. That branch is still in review.

So you branch off it anyway, because waiting for a review to land before you can write a single line is not how the work gets done. You keep working. Review takes longer than expected — it always does. Eventually the base branch gets approved and merged, as a squash merge, because that's the team's convention: one clean commit per feature on main, no messy history.

Now you go back to your branch and run the routine command:

bash
git rebase main

And Git tries to replay every commit from the base branch on top of main — even though those commits are already there, just squashed into a single new commit with a different hash. Git doesn't know that. It sees commits it doesn't recognize, and it does what rebase always does with unrecognized commits: it replays them, one by one, and asks you to resolve every conflict along the way.

The frustrating part isn't that there are conflicts. It's that the conflicts are fake. The code is already there. You're being asked to manually reconcile changes against a version of themselves.

Why the plain rebase breaks

A squash merge collapses N commits into 1. The new commit's hash has no relationship to any of the original commits — Git can't trace lineage through it.

gitGraph
  commit id: "main before"
  branch base-branch
  checkout base-branch
  commit id: "C1 original commit"
  branch your-branch
  checkout your-branch
  commit id: "C2 your work"
  checkout main
  commit id: "S1 (squashed base branch)"

When you rebase your-branch onto main, Git's default behavior is to find every commit in your-branch that isn't an ancestor of main, and replay them in order. Since C1 predates the squash and isn't recognized as "already merged," it gets replayed too — on top of a main that already contains its changes (inside S1). Conflict.

Multiply this by every file C1 touched, and a five-minute rebase becomes a thirty-minute conflict-resolution exercise on commits you never needed to touch.

The fix: git rebase --onto

--onto lets you tell Git exactly which commits to move, and where to graft them — instead of letting it guess based on ancestry it can no longer see.

bash
git rebase --onto <new-base> <old-base> <your-branch>

In this scenario:

bash
git rebase --onto main base-branch your-branch

Read it as: take everything in your-branch that isn't already in base-branch, and replay only that, on top of main.

Before:

gitGraph
  commit id: "main"
  commit id: "S1"
  branch base-branch
  checkout base-branch
  commit id: "C1"
  branch your-branch
  checkout your-branch
  commit id: "C2"

After git rebase --onto main base-branch your-branch:

gitGraph
  commit id: "main"
  commit id: "S1"
  commit id: "C2'"

C1 is never replayed. It's already accounted for, inside S1. Only C2 — your actual new work — moves. Zero conflicts related to the squashed parent.

The workflow that avoids this entirely

The best fix is not reaching for --onto after the fact — it's structuring dependent work so you rarely need to.

  1. Create a shared technical base branch off the trunk for the piece everything else depends on.
  2. Branch every dependent feature off that base branch, not off the trunk directly.
  3. Once the base branch is reviewed and squash-merged, resync every dependent branch with a single --onto command instead of a painful rebase.
bash
git checkout base-branch
git checkout -b your-feature
# ... work ...
# base-branch gets squash-merged into main
git rebase --onto main base-branch your-feature

This turns "wait for review before starting" into "start now, resync in one command later" — without losing the clean, squashed history the team wants on the trunk.

When this isn't worth it

If your team merges fast — reviews land in hours, not days — you probably never hit this. Branching off the trunk directly and rebasing normally is simpler, and you shouldn't add process for a problem you don't have.

This pattern earns its place when dependent work is common and review turnaround is slow enough that "wait" isn't a real option. If that's not your situation, skip the ceremony. If it is, --onto is worth knowing before the pain teaches it to you the hard way.