10

具体我不知道,但据我了解的合并和冲突解决的过程是这样的(假设仓库只有一个文件,在两个分支中修改):

  1. 用户发出git merge命令。
  2. Git 应用一些特定于 git 的算法来自动合并两个修改过的文件。为此,它会创建文件的 BASE、LOCAL、OTHER 和 BACKUP 版本。
  3. 然后它将合并结果写入原始跟踪文件(称为 MERGED)。
  4. 假设存在冲突。Git 使用某种格式来表示冲突(<<<<<<<, |||||||, =======,>>>>>>>标记)。然后它将其状态设置为“正在合并”或类似的。
  5. 如果用户随后发出git mergetool ...配置的外部合并工具,则会打开,参数指向 BASE、LOCAL、OTHER,当然还有 MERGED。

有几点我很困惑:

  • 该工具会始终理解 Git 的冲突格式吗?是否标准化?diff3选项呢?外部工具也普遍理解吗?
  • 该工具会应用它自己的(可能是不同的)合并算法并完全丢弃 Git 的输出吗?
  • 当 Git 需要执行递归合并(因为有多个合并基础)——并且中间合并会产生冲突——它是否会将内部冲突标记视为纯文本,就像任何其他非冲突文本一样?还是冲突格式本身是递归的?

我找不到任何能真正说明整个故事的解释。

4

2 回答 2

5

完整的答案很复杂。Edward Thomson 涵盖了大部分内容。这里有更多的细节。

不过,让我们从这个开始:git mergetool运行——我应该说,运行——<em>在完成所有其余部分之后git merge。您的合并工具在完成​​之前甚至不会进入图片git merge(并且由于冲突而失败)。这会改变很多你思考这些的方式。

(递归和解析)合并如何工作

用户发出git merge命令。

到目前为止,一切都很好。

Git 应用一些特定于 git 的算法来自动合并两个修改过的文件。

哎呀,不,我们已经出轨了,火车可能要冲下悬崖了。:-)

此时的第一步是选择合并策略。让我们选择默认 ( -s recursive) 策略。如果我们选择其他策略,下一步可能会有所不同(对于 完全不同,对于-s ours也有些不同-s octopus,但无论如何现在这些都不是有趣的)。

下一步是找到所有的合并基地。运气好的话,只有一个。我们稍后会回到递归问题。但是,可能没有合并基础。旧版本的 Git 使用空树作为假合并基础。较新的版本(2.9 或更高版本)要求您在--allow-unrelated-histories此处添加(然后以相同的方式继续)。对于空树,在两个非基本提交中都会添加每个文件。

如果有一个合并基础,它可能与一分支尖端相同。如果是这样,则没有要执行的合并。不过,这里也有两个子案例。可能没有要合并的内容,因为合并基础是另一个提交,而另一个提交在当前提交的“后面”(是其祖先)。在这种情况下,Git 总是什么都不做。或者,另一个提交可能在当前提交(的后代)之前。在这种情况下,Git 通常会执行快进操作,除非您指定--no-ff. 在这两种情况下(快进或--no-ff),都不会发生实际的合并。相反,提取了更进一步的提交。它要么变成当前提交(快进合并:无论您在哪个分支上,它现在都指向更前面的提交),或者 Git 使用该提交的树进行新提交,新提交成为当前提交。

真正的合并:将一个合并基础与两个提交合并

我们现在处于一个阶段,我们有一个合并基础提交B和两个提交L(本地或左侧,--ours)和R(远程或右侧,--theirs)。现在,两个正常 (-s recursive-s resolve) 策略在启用重命名检测的情况下执行一对git diff --name-status操作,以查看BL更改中是否有文件更改了它们的名称,以及BR中是否有文件改变那个改变他们的名字。这还可以查明 L 或 R 中是否有新添加的文件以及LR是否删除了文件. 所有这些信息都被组合起来生成文件标识,以便 Git 知道要组合哪些更改集。这里可能存在冲突:例如,一个文件的路径是P B在基础中,但现在是P LP R,具有重命名/重命名冲突。

在这一点上的任何冲突——我称之为高级冲突——都在文件级合并的范围之外:它们将使Git 以冲突结束这个合并过程,无论发生什么其他事情。但与此同时,正如我上面所说的,我们最终得到了“已识别文件”,但没有完全定义它。松散地说,这意味着仅仅因为某些路径P发生了变化,并不意味着它是一个文件。如果base在基本提交B中有一个文件,现在renamedL中调用它但仍在Rbase中调用,Git 将使用新名称,但将B:baseL:renamedB:baseR:base当 Git 去合并文件级别的更改时。

换句话说,我们在这个阶段计算的文件标识告诉我们(和 Git)B中的哪些文件与L和/或R中的哪些文件匹配。此标识不一定是路径名。通常情况下,所有三个路径都匹配。

