I can't understand the behaviour of git rebase

2019-01-07 01:46发布

问题:

I have noticed that the two blocks of following git commands have different behaviours and I don't understand why.

I have a A and a B branch that diverge with one commit

---BRANCH A-------COMMIT1-----
\--BRANCH B--

I want to rebase B branch on the lastest A (and have the commit1 on the B branch)

---BRANCH A-------COMMIT1-----
                          \--BRANCH 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 use the second syntax, and so seems to me completely broken, that's why I ask for this syntax issue.

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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 master
rebase --onto branchA branchB

you will get following commit hierarchy.

      o'---o' (branchB)
     /
o---o (master)
     \
      o---o---o---o (branchA)


回答5:

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 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. 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 but it can be any 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~

Will rebase single commit pointed by HEAD.

Avoid explicit checkouts.

The default HEAD or current_branch are contextually taken from palce you're in. This is why most people checkout to branch which they want to rebase. But when argument is given explicitly you don't have to checkout before rebase to pass it in implicit way.

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.