A situation that happens a lot in teams: a teammate opens feature-a, you need their work, so you branch feature-b off it and keep going. A few days later feature-a gets merged into main. Now you want feature-b to sit on top of main.

A stacked branch

The obvious move doesn’t work

You run git rebase main and… conflicts. Lots of them, in code you never touched.

The reason: feature-a was squash-merged. Its 5 commits were combined into one brand-new commit S on main. Git doesn’t know that S and A1..A5 are “the same work”, they’re different commits. So git rebase main tries to replay all 7 commits on feature-b (A1..A5 plus your B1, B2) on top of S, and A1..A5 collide with their own squashed copy.

After the squash merge

And the “nuke it” move loses your work

git reset --hard main makes feature-b exactly main. A1..A5 are fine (they’re in S), but B1 and B2 are gone from the branch, plus any uncommitted changes. Not what you want.

git rebase --onto

What you actually want is: “take only my commits, the ones after A5, and replay them on main.” That’s exactly what --onto does:

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

git rebase --onto main A5 feature-b

Read it as: move the commits of feature-b that come after A5 onto main.

  • <new-base>: where the commits should land (main)
  • <old-base>: the last commit you don’t want to bring along (the tip of feature-a, A5)
  • <branch>: the branch to move (feature-b, optional if you’re already on it)

After rebase &ndash;onto

Only B1 and B2 are replayed. If they conflict, it’s a real conflict between your work and what landed on main, which is worth looking at.

Finding <old-base>

If the old feature-a branch still exists locally, just use its name:

git rebase --onto main feature-a feature-b

If it’s already deleted, list the commits on your branch and pick the last one that isn’t yours:

git log --oneline --format='%h %an %s' main..feature-b

Wrap up

git fetch origin
git rebase --onto origin/main <last-commit-of-feature-a> feature-b
git push --force-with-lease

--force-with-lease because the history changed; it refuses to push if someone else pushed to your branch in the meantime, unlike plain --force.

And if anything goes wrong mid-way: git rebase --abort puts everything back as it was. Even after a bad rebase finishes, git reflog still knows where your branch used to be.