git rebase 奇怪的行为

git rebase strange behavior

我对下一个示例有奇怪的 git 变基行为。 假设,我有本地树:

                     ∨
           b1--b2--b3                      
          /
a1--a2--a3

和远程树

            b1--b2--b3
          /
a1--a2--a3--a4--a5--a6

其中分支a是master

目前,我在 b3。 我调用下一个命令:

git pull origin master master // fixed typo
git rebase master

在这个动作之后我得到了树,它看起来像:

          a4'--a5'--a6'--b1--b2--b3                
         /
a1--a2--a3--a4--a5--a6

改为

                       b1--b2--b3
                      / 
a1--a2--a3--a4--a5--a6

为什么会发生?

我总是建议人们在非常熟悉 Git 之前避免使用 git pull,因为它做的太多了,以一种有点神秘的方式,很难解释。它通常(并且相当准确地)被描述为等同于 运行ning git fetch 后跟 git mergegit rebase,但棘手的部分是它传递给 git mergegit rebase 不是您所期望的。

话虽如此,让我们分解这些并仔细查看每个步骤。我也会以不同的方式绘制您的提交图。 (你在上面称它为 "tree",这不是最好的术语,原因有二。首先,提交图是 graph,特别是 Directed Acyclic Graph or DAG.所有的树都是DAG,但并不是所有的DAG都是树。其次,Git 使用术语 tree 来指代 "tree objects";每个提交都带有一个树对象。)

我相信您的初始提交图看起来像这样:

           B1--B2--B3   <-- branch (HEAD)
          /
A1--A2--A3   <-- master, origin/master

请注意,您的本地 b运行ch-name master 指向提交 A3。您的远程跟踪 b运行ch 名称 origin/master 也指向提交 A3。但是,您当前的 b运行ch 名为 branch(我必须发明这个名称)并且它指向提交 B3.

你现在 运行 git pull origin master master,或者 git pull origin mastert master(你使用其中的哪一个很重要,你的评论与你的问题相矛盾)。此 git pull 命令 运行s git fetch 带有多个参数。

git fetch步骤获得新提交

每当你 运行 git fetch,包括当你使用 git pull 到 运行 git fetch 时,你的 Git 会调用另一个 Git,可能位于存储在您的远程名称 origin 下的 URL。在这里——作为 git pul origin master mastergit pull origin mastert master 的 运行——另一个 Git 已经提交了几个新的提交。您的 Git 将这些新提交存储在您的存储库中,如果您的 Git 至少是 1.8.2 版,您的 Git 会更新您的 origin/master(也许您的 origin/mastert) 指向这些新提交。

您的 Git 还在特殊文件 FETCH_HEAD 中存储了新提交的 ID 和从偏僻的。因此我们可以将其添加到图形绘制中:

           B1--B2--B3   <-- branch (HEAD)
          /
A1--A2--A3   <-- master
          \
           A4--A5--A6   <-- origin/master, FETCH_HEAD

FETCH_HEAD 中的内容有点棘手。此文件的内容包括原始名称,即 master,也许还有 mastert,如 other Git 中所示,没有任何origin/ 重命名您的 Git 通常使用的名称。标签也可能有一些行。其中一些行也可能用 not-for-merge 注释。所有行都包含特定 Git 对象(提交和有时标记)的哈希 ID。 git fetch 程序专门留下这些行供 git pull 程序检查。

接下来,您的 git pull 命令从 FETCH_HEAD 中提取新获得的提交的哈希 ID。也就是说,它通过 FETCH_HEAD 搜索 而非 标记为 not-for-merge 的提交的哈希 ID。 (我在这里假设您正在执行合并样式拉取而不是变基样式拉取。)然后 运行s git merge 使用这些哈希 ID。

git merge 步骤进行合并提交

同样,现在重要的是 运行 git pull origin master master 还是 git pull origin mastert master。如果你 运行 后者,并且有一个名为 mastert 的 b运行ch,将会有 两个 匹配行,并且可能有两个不同的哈希 ID .在这种情况下,您的 git pull 命令将对这两个哈希值执行 octopus merge

如果没有,您将得到一个散列的正常合并。我假设你得到了正常的合并。结果是这样的:

           B1--B2--B3------M   <-- branch (HEAD)
          /               /
A1--A2--A3   <-- master  /
          \             /
           A4--A5-----A6   <-- origin/master, FETCH_HEAD

请注意,您的 b运行ch 名称 master 仍然指向提交 A3,而不是提交 A6.

你现在运行git rebase master.

Rebase 剥离合并

git rebase 的作用是 复制 提交。它复制的提交是您指定的提交。新副本的目的地也是您指定的。

当你运行:

git rebase master

你告诉 Git 复制你的 current b运行ch(即 branch)上的任何提交not 无法从名为 master 的 b运行ch 访问,并且这不是合并提交(您将从 [=162= 复制可访问的提交 ] 合并,但不是合并本身)。请注意,这不是使用 origin/master,而是 master。因此,您要求 Git 复制的提交是 B1B2B3,以及 A4A5A6 ...以某种顺序(实际顺序有点难以预测,尽管 A 和 B 组将被一起复制,因为 Git 在收集要复制的提交 ID 时使用 --topo-order)。

您要求 Git 复制它们的位置是 "after the tip of master",即在 A3.

之后

如果Git首先复制B1,它通常会保留B1不变,因为git rebase会尽可能这样做。如果它首先复制 A4,它也可以尝试保持 A4 不变,原因相同;但是执行此操作的机制被这个特定的变基所破坏(见下面的评论)。一旦 Git 通过 A6 复制了 A4,它 必须 B1 复制到父级为 [=86= 的新提交] A6 的副本(此提交与原始 B1 不同,因为 B1 的父级是 A3)。

(但是,无论如何,您都可以强制 git rebase 进行复制,例如使用 --no-ff--force。)

您的结果表明您的 Git 选择先复制 A4。这给你:

           A4'-A5'-A6'-B1'-B2'-B3'  <-- branch
          /
A1--A2--A3   <-- master
          \
           A4--A5--A6   <-- origin/master

没有 --force--no-ff,如果 git rebase 稍微聪明一点,它可以(但不会)产生:

A1--A2--A3   <-- master
          \
           A4--A5--A6   <-- origin/master
                     \
                      B1'-B2'-B3'  <-- branch (HEAD)

或(可以而且有时会)产生:

           B1--B2--B3--A4'-A5'-A6'   <-- branch (HEAD)
          /
A1--A2--A3   <-- master
          \
           A4--A5--A6   <-- origin/master