'rebase master'和'rebase --onto master'之间的区别来自从master的分支派生的分支

pun*_*lly 33 git git-rebase

给定以下分支结构:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)
Run Code Online (Sandbox Code Playgroud)

如果我想合并我的B更改(并且只有我的B更改,没有A更改)到master,这两组命令之间有什么区别?

>(B)      git rebase master
>(B)      git checkout master
>(master) git merge B
Run Code Online (Sandbox Code Playgroud)
>(B)      git rebase --onto master A B
>(B)      git checkout master
>(master) git merge B
Run Code Online (Sandbox Code Playgroud)

如果我使用第一种方式,我主要感兴趣的是如果来自分支A的代码可以使它成为主.

tor*_*rek 102

在我回答问题之前,请耐心等待一段时间.一个较早的答案是正确的,但有标签和其他相对较小(但可能令人困惑)的问题,所以我想从分支图纸和分支标签开始.此外,来自其他系统的人,或者甚至可能只是修改控制和git的新手,通常认为分支是"发展线"而不是"历史痕迹"(git将它们实现为后者,而不是前者,所以提交不一定是任何特定的"开发线").

首先,绘制图表的方式存在一个小问题:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)
Run Code Online (Sandbox Code Playgroud)

这是完全相同的图形,但标签以不同方式绘制,并添加了一些更多的箭头(我在下面使用了提交节点编号):

0 <- 1 <- 2         <-------------------- master
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- HEAD=B
Run Code Online (Sandbox Code Playgroud)

为什么这很重要的是git对于提交"在某个分支上"的含义是相当松散的 - 或者更好的一句话就是说某些提交"包含在"某些分支中.提交不能移动或更改,但分支标签可以并且确实移动.

更具体地说,分支master,AB指向一个具体的承诺.在这种情况下,master指向提交2,A指向提交6,B指向提交9.前几个提交0到2包含在所有三个分支中; 提交3,图4和5都包含在这两个AB; commit 6仅包含在A; 并且提交7到9仅包含在B.(顺便提一下,多个名称可以指向相同的提交,这在您创建新分支时是正常的.)

在我们继续之前,让我再一次重新绘制图形:

0
 \
  1
   \
    2     <-- master
     \
      3 - 4 - 5
              |\
              | 6   <-- A
               \
                7
                 \
                  8
                   \
                    9   <-- HEAD=B       
Run Code Online (Sandbox Code Playgroud)

这只是强调它不是一个重要的提交水平线,而是父/子关系.分支标签指向一个开始提交,然后(至少绘制这些图形的方式)我们向左移动,也可能根据需要向上或向下移动,以找到父提交.


重新提交提交时,实际上是在复制这些提交.

Git永远不会改变任何提交

对于任何提交(或者实际上是git存储库中的任何对象),都有一个"真实的名称",即它的SHA-1:就像9f317ce...你在git log中看到的40-hex-digit字符串一样.SHA-1是对象内容的加密1校验和.内容是作者和提交者(姓名和电子邮件),时间戳,源树和父提交列表.提交#7的父代始终是#5.如果您制作了提交#7的大致精确副本,但将其父级设置为提交#2而不是提交#5,则会获得具有不同ID的不同提交.(此时我已经用完了一位数 - 通常我使用单个大写字母来表示提交ID,但是命名为分支A,B我认为这会让人感到困惑.所以我会调用#7,#7a的副本,下面.)

是什么git rebase

当你要求git重新提交一系列提交时 - 例如上面的提交#7-8-9-它必须复制它们,至少如果它们要移动到任何地方(如果它们不移动它可以只是离开原件到位).它默认从当前签出的分支复制提交,因此git rebase只需要两条额外的信息:

  • 应该复制哪些提交?
  • 副本应该在哪里登陆?也就是说,首次复制提交的目标父ID是什么?(其他提交只是指向第一次复制,第二次复制,依此类推.)

