How to git rebase a branch with the onto command?
GitGit RebaseGit Problem Overview
I have noticed that the two blocks of following git commands have different behaviours and I don't understand why.
I have an A
and a B
branches that diverge with one commit
---COMMIT--- (A)
\
--- (B)
I want to rebase B
branch on the lastest A
(and have the commit on the B
branch)
---COMMIT--- (A)
\
--- (B)
No problem if I do:
checkout B
rebase A
But if I do:
checkout B
rebase --onto B A
It doesn't work at all, nothing happens. I don't understand why the two behaviours are different.
PhpStorm GIT client uses the second syntax, and so seems to be completely broken, that's why I ask for this syntax issue.
Git Solutions
Solution 1 - Git
tl;dr
The correct syntax to rebase B
on top of A
using git rebase --onto
in your case is:
git checkout B
git rebase --onto A B^
or rebase B
on top of A
starting from the commit that is the parent of B
referenced with B^
or B~1
.
If you're interested in the difference between git rebase <branch>
and git rebase --onto <branch>
read on.
The Quick: git rebase
git rebase <branch>
is going to rebase the branch you currently have checked out, referenced by HEAD
, on top of the latest commit that is reachable from <branch>
but not from HEAD
.
This is the most common case of rebasing and arguably the one that requires less planning up front.
Before After
A---B---C---F---G (branch) A---B---C---F---G (branch)
\ \
D---E (HEAD) D---E (HEAD)
In this example, F
and G
are commits that are reachable from branch
but not from HEAD
. Saying git rebase branch
will take D
, that is the first commit after the branching point, and rebase it (i.e. change its parent) on top of the latest commit reachable from branch
but not from HEAD
, that is G
.
The Precise: git rebase --onto with 2 arguments
git rebase --onto
allows you to rebase starting from a specific commit. It grants you exact control over what is being rebased and where. This is for scenarios where you need to be precise.
For example, let's imagine that we need to rebase HEAD
precisely on top of F
starting from E
. We're only interested in bringing F
into our working branch while, at the same time, we don't want to keep D
because it contains some incompatible changes.
Before After
A---B---C---F---G (branch) A---B---C---F---G (branch)
\ \
D---E---H---I (HEAD) E---H---I (HEAD)
In this case, we would say git rebase --onto F D
. This means:
> Rebase the commit reachable from HEAD
whose parent is D
on top of F
.
In other words, change the parent of E
from D
to F
. The syntax of git rebase --onto
is then git rebase --onto <newparent> <oldparent>
.
Another scenario where this comes in handy is when you want to quickly remove some commits from the current branch without having to do an interactive rebase:
Before After
A---B---C---E---F (HEAD) A---B---F (HEAD)
In this example, in order to remove C
and E
from the sequence you would say git rebase --onto B E
, or rebase HEAD
on top of B
where the old parent was E
.
The Surgeon: git rebase --onto with 3 arguments
git rebase --onto
can go one step further in terms of precision. In fact, it allows you to rebase an arbitrary range of commits on top of another one.
Here's an example:
Before After
A---B---C---F---G (branch) A---B---C---F---G (branch)
\ \
D---E---H---I (HEAD) E---H (HEAD)
In this case, we want to rebase the exact range E---H
on top of F
, ignoring where HEAD
is currently pointing to. We can do that by saying git rebase --onto F D H
, which means:
> Rebase the range of commits whose parent is D
up to H
on top of F
.
The syntax of git rebase --onto
with a range of commits then becomes git rebase --onto <newparent> <oldparent> <until>
. The trick here is remembering that the commit referenced by <until>
is included in the range and will become the new HEAD
after the rebase is complete.
Solution 2 - Git
This is all you need to know to understand --onto
:
git rebase --onto <newparent> <oldparent>
You're switching a parent on a commit, but you're not providing the sha of the commit, only the sha of it's current (old) parent.
Solution 3 - Git
Put shortly, given:
Before rebase After rebase
A---B---C---F---G (branch) A---B---C---F---G (branch)
\ \ \
D---E---H---I (HEAD) \ E'---H' (HEAD)
\
D---E---H---I
git rebase --onto F D H
Which is the same as (because --onto
takes one argument):
git rebase D H --onto F
Means rebase commits in range (D, H] on top of F. Notice the range is left-hand exclusive. It's exclusive because it's easier to specify 1st commit by typing e.g. branch
to let git
find the 1st diverged commit from branch
i.e. D
which leads to H
.
OP case
o---o (A)
\
o (B)(HEAD)
git checkout B
git rebase --onto B A
Can be changed to single command:
git rebase --onto B A B
What looks like error here is placement of B
which means "move some commits which lead to branch B
on top of B
". The questions is what "some commits" are. If you add -i
flag you will see it is single commit pointed by HEAD
. The commit is skipped because it is already applied to --onto
target B
and so nothing happens.
The command is nonsense in any case where branch name is repeated like that. This is because the range of commits will be some commits which are already in that branch and during rebase all of them will be skipped.
git rebase <upstream> <branch> --onto <newbase>
.
Further explanation and applicable usage of git rebase
defaults.
git rebase master
Expands to either :
git rebase --onto master master HEAD
git rebase --onto master master current_branch
Automatic checkout after rebase.
When used in standard way, like:
git checkout branch
git rebase master
You won't notice that after rebase git
moves branch
to most recently rebased commit and does git checkout branch
(see git reflog
history). What is interesting when 2nd argument is commit hash instead branch name rebase still works but there is no branch to move so you end up in "detached HEAD" instead being checked out to moved branch.
Omit primary diverged commits.
The master
in --onto
is taken from 1st git rebase
argument.
git rebase master
/ \
git rebase --onto master master
So practicaly it can be any other commit or branch. This way you can limit number of rebase commits by taking the latest ones and leaving primary diverged commits.
git rebase --onto master HEAD~
git rebase --onto master HEAD~ HEAD # Expanded.
Will rebase single commit pointed by HEAD
to master
and end up in "detached HEAD".
Avoid explicit checkouts.
The default HEAD
or current_branch
argument is contextually taken from place you're in. This is why most people checkout to branch which they want to rebase. But when 2nd rebase argument is given explicitly you don't have to checkout before rebase to pass it in implicit way.
(branch) $ git rebase master
(branch) $ git rebase master branch # Expanded.
(branch) $ git rebase master $(git rev-parse --abbrev-ref HEAD) # Kind of what git does.
This means you can rebase commits and branches from any place. So together with Automatic checkout after rebase. you don't have to separately checkout rebased branch before or after rebase.
(master) $ git rebase master branch
(branch) $ # Rebased. Notice checkout.
Solution 4 - Git
To better understand difference between git rebase
and git rebase --onto
it is good to know what are the possible behaviors for both commands. git rebase
allow us to move our commits on top of the selected branch. Like here:
git rebase master
and the result is:
Before After
A---B---C---F---G (master) A---B---C---F---G (master)
\ \
D---E (HEAD next-feature) D'---E' (HEAD next-feature)
git rebase --onto
is more precises. It allows us to choose specific commit where we want to start and also where we want to finish. Like here:
git rebase --onto F D
and the result is:
Before After
A---B---C---F---G (branch) A---B---C---F---G (branch)
\ \
D---E---H---I (HEAD my-branch) E'---H'---I' (HEAD my-branch)
To get more details I recommend you to check out my own article about git rebase --onto overview
Solution 5 - Git
Git wording is a bit confusing here. It might help if you pretended that the command looks like this:
git rebase --onto=<new_base> <old_base> [<branch>]
If we are on branch
now, it can be omitted:
git rebase --onto=<new_base> <old_base>
And if new_base
is the same as old_base
, we can omit the --onto
parameter:
git rebase <new_old_base>
This might sound weird: how are you rebasing if the old base is the same as new base? But think about it like this, if you have a feature branch foo
, it's already (likely) based on some commit in your main
branch. By “re-basing”, we are only making the commit it's based on more current.
(In fact, <old_base>
is something that we compare branch
against. If it's a branch, then git looks for a common ancestor (see also --fork-point
); if it's a commit on current branch, the commits after that are used; if it's a commit that has no common ancestor with current branch, all commits from current branch are used. <new_base>
can also be a commit. So, for instance, git rebase --onto HEAD~ HEAD
will take commits between old base HEAD
and current HEAD
and place them on top of HEAD~
, effectively deleting the last commit.)
Solution 6 - Git
Simply put, git rebase --onto
selects a range of commits and rebases them on the commit given as parameter.
Read the man pages for git rebase
, search for "onto". The examples are very good:
example of --onto option is to rebase part of a branch. If we have the following situation:
H---I---J topicB
/
E---F---G topicA
/
A---B---C---D master
then the command
git rebase --onto master topicA topicB
would result in:
H'--I'--J' topicB
/
| E---F---G topicA
|/
A---B---C---D master
In this case you tell git to rebase the commits from topicA
to topicB
on top of master
.
Solution 7 - Git
For onto
you need two additional branches. With that command you can apply commits from branchB
that are based on branchA
onto another branch e.g. master
. In the sample below branchB
is based on branchA
and you want to apply the changes of branchB
on master
without applying the changes of branchA
.
o---o (master)
\
o---o---o---o (branchA)
\
o---o (branchB)
by using the commands:
checkout branchB
rebase --onto master branchA
you will get following commit hierarchy.
o'---o' (branchB)
/
o---o (master)
\
o---o---o---o (branchA)
Solution 8 - Git
There is another case where git rebase --onto
is hard to grasp: when you rebase onto a commit resulting of a symmetric difference selector (the three dots '...
')
Git 2.24 (Q4 2019) does a better job of managing that case:
See commit 414d924, commit 4effc5b, commit c0efb4c, commit 2b318aa (27 Aug 2019), and commit 793ac7e, commit 359eceb (25 Aug 2019) by Denton Liu (Denton-L
).
Helped-by: Eric Sunshine (sunshineco
), Junio C Hamano (gitster
), Ævar Arnfjörð Bjarmason (avar
), and Johannes Schindelin (dscho
).
See commit 6330209, commit c9efc21 (27 Aug 2019), and commit 4336d36 (25 Aug 2019) by Ævar Arnfjörð Bjarmason (avar
).
Helped-by: Eric Sunshine (sunshineco
), Junio C Hamano (gitster
), Ævar Arnfjörð Bjarmason (avar
), and Johannes Schindelin (dscho
).
(Merged by Junio C Hamano -- gitster
-- in commit 640f9cd, 30 Sep 2019)
> ## rebase
: fast-forward --onto
in more cases
> Before, when we had the following graph,
> A---B---C (master)
D (side)
> running 'git rebase --onto master... master side
' would result in D
being always rebased, no matter what.
At this point, read "What are the differences between double-dot '..
' and triple-dot "...
" in Git diff commit ranges?"
Here: "master...
" refers to master...HEAD
, which is B
: HEAD is side HEAD (currently checked out): you are rebasing onto B
.
What are you rebasing? Any commit not in master, and reachable from side
branch: there is only one commit fitting that description: D
... which is already on top of B
!
Again, before Git 2.24, such a rebase --onto
would result in D
being always rebased, no matter what.
> However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
That is akin to the rebase --onto B A
of the OP, which did nothing.
> Add detection to can_fast_forward
so that this case can be detected and a fast-forward will be performed.
First of all, rewrite the function to use gotos which simplifies the logic.
Next, since the
> options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
> conditions were removed in cmd_rebase
, we reintroduce a substitute in
can_fast_forward
.
In particular, checking the merge bases of upstream
and head
fixes a failing case in t3416
.
> The abbreviated graph for t3416 is as follows:
F---G topic
/
A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
> Before, Git would see that there was one merge base (C
, result of master...topic
), and the merge and onto were the same so it would incorrectly return 1, indicating that
we could fast-forward. This would cause the rebased graph to be 'ABCFG
'
when we were expecting 'ABCG
'.
A rebase --onto C F topic
means any commit after F
, reachable by topic
HEAD: that is G
only, not F
itself.
Fast-forwarding in this case would include F
in the rebased branch, which is wrong.
> With the additional logic, we detect that upstream and head's merge base
is F
. Since onto isn't F
, it means we're not rebasing the full set of
commits from master..topic
.
Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
> Add '-f
' to test cases that failed as a result of this change because
they were not expecting a fast-forward so that a rebase is forced.