diff您可以在第一阶段插入一些小调整:

  • 重新规范化 ( ):您可以让 Git 应用来自和/或设置merge.renormalize的文本转换。设置包括过滤器以及任何涂抹和清洁过滤器(尽管此处仅适用涂抹方向)。.gitattributescore.eol.gitattributesident

    (我认为 Git 很早就这样做了,因为它可能会影响重命名检测。不过,我还没有实际测试过,我只是查看了 Git 源代码,在这个阶段似乎没有merge.renormalize使用它。所以可能不适用于这里,即使涂抹过滤器可以彻底重写文件。例如,考虑一个加密和解密的过滤器对。这可能是一个错误,尽管是一个小错误。幸运的是,EOL 转换对相似性索引值根本没有影响。)

  • 您可以设置 Git 何时考虑重命名文件的相似性索引,或完全禁用重命名检测。这是扩展策略选项,以前称为重命名阈值。它与or选项相同。-X find-renames=ngit diff -M--find-renames

  • Git 目前无法将“中断”阈值设置为 la git diff -B。这也会影响文件身份计算,但如果你不能设置它,那并不重要。(您可能应该能够设置它:另一个小号。)

合并单个文件

现在我们已经确定了文件并确定了哪些文件与其他文件匹配,我们最终进入文件合并级别。请注意,如果您使用的是内置的合并驱动程序,那么剩余的可设置差异选项将开始变得重要。

让我再次引用这一点,因为它是相关的:

Git 应用了一些 ... 算法来自动合并两个修改过的文件。为此,它会创建文件的 BASE、LOCAL、OTHER 和 BACKUP 版本。

此时涉及三个(不是四个)文件,但 Git 不会创建其中任何一个。它们是来自BLR的文件。这三个文件作为存储库中的blob 对象存在。(如果 Git 正在重新规范化文件,此时它确实必须将重新规范化的文件创建为 blob 对象,但随后它们会存在于存储库中,而 Git 只是假装它们在原始提交中。)

下一步非常关键,这就是索引出现的地方。这三个 blob 对象的哈希 ID 是 H B、 H L和 H R。Git 已准备好将这三个散列放入索引中,分别位于插槽 1、2 和 3 中,但现在使用3-Way Merge 部分下git read-tree文档中描述的规则:

  • 如果所有三个哈希值都相等,则文件已经合并并且没有任何反应:哈希值进入插槽零。即使只有第二个和第三个哈希值相等,文件仍然已经合并:LR都对B进行了相同的更改。新的散列进入槽零并且文件合并完成。
  • 如果 H B = H L且 H B ≠ H R,则右侧(远程/其他/ --theirs)文件应该是结果。此哈希进入插槽零并且文件合并完成。
  • 如果 H B ≠ H L且 H B = H R,则左侧(本地/ --ours)文件应该是结果。此哈希进入插槽零并且文件合并完成。
  • 这只留下了所有三个哈希都不同的情况。现在文件确实需要合并。Git 将所有三个哈希值放入三个索引槽中。

在这一点上可以应用一些特殊情况,所有这些都与更高级别的冲突有关。对于某些路径名,可能会有一个或两个索引槽为空,因为索引是经过精心管理的,以使其与工作树保持同步(这样它就可以发挥缓存的作用,加快 Git 的运行速度)很多)。但原则上,特别是当我们关注合并驱动程序时,我们可以将其视为“所有三个插槽”——在重命名文件的情况下,它们可能只是分布在多个名称中的三个插槽。

调用合并驱动程序 ( .gitattributes)

此时,我们有一个实际的文件级合并要执行。我们有三个输入文件。它们的实际内容作为 blob 对象存储在存储库中。它们的哈希 ID存储在索引中的插槽 1 到 3 中(通常是单个索引条目,但在重命名的情况下,可能使用多个索引条目)。我们现在可以:

  • 使用 git 的内置文件合并(也可作为外部命令使用git merge-file)。

    内置文件合并直接从索引工作(尽管如果我们想通过它运行它,git merge-file我们必须将 blob 提取到文件系统中)。它提取文件,合并它们,并且可选地——取决于扩展策略选项-X ours或——也-X theirs写入冲突标记。它将最终结果放到工作树中,无论 Git 选择什么路径名作为最终路径名,然后就完成了。

  • 使用合并驱动程序(通过.gitattributes)。合并驱动程序使用参数运行。但是,这些参数是通过让 Git将三个 blob 对象提取到三个临时文件来构建的。

    参数从我们输入的任何内容扩展为%O, %A, %B,%L%P。这些参数字母与我们一直使用的不太匹配:%O基本文件%A的名称,是左侧/本地/--ours版本%B的名称,是右侧/其他/远程/--theirs版本的名称,%Lconflict-marker-size设置(默认 7),%P是 Git 想要用来将最终结果保存在工作树中的路径。

    请注意%O%A、 和是 Git 创建的所有临时文件%B的名称(用于保存 blob 内容)。他们都不匹配。Git 期望合并驱动程序将合并的结果留在路径中(然后 Git 将自行重命名为)。%P%A%P