当你运行时git rebase <upstream>,你让git从一条信息中找出这两个部分.当你使用--onto,你分别告诉git的关于这两个部分:你还是提供一个upstream,但它不计算目标距离<upstream>,只计算提交复制<upstream>.(顺便说一句,我认为<upstream>这不是一个好名字,但它是rebase使用的东西,我没有更好的方法,所以让我们坚持下去.Rebase调用目标 <newbase>,但我认为目标是一个更好的名字.)

我们先来看看这两个选项.两者都假设你B首先在分支机构:

  1. git rebase master
  2. git rebase --onto master A

使用第一个命令,<upstream>参数rebasemaster.第二个是它A.

下面是其承诺的git计算到复制:它交给当前分支git rev-list,也手<upstream>git rev-list,但使用--not-或者更确切地说,与两个点相当于exclude..include符号.这意味着我们需要知道它是如何git rev-list工作的.

虽然git rev-list非常复杂 - 大多数git命令最终都使用它; 它的发动机git log,git bisect,rebase,filter-branch,等等,这种特殊情况下是不太难:用双点符号,rev-list列出每一个承诺从右侧到达(包括承诺本身),但不包括每一个承诺到达距离左手边.

在这种情况下,git rev-list HEAD发现所有提交都可以从HEAD-that,几乎所有提交:提交0-5和7-9-并git rev-list master找到所有提交的提交master,即提交#s 0,1和2.减去0-through- 2从0-5,7-9叶3-5,7-9.这些是要复制的候选提交,如下所列git rev-list master..HEAD.

对于我们的第二个命令,我们A..HEAD而不是master..HEAD,所以减去的提交是0-6.提交#6没有出现在HEAD集合中,但是没关系:减去不在那里的东西,不在那里.因此,所得到的候选人复制品是7-9.

这仍然让我们弄清楚了rebase 的目标,即复制提交应该在哪里登陆?使用第二个命令,答案是"由--onto参数标识的提交".因为我们说过--onto master,这意味着目标是提交#2.

rebase#1

git rebase master

但是,使用第一个命令,我们没有直接指定目标,因此git使用由标识的提交<upstream>.在<upstream>我们都给了master,这点提交#2,所以目标是提交#2.

因此,第一个命令将从复制commit#3开始,只需要进行任何最小的更改,以使其父级为#2.它的父母已经提交#2.没有什么必须改变,所以没有任何改变,并且rebase只是重新使用现有的#3提交.然后必须复制#4以使其父级为#3,但父级已经是#3,所以它只是重新使用#4.同样,#5已经很好了.它完全忽略#6(不在复制的提交集中); 它会检查#s 7-9,但它们都很好,所以整个rebase最终只是重新使用所有原始提交.你可以强制复制-f,但你没有,所以这整个rebase最终什么都不做.

rebase#2

git rebase --onto master A

第二个rebase命令用于--onto选择#2作为其目标,但告诉git只复制提交7-9.提交#7的父级是提交#5,所以这个副本确实需要做一些事情.2 所以git创建了一个新的commit-let调用#7a,它将#2作为其父项.rebase继续提交#8:副本现在需要#7a作为其父级.最后,rebase继续提交#9,需要#8a作为其父级.复制完所有提交后,rebase最后做的就是移动标签(记住,标签移动和更改!).这给出了如下图:

          7a - 8a - 9a       <-- HEAD=B
         /
0 - 1 - 2                    <-- master
         \
          3 - 4 - 5 - 6      <-- A
                    \
                     7 - 8 - 9   [abandoned]
Run Code Online (Sandbox Code Playgroud)

好的,但是怎么样git rebase --onto master A B

这跟几乎一样git rebase --onto master A.不同之处在于最后的额外费用B.幸运的是,这种差异非常简单:如果你给出git rebase一个额外的参数,它git checkout首先在该参数上运行.3

你的原始命令

在第一组命令中,您git rebase master在分支上运行B.如上所述,这是一个很大的无操作:因为没有什么需要移动,git根本没有复制(除非你使用-f/ --force,你没有).然后你检查master并使用了git merge B,如果它被告知4 -用合并创建一个新的提交.因此Dherik的回答,至少在我看到它的时候,在这里是正确的:合并提交有两个父,其中一个是分支的尖端B,并且该分支通过三个提交到达分支A,因此一些最后A融入的是什么master.

