How to git rebase a branch with the onto command?
Asked Answered
H

9

305

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.

Highpressure answered 28/4, 2015 at 8:20 Comment(3)
git-scm.com/book/ch3-6.htmlTansy
try git rebase --onto A A after checking out BPyromagnetic
After all this years from this question, I think the root understanding problem we faced with this argument is semantical : onto does not mean "onto" , it means "on top of"Highpressure
N
749

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 and including 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.

Necropsy answered 28/4, 2015 at 10:5 Comment(23)
Nice answer. Just a small addition for the general case: The <oldparent> name breaks down if the two parts of the range are on different branches. Generally it's: "Include every commit that's reachable from <until> but exclude every commit that's reachable from <oldparent>."Abdicate
@EnricoCampidoglio your answer completes nicely the official git-rebase doc with more practical usages. I have a case with 3 branches. Can you please give your opinion if my rebase attempt is correct: Branch off a branch, How to rebase on another branchLustreware
git rebase --onto <newparent> <oldparent> is best explanation of --onto behavior I've seen!Canady
Thanks! I was kinda struggling with the --onto option but this made it crystal clear! I don't even get it how I couldn't have understood it before :D Thanks for excellent "tutorial" :-)Nonalcoholic
While this answer is excellent, I feel it does not cover all possible cases. The last syntax form can also be used to express a more subtle type of rebase. Going off the example in Pro Git (2nd Ed.), D does not necessarily have to be an ancestor of H. Instead, D and H could also be commits with a common ancestor - in this case, Git will figure out their common ancestor and replay from that ancestor to H onto F.Telephonist
This was helpful. The man page doesn't explain the arguments at all.Albania
@EnricoCampidoglio Just loved the way this answer is explained!Gripe
I was reading after rebase for few hours and I could not understand why there is so many syntaxes. Your post makes it clear. You have a true gift for explaining.Buddybuderus
Excellent explanation in general, though it would probably be helpful to mention that the diagram of the after state in the 3 argument example (git rebase --onto D F H) only looks like that if you start out in a detached HEAD state. If there is a branch pointing to I in the initial state, you'll have two each of your E and H commits. Your diagram does look like it's referring to a detached HEAD in each case to someone who knows what to look for, but it's not obvious to someone who doesn't, and it makes a big difference (in the 3rd example)Crest
Common use-case: git rebase --onto 0922e48 master feature-a, This will take all commits on feature-a that aren’t on master and replay them on top of 0922e48.Pyrostat
That first explanation of the arguments should be in the man page for git, what's there is so much longer and harder to understand.Theatricalize
@Telephonist It's true that <oldparent> does not need to be an ancestor of H. It's more like that anything reachable from <oldparent> will not go into the rebase. I read git rebase <newparent> <oldparent> <until> as "move to <newparent> anything reachable from <until> that is not reachable from <oldparent>" .Merlynmermaid
life changing (and saving) explanationMemorize
@SimónRamírezAmaya your explanations seems the best. it matches also what is described here git-scm.com/book/en/v2/Git-Branching-Rebasing#rbdiag_eProlong
Wow, i love this explanation. Any reason you excluded the commits that didn't get rebased from your diagrams? Was going to edit it show a slightly clearer picture, but then thought there might a reason for thatBiddle
@RalphCallaway They're excluded from the graph because they become unreachable after the rebase. As such, they're eventually going to be deleted.Necropsy
Everytime I return to this answer (and I do really A LOT), I wish I could cast more than one upvote. One of best answers you could find on StackOverflow.Journey
In the surgeon example what happens to D and H? Are they lost in the void?Dewdrop
@ChristopherPisz They become dangling and are eventually going to be garbage collected.Necropsy
do a concrete example!Wilbert
Your explanation is really good, you might want to add the ' symbols to the commits that are replayed so it is clear that those are new and not the same sha-1 as the old ones :) thank you for taking the time to write this!Exuberant
I now memorize git rebase --onto <newparent> <oldparent> <ultimate head>. I deliberately name the last one "ultimate head" such that the parameters are in alphabetical order for maximum clarity. In combination with git log --oneline --graph I get a good visual reference of the graph I currently have, and to get the right hashes to copy.Dyche
What does the emphasized part of this quote mean: "the latest commit that is reachable from <branch> but not from HEAD?" Is there another way to say that?Vacillate
S
131

This is all you need to know to understand --onto:

git rebase --onto <newparent> <oldparent>

You're switching the parent on a commit, but you're not providing the SHA of the commit, only the SHA of its current (old) parent.