在所有情况下,合并的文件都进入工作树,此时。如果合并顺利,索引中编号较高的插槽将被清除:实际上,Git 在git add工作树文件上运行,将数据作为 blob 对象写入存储库,并获取进入的哈希 ID插槽零。如果合并因冲突而失败,则编号较高的插槽保持原位;插槽零为空。

所有这一切的最终结果是工作树保存合并的文件,可能带有冲突标记,而索引保存合并的结果,可能带有应该解决的冲突。

使用git mergetool

这与合并驱动程序的工作方式大致相同。但是,除了仅合并完成后运行,其结果在索引和工作树中,主要区别是:

  • git mergetool.orig将制作文件(文件)的额外副本。
  • 它确切地知道如何运行每个已知工具,即传递什么参数以使该工具做一些有用的事情。例如,没有等效于驱动程序%O占位符。
  • 它可以对某个目录中所有尚未合并的文件运行命令。

其实git mergetool是一个很大的shell脚本:它git ls-files -u用来查找未合并的索引条目,并git checkout-index从索引中提取每个阶段。它甚至有更高级别冲突的特殊情况,例如添加/添加或重命名/删除。

每个已知工具都有一个额外的驱动程序外壳脚本片段:查看

$ ls $(git --exec-path)/mergetools

查看所有单独的工具驱动程序。这些被传递了一个标志,$base_present,用于处理添加/添加冲突。(它们是有源的,即使用 运行. "$MERGE_TOOLS_DIR/$tool",因此它们可以覆盖脚本中定义的 shell 函数。)

对于未知工具,您可以使用 shell 的变量名称$BASE$LOCAL$REMOTE来了解脚本将从索引中提取的三个文件放在哪里,然后将结果写入$MERGED(实际上是文件的工作树名称)。该脚本执行此操作:

setup_user_tool () {
        merge_tool_cmd=$(get_merge_tool_cmd "$tool")
        test -n "$merge_tool_cmd" || return 1

        diff_cmd () {
                ( eval $merge_tool_cmd )
        }

        merge_cmd () {
                ( eval $merge_tool_cmd )
        }
}

即,eval将您的工具命令放在子外壳中,这样您就不能像已知工具那样覆盖事物。

递归合并

当 Git 需要执行递归合并时...

在这一点上,这个问题的大部分内容都没有实际意义。合并工具根本看不到这种情况,因为在Git 本身完成递归合并并将结果留在索引和工作树中之后git mergetool调用。但是,合并驱动程序确实在这里有发言权。

-s recursive合并策略合并合并基础以进行新的“虚拟提交”时,它会git merge在合并基础提交上调用另一个——嗯,更准确地说,只是递归地调用自身(但见下文)。这个内部git merge知道它是被递归调用的,所以当它要应用一个.gitattributes合并驱动程序时,它会检查recursive =那里的设置。这确定是再次使用合并驱动程序,还是使用其他合并驱动程序进行内部合并。对于内置的合并驱动程序,Git 关闭了扩展策略选项,即既不生效-X ours也不-X theirs生效。

当内部合并完成时,它的结果(如果这不是内部递归合并,所有将留在工作树中的文件)实际上会保存为真正的提交。即使存在未解决的冲突也是如此。这些未解决的冲突甚至可能包含冲突标记。尽管如此,这是新的“虚拟合并基础”提交,而且是真正的提交;它只是没有外部名称,您可以通过它找到它的提交哈希。

如果在此特定级别存在三个或更多合并基础,而不仅仅是两个合并基础,则此新的虚拟合并基础现在迭代地与下一个剩余合并基础合并。从逻辑上讲,Git 可以在这里使用分而治之的策略:如果最初有 32 个合并基,它可以一次合并两个以产生 16 个提交,一次合并这两个以产生 8 个,依此类推。但是,除了进行 ceil(log2(N)) 合并而不是 N-1 合并之外,还不清楚这是否会带来很多好处:N > 1 已经非常罕见了。

于 2017-05-27T16:29:06.640 回答
4

合并工具不会使用冲突标记解析工作目录中的文件。他们读取从索引创建的祖先、我们和他们的文件,git mergetool并为他们放置在磁盘上。

他们将使用自己的逻辑来生成合并结果,并将覆盖 Git 创建的文件。

于 2017-05-27T04:39:55.890 回答