Skip to content

Rebase Onto Explained


What Will We Learn?

By the end of this article, you’ll know the difference between three flavors of git rebase:

  • Plain git rebase <branch> — the everyday version
  • git rebase --onto <new-parent> <old-parent> — moving a chain of commits to a different base
  • git rebase --onto <new-parent> <old-parent> <until> — moving only a slice of that chain

We’ll also cover why the three-arg version leaves you in a detached HEAD, and how to not lose your work when that happens.

Markdown is supported.

Start Now

Let’s build a small repo we can break (and fix) safely:

mkdir rebase-onto
cd rebase-onto
git init
echo "A" > file; git add file; git commit -m "A"
echo "B" > file; git add file; git commit -m "B"
git checkout -b feat
echo "D" > file; git add file; git commit -m "D"
git checkout master
echo "C" > file; git add file; git commit -m "C"
git checkout feat
echo "E" > file; git add file; git commit -m "E"
echo "F" > file; git add file; git commit -m "F"

That gives us two diverged branches — master with its own commit, and feat with three of its own:

    ---
config:
  gitGraph:
    mainBranchName: master
---
gitGraph
    commit id: "A"
    commit id: "B"
    branch feat
    checkout feat
    commit id: "D"
    checkout master
    commit id: "C"
    checkout feat
    commit id: "E"
    commit id: "F"
  

Note: In the examples below, letters like A, B, D stand in for commit messages so the diagrams stay readable — they are not valid git refs on their own. When you actually run these commands, swap them for real commit hashes (git log --oneline) or a :/<message> search, e.g. git rebase --onto :/A :/B.

Normal Rebase (no --onto)

Syntax:

git rebase <branch>

Meaning:

Take every commit that’s reachable from HEAD but not from <branch> (i.e. the commits unique to your current branch), and replay them on top of the tip of <branch>.

In other words: it finds the first commit after your branching point, and moves it — along with everything built on top of it — so its new parent is the latest commit on <branch>.

Before:

git log --oneline --graph --all
* a7ce441 (HEAD -> feat) F
* aff63c7 E
* df3eb9f D
| * 23ef42f (master) C
|/
* fc29c9a B
* 7687849 A
A---B---C (master)
     \
      D---E---F (HEAD -> feat)

At this point: branch = master first commit unique to feat = D tip of master = C

git checkout feat
git rebase master

Git takes D (the first commit unique to feat) and re-parents it onto C (the tip of master). Everything after DE and F — comes along for the ride since they’re just children of D.

Result:

A---B---C---D'---E'---F' (HEAD -> feat)
        |
      (master)
* 9d54b24 (HEAD -> feat) F
* 84bd507 E
* 4733bd0 D
* 23ef42f (master) C
* fc29c9a B
* 7687849 A

Note the new hashes — rebase doesn’t move commits, it creates new ones with the same changes but a different parent, then points your branch at the last new commit.

Two Args Rebase (--onto <new-parent> <old-parent>)

--onto simply means on top of. This form lets you pick a base other than the commit you’re comparing against.

Syntax:

git rebase --onto <new-parent> <old-parent>

Meaning:

Take every commit in the range (old-parent, HEAD] — i.e. everything after <old-parent>, up to and including your current HEAD — and replay it on top of <new-parent>.

This is really the same mechanism as the normal rebase above, except you get to name the old base explicitly instead of git inferring it from <branch>. That’s what makes it useful for detaching a branch from a base it was never supposed to depend on.

Before:

* 300b860 (HEAD -> feat) F
* 0661834 E
* 464ea49 D
| * 675992d (master) C
|/
* 4680cfb B
* 5b66a0b A
A---B---C (master)
     \
      D---E---F (HEAD -> feat)

Say you want to change the parent of D from B to A — effectively cutting B out of feat’s history entirely:

old-parent of D = B new-parent = A

git checkout feat # because D is in feat
git rebase --onto A B

Result:

A---B---C (master)
 \
  D'---E'---F' (HEAD -> feat)
* e361623 (HEAD -> feat) F'
* d9f1440 E'
* 8aecdec D'
| * 675992d (master) C
| * 4680cfb B
|/
* 5b66a0b A

feat no longer contains B at all — the whole D-E-F chain now branches directly off A.

Three Args Rebase (--onto <new-parent> <old-parent> <until>)

Syntax:

git rebase --onto <new-parent> <old-parent> <until>

Meaning:

Take the commits in the range (old-parent, until] and replay them on top of <new-parent>. <until> becomes the new tip after the rebase.

Because <until> is usually a bare commit — not a branch name — HEAD ends up detached and pointing at the new tip instead of at any branch.