Stearin answered 22/8, 2016 at 14:6 Comment(5)
Short and easy. Actually, realizing that I have to provide parent commit of the one that I want to rebase instead of that commit took me the longest time.Shebashebang
Important piece of detail is that you pick a child of a oldparent from current branch because one commit can be parent to many commits but when you restrict yourself to current branch then commit can be parent only to one commit. In other words parent relation ship is unique on branch but does not have to be if you do not specify branch.Buddybuderus
To note: You need to be in the branch, or add the branch name as the 3rd parameter git rebase --onto <newparent> <oldparent> <feature-branch>Raycher
This answer is amazing, straight to the point as needed in this threadToxinantitoxin
What is the definition of a "parent"?Vacillate
B
30

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 precise. It allows us to choose a 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

Ballplayer answered 21/4, 2020 at 13:31 Comment(5)
@Makyen Sure, I will keep it in mind in the future :)Ballplayer
So, we can read git rebase --onto F D as set child of D's parent as F, can't we?Pyromagnetic
@Pyromagnetic Yes, I think so :)Ballplayer
no matter rebase directly or onto a specific tip, if the old tree was already created in 'remotes/origin/branchname', you will have to force push whatsoeverFonda
@Pyromagnetic that actually seems correct but I think sticking with the real definition is bullet proof. <newbase> <oldbase> is the the best way I've found of understanding rebase. you have an old base and you want it to use a new base... the base is a commit hash. so git rebase --onto F D is stating "F" is the new base and "D" is the old one... so you can see that "F" is replaced by "D". literally pluck the branch by the root (D) and stick it(D) exactly on "F". I've never gone wrong with this definition.Subacute
V
26

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.

Further explanation and applicable usage of git rebase <upstream> <branch> --onto <newbase>.

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.
Vickers answered 25/10, 2018 at 8:23 Comment(1)
This. Is. The Best. Explanation! I never grasped that "git rebase <upstream>" IS JUST SHORTHAND for "git rebase <upstream> --onto <upstream>". This post also taught me: WE ARE ALWAYS SELECTING A RANGE OF COMMITS -- From the 'git rebase' docs: "Reapply COMMITS on top of another base tip"Skillet
W
23

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.)

Wainscoting answered 15/9, 2020 at 21:16 Comment(0)
S
15

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.

Schauer answered 28/4, 2015 at 9:5 Comment(0)
F
9

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)
Feaster answered 28/4, 2015 at 8:34 Comment(4)
Can you please explain a little more, if we want to rebase onto master, how come it becomes the current branch? If you do rebase --onto branchA branchB would that put the entire master branch on the head of branchA?Lustreware
shouldn't this be checkout branchB: rebase --onto master branchA?Phiphenomenon
Why has this been upvoted? this does not do what it says it does.Crest
I edited and fixed the answer, so people don't have to first break their repo branches and just after that come and read the comments... 🙄Fake
S
2

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?"

https://static.mcmap.net/file/mcmap/ZG-AbGLDKwfxbFhpangQa7lAZFljKVIlW7MAbw2jaRA/~mark/git-diff-help.png

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.

Sarver answered 7/10, 2019 at 21:11 Comment(0)
V
1

Even though I'd say I have a solid understanding of git and the rebase command (sans --onto), I read the majority of these answers and still couldn't figure out what it did. IMHO, the git book explains it best—as long as you start reading at the right spot, because the language before it can be overwhelming. I'll inline the area of the git book that clarified --onto for me.


Here is how you would transplant a topic branch based on one branch to another, to pretend that you forked the topic branch from the latter branch, using rebase --onto.

First let’s assume your topic is based on branch next. For example, a feature developed in topic depends on some functionality which is found in next.

o---o---o---o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

We want to make topic forked from branch master; for example, because the functionality on which topic depends was merged into the more stable master branch. We want our tree to look like this:

o---o---o---o---o  master
    |            \
    |             o'--o'--o'  topic
     \
      o---o---o---o---o  next

We can get this using the following command:

git rebase --onto master next topic

Another 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

This is useful when topicB does not depend on topicA.

A range of commits could also be removed with rebase. If we have the following situation:

E---F---G---H---I---J  topicA

then the command

git rebase --onto topicA~5 topicA~3 topicA

would result in the removal of commits F and G:

E---H'---I'---J'  topicA

This is useful if F and G were flawed in some way, or should not be part of topicA. Note that the argument to --onto and the <upstream> parameter can be any valid commit-ish.

Vacillate answered 14/3 at 0:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.