← Back to all posts

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:

git rebase --onto origin/main B — commit graphB..HEADonto origin/mainABCDEFGEFGorigin/mainyour-branch (HEAD)
base branchcutoff (upstream)replayed rangenew commitsabandoned originals
Before: this cuts at B and replays B..HEADE F G.

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 B cuts where you told it to — at B.
  • git rebase origin/main cuts 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:

git rebase origin/main — commit graphorigin/main..HEADonto origin/mainABP+QPQEFGPQEFGorigin/mainyour-branch (HEAD)
base branchcutoff (upstream)already merged into the basereplayed rangenew commitsduplicated work → conflictsabandoned originals
Before: this cuts at A and replays origin/main..HEADP Q E F G. That range includes P Q, whose changes origin/main already has in P+Q.

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

--onto names the destination. The next argument is the "delete everything up to and including this" line.

Cut here, land there.