git-rebase如何识别“别名”提交?

7

我试图更好地理解git-rebase的魔力。今天我惊喜地发现了以下行为,这是我没有预料到的。

简而言之:我对一个共享分支进行了rebase,导致所有提交的sha1值都发生了变化。尽管如此,一个派生分支仍然能够准确地识别出其原始提交被“别名”为具有不同sha1的新提交。rebase一点问题也没有产生。

细节

以主分支M1为例:

从主分支中分离出分支X,添加一些额外的提交:M1-A1-B1-C1。记录下git-log输出。

从分支X中分离出分支Y,添加一个额外的提交:M1-A1-B1-C1-D1。记录下git-log输出。

在主分支的末尾添加一个新提交:M1-M2

将分支X rebase到更新后的主分支上:M1-M2-A2-B2-C2。请注意,A2-B2-C2都具有与A1-B1-C1相同的消息、内容和作者日期。但是,它们具有完全不同的sha1值,以及提交日期。根据这篇文章,SHA1不同的原因是提交的父项已更改。

将分支Y rebase到更新后的分支X上。结果:M1-M2-A2-B2-C2-D2

值得注意的是,只有D1提交被应用(并成为D2)。在分支Y中的A1-B1-C1提交完全被git-rebase忽略了。您可以在输出日志中看到这一点。

这很棒,但是git-rebase如何知道要忽略A1-B1-C1?git-rebase如何知道A2-B2-C2与A1-B1-C1相同,因此可以安全地忽略它们?我一直认为git使用sha1标识符来跟踪提交,但尽管上述提交具有不同的sha1,git仍然以某种方式知道它们彼此链接。它是如何做到的?鉴于上述行为,在共享分支上rebase何时真正危险?

3个回答

13

git rebase会在内部列出应该进行变基的提交,并为这些提交计算一个补丁ID。与提交ID不同的是,它只散列补丁的内容,而不是树和提交对象的内容。所以,尽管A1和A2具有不同的标识符,它们具有相同的补丁ID。然后,git rebase会跳过补丁ID已经存在的补丁。

要获取更多信息,请在此处搜索patch-id: https://git-scm.com/book/en/v2/Git-Branching-Rebasing


上述相关部分(图表缺失):

如果你的团队中的某个人强制推送了覆盖了你所依赖工作的更改,你需要找出哪些是你的工作,哪些是他们重写的。

Git除了提交SHA-1校验和之外,还计算了一个仅基于提交引入的补丁的校验和。这被称为“补丁ID”。

如果你拉取了被重写的工作,并在合作伙伴的新提交之上对其进行了变基,Git通常可以成功地找出哪些是你独有的,并将它们应用于新分支的顶部。

