合并提交的共同祖先位于 Git 中(当存在交叉合并时)?

Where does the common ancestor of merged commits reside in Git (when there's criss-cross merge)?

我是如何从@torek 的彻底回答(例如,)中得知的,当 Git 在两个分支(提交)之间进行合并并且一些冲突阻碍了它时,有三个索引中的非空“插槽”:第一个存储每个文件的通用(基本)版本,第二个存储本地(我们的)版本,第三个存储远程(他们的)版本。

设想起来非常简单,因为所有这些版本实际上都作为 blob 对象存储在 objects 目录中。所以 Git 可以从这些 blob 对象中提取需要的版本到它的索引中。

但是,如果出现纵横交错的合并怎么办?在这种情况下,我们有多个合并基础,并且 Git 将它们全部合并到一个提交中,考虑到新的合并基础是单个基础(参见 )。

我发现 Git 没有为此新提交创建任何 blob 对象。这是否意味着基本版本只存在于工作内存中(Git 索引的第一个插槽)?因此,如果我的计算机关闭并且我有一个未解决的冲突,生成的基本提交是否会丢失?

根据评论更新(现在没有时间研究这个,所以我假设 ET 笔记是准确的...)


首先快速说明一下,这一切都以递归合并策略为前提,并且在相当具体的情况下,它将尝试从多个候选者计算合并基础。

有了这个,如果我们设法让所有这些合并影响单个文件,就很容易看到发生了什么。 “最简单”是一个相对术语,但我们开始...

O -- A - M2 <--(master)
 \     X
  ---B - M1 <--(dev)

O 中,我们有一个名为 foo 的文件。 AB 各自以不冲突的方式更改此文件。 (它不冲突可能不是绝对必要的 - 显然即使是冲突的结果也可以作为递归策略中的计算合并基础 - 但如果你想重现这些步骤并查看回购协议,它可以更容易地看到发生了什么.)

M1AB 的“邪恶合并”;虽然 AB 会完全合并,但在创建 M1.

时对 foo 进行了额外更改

同样,M2AB 的“邪恶合并”; M2 中对 foo 的添加更改与 M1 中对 foo 的添加更改冲突。

现在,如果我们尝试将 devmaster 合并,那么 foo 就会发生冲突;果然

git cat-file -p :1:foo

将向我们展示一个我们之前从未存储过的文件 - AB.

中的 foo 版本之间完全合并的结果

现在回答你的问题:

I found Git does not create any blob object for this new commit

这个说法有点令人困惑;提交不由 BLOB 个对象表示。确实没有 COMMIT 对象表示计算出的合并基础。

但是 :1:foo 的新 BLOB - 就像任何索引条目 [1] 一样。还有(在我的测试中)一个包含 BLOBTREE 对象。换句话说,计算出的合并基础的整个 content 实际上存储在 .git/objects.

Does it mean the base versions exist nowhere but in the working memory (the 1st slot of the index for Git)?

基础版本存在于索引的第一个位置,但那是绝对不是“工作内存”。在您解决冲突期间,通常甚至没有 git 进程 运行 - 因此没有进程的工作内存可用于包含它。

如上所述,索引由磁盘上的 BLOB 个对象组成。

And so, if my computer shuts down and I have an unresolved conflict, would the generated base commit be lost?

没有

但如果是的话会有关系吗?如果不知何故自动生成的合并基丢失了,你必须重新开始,git会自动重新生成它。


[1] - 好的... 叹息... 不是“任何”索引条目。当您声明添加文件的意图时,有“content-less”索引条目。但这与这一切并没有真正的关系,它只是混淆了事情。提它只是为了领先于学究和巨魔。

出于好奇,我一度在 git merge-recursive 中深入研究了这段代码。这是可怕的细节(每个 的“可怕”)。

首先,共享大部分代码的 git merge-recursivegit merge-resolve 收集所有合并基础提交哈希 ID。这形成了一个简单的列表(当我查看时存储为链接列表,但重要的是它是一个有序列表)。如果列表的长度为 1,则我们有一个合并基础,我们就完成了。否则我们有多个合并基础:

  • 如果我们git merge-resolve,我们“随机”选择一个(最方便的,可能是列表的头部)并使用它,我们就完成了。

  • 否则——当我们git merge-recursive——我们使用以下算法:

    • 而列表中至少有两个条目:

      1. 用递归调用合并前两个合并基。这会产生合并结果(在索引中)。如果有合并冲突,冲突会出现在索引中。1
      2. 强制将所有结果推入索引槽零。从此树进行新提交。
      3. 用步骤 2 中的单个提交哈希 ID 替换提交哈希 ID 对。

    本次循环结束时,列表中只有一个哈希ID。这是合并基地。

注意这个算法在合并碱基的数量上是线性的,当我们可以使用对数策略时:如果有,比如说,16 个合并碱基,我们可以合并每一对得到 8 个合并碱基,然后合并每个pair 得到 4,然后合并每对得到 2,然后合并 pair。相反,我们合并 2 得到 1,留下 15 合并;合并2得到1,剩下14;等等。

这可能是完全合理的。 Criss-cross 合并可为您提供 two-merge-base 设置,但这种情况很少见。我不确定您将如何设法设置 16-merge-base 情况。 编辑: 而且,,我们无论如何都不会保存任何主要工作。


1这解释了为什么冲突的对象出现在存储库中。然而,如果我们正在执行最外层的合并——递归代码总是知道这一点——就没有必要将这个对象放入存储库中。请注意,此处用于低级合并的 merge.driver 是执行最外层合并时定义的驱动程序,以及执行任何内部合并时的递归驱动程序;这就是为什么我们知道这是否是最外层的合并。如果它是最外层的合并,则无需在 Git 存储库数据库中创建 blob。