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 basegit 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,Dstand 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
HEADbut 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 AA---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 masterGit takes D (the first commit unique to feat) and re-parents it onto C (the tip of master). Everything after D — E 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 ANote 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 currentHEAD— 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 AA---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 BResult:
A---B---C (master)
\
D'---E'---F' (HEAD -> feat)* e361623 (HEAD -> feat) F'
* d9f1440 E'
* 8aecdec D'
| * 675992d (master) C
| * 4680cfb B
|/
* 5b66a0b Afeat 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 —HEADends up detached and pointing at the new tip instead of at any branch.
Careful: switching branches out of that detached
HEADdoesn’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 viagit reflogfor 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): ABefore:
* 2ca50ad (HEAD -> feat) F
* 6b164b7 E
* c14a7fb D
| * 3a1f620 (master) C
|/
* 2cc57bd B
* c661fb9 AA---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 EThe 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 AHEAD 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"]
| Form | Command | What moves | Where HEAD ends up |
|---|---|---|---|
| Normal | git rebase <branch> | Everything unique to HEAD | Stays attached to current branch |
Two-arg --onto | git rebase --onto <new> <old> | Range (old, HEAD] | Stays attached to current branch |
Three-arg --onto | git rebase --onto <new> <old> <until> | Range (old, until] | Detached (unless <until> is a branch name) |
When to reach for each
Normal rebase — git rebase <branch>
- Bringing a feature branch up to date with
main/masterbefore opening a PR - Running
git pull --rebaseto 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 --onto — git rebase --onto <new> <old>
- Your feature branch was accidentally cut off another feature branch instead of
main, and you need to move it directly ontomain - 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 --onto — git 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