例如,在前面的场景中,如果我们不是运行 git merge,而是当我们处于Someone pushes rebased commits, abandoning commits you’ve based your work on时运行git rebase teamone/master,Git会:

  • 确定哪些工作是我们分支独有的(C2、C3、C4、C6、C7)
  • 确定哪些不是合并提交(C2、C3、C4)
  • 确定哪些提交还没有合并到目标分支(只有 C2 和 C3,因为C4和C4'是相同的补丁)
  • 将这些提交应用到 teamone/master 分支的顶部

这仅适用于您的伙伴制作的 C4 和 C4' 几乎完全相同的补丁。否则,rebase 将无法识别它是重复的,并会添加另一个类似于 C4 的补丁(由于更改已经存在,所以可能无法干净地应用)。


5
事实上,git rebase 使用了几种不同的方法来消除冗余的副本。

Patch-ID

第一种,也是最安全的方法,是通过与 git cherry 用于识别樱桃挑选提交的相同方法。然而,如果您阅读链接的文档,关于 如何工作 的唯一线索在于文档末尾,那里链接到了 git patch-id 文档
阅读此第二个手册页将让您对如何建立“提交等效性”有一个很好的了解:Git 只是在任何普通(非合并)提交的输出上计算一个 git patch-id。实际上,它运行的是 git diff-tree 而不是面向用户的 git show,但效果大致相同。
但是仍然有些东西缺失,而且在git rebasegit cherry中都没有很好的文档记录。它在git rev-list中有些更好的文档记录,但这是一个相当令人生畏的手册页面。有两个关键点:使用三个点语法表示的对称差异的概念,在gitrevisions documentation中描述,以及git rev-list--left-right--cherry-mark选项。
一旦您理解了我们如何获取一个DAGlet,例如:
...--o--o--L1--L2--L3   <-- left
         \
          R1--R2--R3   <-- right

当使用left...right选择三个LR提交时,--left-right选项本身就有很多意义:它标记文本输出中哪些提交来自三个点的左侧,哪些来自右侧。第二步是发现git rev-list可以为每个“侧面”的每个提交计算补丁ID。Git然后可以将所有左侧补丁ID与所有右侧补丁ID进行比较。--cherry-mark选项及其相关选项使用这些标记等效或不等效的提交,或省略等效提交。这个谜题的最后一部分是git rebase没有像文档所说的那样使用<upstream>..HEAD。相反,它使用等效于git rev-list --cherry-pick --right-only --no-merges <upstream>...HEAD来获取要复制的提交集。(我们还必须添加--topo-order--reverse这些选项。)

分叉点

第二种用于省略提交的方法是git rebase使用的--fork-point机制,该机制现在已内置于git merge-base中。这种机制特别难以描述,并且还依赖于reflog条目,以了解过去曾经在分支上但现在不再存在的提交。有时它会给出不良结果,并且在这种特定的rebase中没有用处。
我主要在这里提到它,因为寻找git rebase省略某些提交原因的人可能遇到过fork-point机制失效的情况。例如,请参见:

1

第二次变基后,分支Y的提交记录为空

实际上并没有什么神奇的隐藏在里面。变基会寻找共同的历史并忽略它(在这种情况下只有提交M1)。将历史从变基的分支(Y)中分离出来,并尝试在新的基础(分支X)上进行拾取。

拾取方法从之前和已拾取的提交派生出一个补丁。由于A1、B1和C1是空的,它会简单地跳过这些提交。只有D1被拾取,因此创建了一个D2(其父链接的头部更改中具有新的SHA,正如问题中所正确说明的那样)。


1
顺便提一下,共享分支的变基很不愉快,因为之后需要强制推送。远程和其他用户的本地历史记录会发生分歧,如果他们不够小心,他们的新工作可能会丢失。所以,在共享分支上永远不要强制推送 :) - petrpulc
1
本质上这意味着:Git 发现来自 A1 的更改已经以 A2 的形式存在于更新的 branch-X 上,因此它只是将 A1 作为冗余内容丢弃了。 - Eevee
@Eevee您是在暗示因为A1引入的更改已经匹配了branch-X的最新状态,所以提交结果被丢弃了吗?我想这可能可以解释我所看到的行为,尽管我需要再次检查。如果这个假设是正确的,那么如果A1和B1都触及相同的代码行,我们应该会看到许多冲突和多余的提交? - RvPr
@petrpulc 感谢您的澄清。我之前不了解在rebase中如何生成/使用补丁。一旦我明白了这一点,我所提出的问题的行为现在就有意义了。 - RvPr
1
虽然提交记录会变为空(然后被跳过,除非你添加了 --keep-empty),但实际上 git rebase 在开始之前就已经将它们从列表中剥离了。对于非交互式和交互式的 rebase,具体机制是不同的(非交互式默认使用 git format-patch 而不是 git cherry-pick),但两种类型的 rebase 都使用复杂的 git rev-list 命令来省略等效补丁提交记录。(如果你确实添加了 --keep-empty,则非交互式的 rebase 将被强制使用 cherry-pick,因为 format-patch 不会生成空补丁!) - torek
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接