Why is “rebase --onto ABC” different than “rebase

2019-01-26 02:15发布

Using git 2.11, git rebase documentation says:

The current branch is reset to <upstream>, or <newbase> if the --onto option was supplied. This has the exact same effect as git reset --hard (or ). ORIG_HEAD is set to point at the tip of the branch before the reset.

I understand it as if upstream and newbase points to the same "base reference", then this means the two rebase syntaxes below are equivalent:

git rebase ABC
git rebase --onto ABC

Here is the demo I setup. Let's assume the current branch is FeatureABC it is perfectly in sync with the remote branch.

#---create two identical branches, behind current branch by 5 commits
(FeatureABC) git branch Demo1-Rebase-ABC      HEAD~4
(FeatureABC) git branch Demo2-Rebase-onto-ABC HEAD~4

#---Make a new commit in branch Demo1
git checkout Demo1-Rebase-ABC
echo "Demo of: git rebase FeatureABC Demo1-Rebase-ABC" > ./Demo1_BogusFile.txt
git add ./Demo1_BogusFile.txt
git commit -m "Create file Demo1_BogusFile.txt"

git rebase FeatureABC

First, rewinding head to replay your work on top of it
... Applying: Create file Demo1_BogusFile.txt

git log --oneline -3 shows that the branch Demo1-Rebase-ABC is sync'ed with the HEAD of FeatureABC. And the commit "Create file Demo1_BogusFile.txt" was correctly applied on top of it.

#---Make a new commit in branch Demo2
git checkout Demo2-Rebase-onto-ABC
echo "Demo of: git rebase --onto FeatureABC Demo2-Rebase-onto-ABC" > ./Demo2_Onto_BogusFile.txt
git add ./Demo2_Onto_BogusFile.txt
git commit -m "Create file Demo2_Onto_BogusFile.txt"

git rebase --onto FeatureABC

There is no tracking information for the current branch. Please specify which branch you want to rebase against. See git-rebase(1) for details.

git rebase <branch>

If you wish to set tracking information for this branch you can do so with:

git branch --set-upstream-to=origin/<branch> Demo2-Rebase-onto-ABC

I misunderstood the warning message. Thinking that git was confused in the defaults when --onto is used. So I just want to "help" by telling git the current branch I want to rebase

git rebase --onto FeatureABC Demo2-Rebase-onto-ABC

First, rewinding head to replay your work on top of it...

git log --oneline -3 shows that the branch Demo2-Rebase-onto-ABC becomes identical than FeatureABC. The last commit "Create file Demo2_Onto_BogusFile.txt" has vanished and the file ./Demo2_Onto_BogusFile.txt is deleted.

Question: what is the reason the git rebase --onto FeatureABC Demo2-Rebase-onto-ABC didn't apply the new commits made on the Demo2-Rebase-onto-ABC branch?

1条回答
来,给爷笑一个
2楼-- · 2019-01-26 02:44

They are not the same, and this can get complicated by the --fork-point option as well. I think this may be what bit you, although it's not possible to be sure, just from what you have described, as one of the steps you outlined merely produces an error.

I start with a reasonable guess, but it is a guess

To see what's really happening it is very helpful to draw (part of) the commit graph, with special attention to labeling since you are using multiple names that all point to a single commit.

Let's assume the current branch is FeatureABC it is perfectly in sync with the remote branch.

Hence we have something like this—but something like is not really good enough; you have the repository, so you should draw the graph; I have to guess:

...--o--A--B--C--D--E   <-- FeatureABC (HEAD), origin/FeatureABC

Now you run:

#---create two identical branches, behind current branch by 5 commits
(FeatureABC) git branch Demo1-Rebase-ABC      HEAD~4
(FeatureABC) git branch Demo2-Rebase-onto-ABC HEAD~4

Since HEAD~4 names commit A (HEAD~1 is D, HEAD~2 is C, and so on), we need to do something to mark the fact that these two new names point to commit A. I'm going to shorten the names to just Demo1 and Demo2 though. (I've created a repository with only commits o through E at this point, and actually run git branch Demo1 HEAD~4; git branch Demo2 HEAD~4 here.)

...--o--A              <-- Demo1, Demo2
         \
          B--C--D--E   <-- FeatureABC (HEAD), origin/FeatureABC

Incidentally, git log --all --decorate --oneline --graph ("get help from A DOG" as someone put it) shows this test repository this way (there is no origin/ branch in my case):

* c4a0671 (HEAD -> master) E
* a7b8ae4 D
* 3deea72 C
* b11828d B
* ffc29b5 (Demo2, Demo1) A
* 3309a8d initial

Next, you check out Demo1, moving HEAD:

git checkout Demo1-Rebase-ABC
...--o--A              <-- Demo1 (HEAD), Demo2
         \
          B--C--D--E   <-- FeatureABC, origin/FeatureABC

and modify the work-tree, add the modified file to the index, and commit, to make a new commit which I will call F, which updates the HEAD branch and therefore separates Demo1 and Demo2. I'll now use my own commands and their output here:

$ git checkout Demo1
Switched to branch 'Demo1'
$ echo demo1 > demo1.txt && git add demo1.txt && git commit -m F
[Demo1 89773b6] F
 1 file changed, 1 insertion(+)
 create mode 100644 demo1.txt

Drawing the graph gets a bit harder; I'll use a row up:

          F            <-- Demo1 (HEAD)
         /