Careful: switching branches out of that detached HEAD doesn’t undo the rebase — the new commits still exist, they just stop being referenced by anything. Git will warn you and they’ll stay recoverable via git reflog for a while, but eventually they’re eligible for garbage collection. The safe move is to create a branch first if you want to keep them:

git branch <new-branch-name>

See it happen:

git switch master
Warning: you are leaving 2 commits behind, not connected to
any of your branches:

  9bd9208 E'
  e50ddbf D'

If you want to keep them by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 9bd9208

Switched to branch 'master'

git reflog still has the full trail:

7f331dc (HEAD -> master) HEAD@{0}: checkout: moving from 9bd9208d81c882de63846bc16fa7970a62ba39e7 to master
9bd9208 HEAD@{1}: rebase (continue): E'
e50ddbf HEAD@{2}: rebase (continue): D'
2dce658 HEAD@{3}: rebase (start): checkout 2dce
ad58d5f (feat) HEAD@{4}: commit: F
5fc1ff1 HEAD@{5}: commit: E
9b9454a HEAD@{6}: checkout: moving from master to feat
7f331dc (HEAD -> master) HEAD@{7}: commit: C
725c4b6 HEAD@{8}: checkout: moving from feat to master
9b9454a HEAD@{9}: commit: D
725c4b6 HEAD@{10}: checkout: moving from master to feat
725c4b6 HEAD@{11}: commit: B
2dce658 HEAD@{12}: commit (initial): A

Before:

* 2ca50ad (HEAD -> feat) F
* 6b164b7 E
* c14a7fb D
| * 3a1f620 (master) C
|/
* 2cc57bd B
* c661fb9 A
A---B---C (master)
     \
      D---E---F (HEAD -> feat)

Say you want A to be the parent of D, but only up through E — skipping F entirely:

new-parent = A old-parent = B until = E

git checkout feat
git rebase --onto A B E

The range is (B, E]B is excluded, E is included, which covers D and E. HEAD ends up pointing at the new E'. The originals — D, E, and F — are untouched and still live on feat.

After:

  D'---E' (HEAD, detached)
 /
A---B---C (master)
     \
      D---E---F (feat)
* 9bd9208 (HEAD) E'
* e50ddbf D'
| * ad58d5f (feat) F
| * 5fc1ff1 E
| * 9b9454a D
| | * 7f331dc (master) C
| |/
| * 725c4b6 B
|/
* 2dce658 A

HEAD isn’t attached to master or feat — it’s just sitting on E'. Checking out any branch will re-attach HEAD to it (and, as covered above, leave D'/E' dangling unless you saved them to a branch first).

Picking the Right One

    flowchart TD
    A["Need to move commits<br/>to a new base?"] --> B{"Want everything<br/>unique to HEAD?"}
    B -->|"Yes, base = a branch tip"| C["Normal rebase<br/>git rebase branch"]
    B -->|"No, custom old base"| D{"Move commits all<br/>the way to HEAD?"}
    D -->|"Yes"| E["Two-arg --onto<br/>git rebase --onto new old"]
    D -->|"No, stop earlier"| F["Three-arg --onto<br/>git rebase --onto new old until"]
  
FormCommandWhat movesWhere HEAD ends up
Normalgit rebase <branch>Everything unique to HEADStays attached to current branch
Two-arg --ontogit rebase --onto <new> <old>Range (old, HEAD]Stays attached to current branch
Three-arg --ontogit rebase --onto <new> <old> <until>Range (old, until]Detached (unless <until> is a branch name)

When to reach for each

Normal rebasegit rebase <branch>

  • Bringing a feature branch up to date with main/master before opening a PR
  • Running git pull --rebase to replay local commits on top of freshly fetched remote ones
  • Flattening history before a merge so you avoid noisy merge commits
  • Rebasing after a dependency branch got squash-merged upstream

Two-arg --ontogit rebase --onto <new> <old>

  • Your feature branch was accidentally cut off another feature branch instead of main, and you need to move it directly onto main
  • A shared base branch got rewritten (e.g. someone rebased develop), and you need to replay your commits onto the new version of it
  • You want to drop a batch of unwanted commits by picking a later “old-parent” than the actual branch point
  • Detaching a branch from history it shouldn’t depend on anymore, without touching the commits built on top

Three-arg --ontogit rebase --onto <new> <old> <until>

  • Splitting a long-running feature branch into two — extracting an earlier, self-contained chunk into its own branch/PR
  • Cherry-picking a contiguous block of commits onto a hotfix branch, deliberately leaving later, unrelated commits behind
  • Isolating a specific commit range to test or bisect against, without the noise of commits added after it
  • Recovering the “good” part of a branch after later commits turned out to be broken or experimental
Last updated on