使用你的第二个命令序列,你首先检查出来B(你已经开启了,B所以这是多余的,但却是其中的一部分git rebase).然后你使用rebase复制三次提交,生成上面的最终图,提交7a,8a和9a.然后签出master并进行合并提交B(再次参见脚注4).再次Dherik的回答是正确的:唯一缺少的是原始的,被抛弃的提交没有被引入,并且新的合并提交不是明显的.


1这只是因为定位特定校验和非常困难.也就是说,如果信任的人告诉你"我信任ID 1234567的提交......",对于其他人 - 你可能不信任的人 - 几乎不可能提出具有相同ID的提交,但是有不同的内容.偶然发生这种情况的可能性是1比2 160,这比你在被海啸袭击时被海啸淹没时受到闪电击中而心脏病发作的可能性要小得多.:-)

2实际的副本使用同等品git cherry-pick:混帐比较提交的与其父的树来获得一个diff树,然后应用差异为新的父的树.

3实际上,此时字面上是正确的:git rebase是一个解析您的选项的shell脚本,然后决定运行哪种内部rebase:非交互式git-rebase--am或交互式git-rebase--interactive.在找出所有参数之后,如果有一个剩余的分支名称参数,脚本git checkout <branch-name>在启动内部rebase之前就会执行.

4由于master提交2和提交2的点是提交9的祖先,因此通常不会进行合并提交,而是执行Git调用的快进操作.你可以指示Git不要使用快进这些git merge --no-ff.一些接口,比如GitHub的Web界面,也许有些图形用户界面,可以分离各种不同的操作,从而使他们的"合并"强制真正合并这样的.

通过快速合并,第一种情况的最终图表是:

0 <- 1 <- 2         [master used to be here]
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- master, HEAD=B
Run Code Online (Sandbox Code Playgroud)

在任何一种情况下,提交1到9现在都在两个分支上,master 并且 B.与真正的合并相比,差异在于,从图表中,您可以看到包含合并的历史记录.

换句话说,快进合并的优点是它不会留下什么是微不足道的操作的痕迹.快进合并的缺点是它没有留下任何痕迹.因此,是否允许快进的问题实际上是一个问题,即是否在提交形成的历史中留下明确的合并.

  • 我花了很多时间查看您难以置信的详细说明,并学到了很多东西。谢谢! (3认同)

Joh*_*orn 17

在任何给定的操作之前,您的存储库看起来像这样

           o---o---o---o---o  master
                \
                 x---x---x---x---x  A
                                  \
                                   o---o---o  B
Run Code Online (Sandbox Code Playgroud)

在标准rebase(没有--onto master)之后,结构将是:

           o---o---o---o---o  master
               |            \
               |             x'--x'--x'--x'--x'--o'--o'--o'  B
                \
                 x---x---x---x---x  A
Run Code Online (Sandbox Code Playgroud)

...... x'来自A分支机构的提交.(注意它们现在如何在分支的基础上重复B.)

相反,rebase with --onto master将创建以下更简洁的结构:

           o---o---o---o---o  master
               |            \
               |             o'--o'--o'  B
                \
                 x---x---x---x---x  A
Run Code Online (Sandbox Code Playgroud)

  • 到目前为止,这是对拜占庭差异的最明确,最简单的解释,尽管有上述评论. (2认同)

Dhe*_*rik 11

差异:

第一集

没啥事儿.master自创建分支以来,分支中没有新的提交B.

第二集

我想将我的B更改(并且只有我的B更改,没有A更改)合并到master中

小心你对"只有我的B改变"的理解.

在第一组中,B分支是(在最终合并之前):

 *---*---*
          \
           *---*---*
                    \
                     *---*---* [B]
Run Code Online (Sandbox Code Playgroud)

在第二组中你的B分支是:

*---*---*
        |
        |
        |
        *---*---* [B]
Run Code Online (Sandbox Code Playgroud)

如果我理解正确,你想要的只是不在A分支中的B提交.因此,第二组是合并前的正确选择.