Understanding git rebase --onto
Published on 2026-07-09
git rebase --onto is the surgical version of rebase: it lets you cut a run of
commits off one point in your history and replant them somewhere else entirely.
It looks cryptic the first time you see it, but there's a simple model
underneath.
The one sentence that explains it
The set of commits that gets replayed is exactly <upstream>..<branch> —
everything reachable from <branch> but not from <upstream>. Those
commits are then re-applied, in order, on top of <newbase>.
That's the whole feature. Three commits, named in one command:
git rebase --onto <newbase> <upstream> [<branch>]
<newbase>— where the commits land.<upstream>— where you cut. This commit, and everything before it, is not replayed.<branch>— the tip of the range. Omit it and Git uses your current branch (HEAD).
A concrete command
Say you run:
git rebase --onto origin/main HASH
Mapping that onto the three slots:
<newbase>=origin/main<upstream>=HASH<branch>= omitted → the current branch (HEAD)
In English: "Take the commits in HASH..HEAD — everything after HASH, up to
my current tip — and replant them on top of origin/main. Drop HASH and
everything before it from my branch's new lineage."
See it happen
In the graph below, the cutoff is B. The command replays B..HEAD — the
commits E, F, G — onto D, the tip of origin/main. Press
Run rebase:
E, F, and G become brand-new commits E', F', G' — new hashes, a new parent. B is not replayed: it was the cutoff, the boundary of the range, not part of it. The faded originals are still rooted at B, but nothing points to them anymore.
Now press the other command
Hit Reset, switch to git rebase origin/main, and run it again.
Nothing changes. Same commits move, same distance, to the same place.
That's not a shortcut in the diagram. On this graph the two commands are genuinely the same rebase:
git rebase --onto origin/main Bcuts where you told it to — at B.git rebase origin/maincuts at the merge base: the newest commit both branches already share. On this graph that is also B.
Same cut, same range, same landing spot. If your branch forked straight off
main, --onto buys you exactly nothing, and the extra arguments are noise.
Why --onto exists at all
Plain git rebase <upstream> makes one commit do two jobs: it is the cutoff
and the new base. That's fine when the thing you want to cut at is the thing
you want to land on — which is precisely the graph above.
--onto splits those two jobs apart. It lets you say "cut here, but land
there" — two different commits. So the flag only earns its keep when the point
you need to cut at is not on the branch you need to land on.
Where the two commands come apart
That happens when your branch was built on top of another branch that has since been squash-merged.
Picture it: you branched off a feature branch (P, Q), and that feature
branch got squash-merged into main as a single commit. It's drawn below as
P+Q, because that is exactly what it holds: both commits' changes, flattened
into one. What it is not is P and Q. It's a different commit with its own
hash, and nothing on it records where those changes came from. Git cannot see
the connection, only you can.
Now the two commands disagree. Flip between them:
Run git rebase origin/main first. Git cuts at the merge base A — as far as
it can tell, P and Q are simply commits you haven't merged yet — and replays all
five. P' and Q' re-apply changes that P+Q already contains. In practice that
is a conflict in every file the parent branch touched, and if you grind through
the resolutions you land the same work in main twice.
Now switch to git rebase --onto origin/main Q. Same graph, one difference:
the cut moves from A up to Q. P and Q fall outside the range and stay
exactly where they are; only E, F, G replay onto origin/main. No conflict,
no duplicated work.
That's the entire flag. One argument moved the cut point, and nothing else changed:
git rebase --onto origin/main <old-parent-tip>
The hard part isn't the syntax — it's noticing you're in this situation at all. The tell is a rebase that conflicts on changes you know are already upstream.
A mnemonic
--ontonames the destination. The next argument is the "delete everything up to and including this" line.
Cut here, land there.