...--o--A              <-- Demo2
         \
          B--C--D--E   <-- FeatureABC, origin/FeatureABC

Now we get to your first git rebase command. I have to use master, of course:

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: F

This works on the current branch (HEAD or Demo1). It finds commits that are on HEAD that are not on FeatureABC (FeatureABC.. in gitrevisions syntax). That's commit F. These commits get put into a list of commits to maybe copy—git rebase will check for commits with the same git patch-id and skip them, although clearly that did not happen here. So now commit F is copied to new commit F', with different hash ID and different base:

          F              [abandoned]
         /
...--o--A                <-- Demo2
         \
          B--C--D--E     <-- FeatureABC, origin/FeatureABC
                    \
                     F'  <-- Demo1 (HEAD)

(Here's the actual git log output, showing the new commit hash for the copy. The original, now-abandoned F is not shown unless I add Demo1@{1} to the command, which I did here. The original is the second F shown, i.e., the earlier commit:

$ git log --all --decorate --oneline --graph Demo1@{1}
* c1d0896 (HEAD -> Demo1) F
* c4a0671 (master) E
* a7b8ae4 D
* 3deea72 C
* b11828d B
| * 89773b6 F
|/  
* ffc29b5 (Demo2) A
* 3309a8d initial

I like the horizontal graph better, but this one has more information, specifically the abbreviated hash IDs.)

Reproducer fails, and I'll have to guess again

Now we try to repeat this with Demo2, but it fails. Here are my actual commands, cut-and-pasted. The first step works fine:

$ git checkout Demo2
Switched to branch 'Demo2'
$ echo demo2 > demo2.txt && git add demo2.txt && git commit -m G
[Demo2 ae30665] G
 1 file changed, 1 insertion(+)
 create mode 100644 demo2.txt

No longer drawing the original F, here is the new graph. I put G where F used to be, although I could draw this as just ...--o--A--G:

          G              <-- Demo2 (HEAD)
         /
...--o--A
         \
          B--C--D--E     <-- FeatureABC, origin/FeatureABC
                    \
                     F   <-- Demo1

The rebase, however, does not work. Again I have to use master instead of FeatureABC, but this would behave the same way in your example, given that the git branch command did not set an upstream ("tracking") name:

$ git rebase --onto master
There is no tracking information for the current branch.
Please specify which branch you want to rebase against.
See git-rebase(1) for details.

    git rebase <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=<remote>/<branch> Demo2

The reason git rebase failed with this error message is that --onto has absorbed the argument as <newtarget>, leaving us with no <upstream>:

If <upstream> is not specified, the upstream configured in branch.<name>.remote and branch.<name>.merge options will be used (see git-config(1) for details) and the --fork-point option is assumed. If you are currently not on any branch or if the current branch does not have a configured upstream, the rebase will abort.

The boldface here is mine, but it's also, I think, the key. I assume you ran a git rebase --onto <somename> that did not fail. For it to have not-failed, your branch must have had an upstream set. That upstream probably was origin/FeatureABC or similar, and that meant that as far as Git was concerned, you were running:

git rebase --onto FeatureABC --fork-point origin/FeatureABC

and not:

git rebase --onto FeatureABC --no-fork-point origin/FeatureABC

Some further reading in the (overly cryptic, in my opinion) git rebase documentation will turn up this sentence:

If either <upstream> or --root is given on the command line, then the default is --no-fork-point, otherwise the default is --fork-point.

In other words:

git rebase FeatureABC

turns off the --fork-point option, as does:

git rebase --onto FeatureABC FeatureABC

but:

git rebase

or:

git rebase --onto FeatureABC

leaves the --fork-point option on.

What --fork-point is about

The goal of --fork-point is to specifically drop commits that used to be, at one time, in your upstream, but are no longer in your upstream. See Git rebase - commit select in fork-point mode for an example. The specific mechanism is complicated and relies on the upstream branch's reflog. Since I don't have either your repository or your reflog, I cannot test out your specific case—but that's one reason, and probably the most likely reason given the hints in your question, that a commit that would affect the rebase tree result would get dropped. The commits that are dropped due to having the same patch ID as an upstream commit are ones that [edit:] often1 will not affect the final tree of the last-copied commit: they would just cause merge conflicts and/or force you to use git rebase --skip to skip over them, if they were included.


1It occurred to me after writing this that there is an important exception (which probably has nothing to do with the original question, but which I should mention). Rebasing a feature or topic branch onto a more mainline branch, when a commit was first cherry-picked out of the feature into the mainline, and then reverted in the mainline, will cause a problem. Consider, e.g.:

...--o--*--P--Q--C'-R--S--X--T   <-- mainline
         \
          A--B--C--D--E          <-- topic

where C' is a copy of commit C, and X is a revert of commit C that should not have been put into mainline yet. Doing:

git checkout topic
git rebase mainline

will instruct Git to put commits A through E into the "candidates to copy" list, but also look at P through T to see if any were already adopted. Commit C was adopted, as C'. If C and C' have the same patch ID—usually, they will—Git will drop C from the list as "already copied". However, C was explicitly reverted in commit X.

Whoever does the rebase needs to notice, and carefully restore C if it is required and appropriate.

This particular behavior is not a problem with git merge (since merge ignores intermediate commits), only with git rebase.

查看更多
登录 后发表回答