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

前端 未结 1 1177
清歌不尽
清歌不尽 2020-12-18 14:09

Using git 2.11, git rebase documentation says:

The current branch is reset to , or if the --onto option was supplied

相关标签:
1条回答
  • 2020-12-18 14:31

    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.

    0 讨论(0)
提交回复
热议问题