Git:对合并算法、冲突格式以及与 mergetools 的相互作用的混淆
Git: Confusion about merge algorithm, conflict format, and interplay with mergetools
我不知道具体情况,但据我了解合并和解决冲突的过程如下(假设版本库中只有一个文件,在两个分支中修改):
- 用户发出
git merge
命令。
- Git 应用一些 git 特定算法 自动合并两个修改后的文件。为此,它创建文件的 BASE、LOCAL、OTHER 和 BACKUP 版本。
- 然后将合并结果写入原始跟踪文件(称为 MERGED)。
- 假设存在冲突。 Git 使用 某种格式 来表示冲突(
<<<<<<<
、|||||||
、=======
、>>>>>>>
标记)。然后它将其状态设置为 'merging' 或类似的。
- 如果用户随后发出
git mergetool ...
配置的外部合并工具打开,参数指向 BASE、LOCAL、OTHER,当然还有 MERGED。
有几点我很困惑:
- 该工具是否总能理解 Git 的冲突格式?它是标准化的吗?
diff3
选项呢?它也被外部工具普遍理解吗?
- 该工具会应用它自己的(可能不同)合并算法并完全丢弃Git的输出吗?
- 当Git需要执行递归合并(因为有多个合并基础)——中间合并产生冲突——它会把内部冲突标记当作普通的文本与任何其他非冲突文本一样吗?还是冲突格式本身是递归的?
我找不到任何真正讲述整个故事的解释。
合并工具不解析工作目录中带有冲突标记的文件。他们读取 git mergetool
从索引中创建的祖先文件、我们的文件和他们的文件,并为他们将文件放在磁盘上。
他们将使用自己的逻辑生成合并结果,并将覆盖 Git 创建的文件。
完整的答案很复杂。爱德华·汤姆森 (Edward Thomson) 的著作涵盖了大部分内容。这里有更多的细节。
让我们从这个开始:git mergetool
运行s——我应该说,你 运行 它—在之后git merge
的所有剩余部分都完成了。在 git merge
完成(并因冲突而失败)之前,您的合并工具甚至不会进入画面。这会大大改变您思考这些问题的方式。
合并(递归和解析)如何工作
The user issues a git merge
command.
到目前为止一切顺利。
Git applies some git-specific algorithm to automatically merge the two modified files.
糟糕,不,我们已经出轨了,火车可能正驶向悬崖。 :-)
此时的第一步是选择合并策略。让我们选择默认 (-s recursive
) 策略。如果我们选择其他策略,下一步可能会有所不同(-s ours
完全不同,-s octopus
则有些不同,但其中 none 现在很有趣)。
下一步是找到所有的合并基地。运气好的话只有一个。我们稍后会回到递归问题。不过,可能有 no 合并基础。 Git 的旧版本使用空树作为假合并基础。较新的版本(2.9 或更高版本)要求您在此处添加 --allow-unrelated-histories
(然后以相同的方式继续)。对于空树,在 non-base 次提交中添加每个文件。
如果是一个合并基础,它可能与either分支提示相同。如果是这样,则没有要执行的合并。不过,这里也有两个 sub-cases。可能没有什么要合并的,因为合并基础是另一个提交,另一个提交是当前提交的"behind"(是祖先)。在这种情况下,Git 总是什么都不做。或者,另一个提交可能 领先于 (后代)当前提交。在这种情况下,Git 通常执行 fast-forward 操作,除非您指定 --no-ff
。在这两种情况下(fast-forward 或 --no-ff
),没有实际合并发生。相反,further-ahead 提交被提取。它要么 成为 当前提交(fast-forward 合并:无论你在哪个分支上,它现在都指向 further-ahead 提交),或者 Git 使使用该提交树的新提交,新提交成为当前提交。
真正的合并:将一个合并基础与两个提交合并
我们现在处于一个合并基础提交 B 和两个提交 L(本地或 left-side、--ours
) 和 R(远程或 right-side、--theirs
)。现在,两个正常的(-s recursive
和 -s resolve
)策略在启用重命名检测的情况下执行一对 git diff --name-status
操作,以查看 B[= 中是否有文件295=]-to-L change 即改名,如果有文件在B-to-R 改就是改名字。这也会发现 L 或 R 中是否有新添加的文件,以及 L[ 中是否有文件被删除=295=] 或 R。所有这些信息组合起来生成 文件标识 ,以便 Git 知道要组合哪些更改集。这里可能有冲突:一个文件,其路径在base中是PB,但现在都是PL 和 PR,有一个 rename/rename 冲突,例如。
此时的任何冲突——我称它们为高级冲突——都在file-level合并的范围之外:它们将 make Git 以冲突结束此合并过程,无论发生什么情况。但是,与此同时,正如我上面所说,我们最终得到 "identified files",但没有完全定义它。粗略地说,这意味着仅仅因为某些路径 P 发生了变化,并不意味着它是一个 new 文件。如果在基本提交 B 中有一个文件 base
,它现在在 L 中被称为 renamed
但仍然被称为base
in R, Git 将使用新名称,但比较 B:base 与 L:renamed and B:base with R:base when Git goes to combine changes at the file level .
换句话说,我们在这个阶段计算的文件标识告诉我们(和Git)B[=295=中的哪些文件] 匹配 L and/or R 中的哪些文件。此身份不一定是路径名。这只是通常所有三个路径都匹配的情况。
您可以在第一个 diff
阶段插入一些小的调整:
重新规范化 (merge.renormalize
):您可以 Git 应用来自 .gitattributes
and/or core.eol
设置的文本转换。 .gitattributes
设置包括 ident
滤镜和任何涂抹和清洁滤镜(尽管此处仅适用涂抹方向)。
(我假设 Git 做了这个伯爵,因为它可能会影响重命名检测。不过,我还没有实际测试过,我只是查看了 Git 源代码,似乎 没有 在这个阶段使用它。因此,也许 merge.renormalize
不适用于此处,即使涂抹过滤器 可以 从根本上重写文件。例如,考虑一个加密和解密的 filter-pair。这可能是一个错误,尽管很小。幸运的是,EOL 转换对相似性指标值没有任何影响。)
您可以设置相似度指数,以便 Git 考虑重命名文件,或完全禁用重命名检测。这是-X find-renames=<em>n</em>
扩展策略选项,以前叫rename threshold.它与 git diff -M
或 --find-renames
选项相同。
Git 目前无法将 "break" 阈值设置为 la git diff -B
。这也会影响文件身份计算,但如果您不能 设置 它,那也没关系。 (您可能应该能够设置它:另一个小问题。)
合并单个文件
既然我们已经识别了文件并决定了哪些文件与其他文件相匹配,我们最终进入file-merging级别。请注意,如果您使用的是 built-in 合并驱动程序,则剩余的可设置差异选项将开始变得重要。
让我再次引用这一点,因为它是相关的:
Git applies some ... algorithm to automatically merge the two modified files. For this purpose it creates a BASE, LOCAL, OTHER and BACKUP version of the file.
此时涉及 三个(不是四个)文件,但 Git 没有 创建 任何文件.它们是来自 B、L 和 R 的文件。这三个文件在存储库中作为 blob 对象 存在。 (如果 Git 正在重新规范化文件,它 确实 必须在此时将重新规范化的文件创建为 blob 对象,但是它们存在于存储库中,并且 Git只是假装它们在原始提交中。)
下一步非常关键,这就是索引发挥作用的地方。这三个 blob 对象的哈希 ID 是 HB、HL 和 HR。 Git 准备好将这三个哈希放入索引中,分别位于插槽 1、2 和 3 中,但现在使用 the git read-tree
documentation under the 3-Way Merge section:
中描述的规则
- 如果所有三个散列值都相等,则文件已经合并并且什么也没有发生:散列值进入槽零。即使只有第二个和第三个哈希值相等,文件 still 已经合并:L 和 R 相对于 B 进行 same 更改。新哈希进入槽零并且 file-merge 完成。
- 若HB = HL且HB≠HR,右边(remote/other/
--theirs
)文件应该是结果。此散列进入槽零并且 file-merge 完成。
- 若HB≠HL且HB=HR,左边(local/
--ours
)文件应该是结果。此散列进入槽零并且 file-merge 完成。
- 这只剩下所有三个哈希都不同的情况。现在文件 确实 需要合并。 Git 将所有三个哈希放入三个索引槽中。
此时可以应用一些特殊情况,所有这些都与 higher-level 冲突有关。对于某些路径名,一个或两个索引槽可能留空,因为索引以一种使其与 work-tree 同步的方式被仔细管理(以便它可以发挥其作为 cache 大大加快了 Git 的速度)。但原则上,尤其是当我们关注合并驱动程序时,我们可以将其视为 "all three slots"——在重命名文件的情况下,它们可能只是分布在多个名称中的三个插槽。
调用合并驱动程序(.gitattributes
)
此时,我们要执行实际的 file-level 合并。我们有三个 input 文件。它们的 实际内容 作为 blob 对象存储在存储库中。它们的 哈希 ID 存储在索引中的插槽 1 到 3 中(通常是单个索引条目,但在重命名的情况下,可能使用多个索引条目)。我们现在可以:
使用 git 的内置文件合并(也可作为外部命令使用,git merge-file
)。
内置文件合并直接从索引工作(尽管如果我们想通过 git merge-file
运行 它,我们必须将 blob 提取到文件系统中)。它提取文件,合并它们,并有选择地——取决于 extended-strategy-options -X ours
或 -X theirs
——也写入冲突标记。它将最终结果放入 work-tree,在 Git 选择的任何路径名下作为最终路径名,然后完成。
使用合并河流(通过 .gitattributes
)。合并驱动程序是 run with arguments。但是,这些参数是通过 Git 提取 三个 blob 对象到三个临时文件来构造的。
参数是从我们输入的 %O
、%A
、%B
、%L
和 %P
扩展而来的。这些参数字母与我们一直使用的不完全匹配:%O
是 base 文件的名称,%A
是 left-side/local/--ours
版本,%B
是right-side/other/remote/--theirs
版本的名称,%L
是conflict-marker-size
设置(默认7),%P
是Git想要用来保存最终结果在work-tree.
中的路径
注意%O
、%A
和%B
都是Git创建的临时文件的名称(来保存 blob 内容)。 None 个匹配 %P
。 Git 期望合并驱动程序将合并结果留在路径 %A
中(然后 Git 将自行重命名为 %P
)。
在所有情况下,合并后的文件此时都会进入 work-tree。如果合并顺利,索引中的 higher-numbered 槽将被清除:Git,实际上,work-tree 文件上的 运行s git add
,写入将数据作为 blob 对象存入存储库,并获得一个哈希 ID,该哈希 ID 将进入槽零。如果合并因冲突而失败,higher-numbered 槽将保留在原位;插槽零留空。
所有这一切的最终结果是 work-tree 保存合并的文件,可能带有冲突标记,索引保存合并的结果,可能带有应该解决的冲突。
使用git mergetool
这与合并 驱动程序 的工作方式大致相同。除了 运行ning 仅 after 合并已完成,其结果在索引和 work-tree 中,但是,主要区别是:
git mergetool
将制作额外的文件副本(.orig
文件)。
- 它确切地知道如何运行每个已知工具,即传递什么参数来使该工具执行有用的东西。例如,没有等同于驱动程序
%O
占位符的东西。
- 它可以运行命令所有某些目录中的as-yet-unmerged文件。
事实上,git mergetool
是一个很大的shell脚本:它使用git ls-files -u
查找未合并的索引条目,并使用git checkout-index
从索引中提取每个阶段.它甚至有更高级别冲突的特殊情况,例如 add/add 或 rename/delete.
每个已知工具都有一个额外的驱动程序 shell-script 片段:查看
$ ls $(git --exec-path)/mergetools
查看所有单独的工具驱动程序。这些被传递了一个标志,$base_present
,用于处理 add/add 冲突。 (它们是来源的,即 运行 和 . "$MERGE_TOOLS_DIR/$tool"
,因此它们可以覆盖脚本中定义的 shell 函数。)
对于未知工具,您使用shell的变量名$BASE
、$LOCAL
和$REMOTE
来知道脚本将从索引中提取的三个文件放在哪里,然后将结果写入 $MERGED
(实际上是文件的 work-tree 名称)。该脚本执行此操作:
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
在 sub-shell 中使用您的工具命令,这样您就无法像已知工具那样覆盖某些内容。
递归合并
When Git needs to perform a recursive merge ...
这个问题的大部分在这一点上都没有实际意义。合并工具根本不会看到这种情况,因为 git mergetool
在 之后被调用 Git 本身已经完成递归合并并将结果留在索引中并且 work-tree。但是,merge drivers 在这里有发言权。
当 -s recursive
合并 策略 合并 merge-bases 以创建新的 "virtual commit" 时,它会调用另一个 git merge
—好吧,更准确地说,只是递归地调用自己——在合并基础提交上(但见下文)。这个内部 git merge
知道它被递归调用,所以当它要应用 .gitattributes
合并驱动程序时,它会检查那里的 recursive =
设置。这决定了是再次使用合并驱动程序,还是使用其他一些合并驱动程序进行内部合并。对于 built-in 合并驱动程序,Git 关闭扩展策略选项,即 -X ours
和 -X theirs
均无效。
当内部合并完成时,其结果——如果这不是内部递归合并,将留在 work-tree 中的所有文件——实际上保存为 真实 提交。即使存在未解决的冲突也是如此。这些未解决的冲突甚至可能包含冲突标记。 None不过,这是新的 "virtual merge base" 提交,而且是真正的提交;它只是没有外部名称,您可以通过它找到它的提交哈希。
如果在这个特定级别有三个或更多合并基地,而不是只有两个合并基地,这个新的虚拟合并基地现在与下一个剩余的合并基地迭代合并。从逻辑上讲,Git 可以在这里使用 divide-and-conquer 策略:如果有 32 个合并bases 最初,它可以一次合并它们两个以产生 16 个提交,一次合并这两个以产生 8 个,依此类推。但是,除了执行 ceil(log2(N)) 合并而不是 N-1 合并之外,尚不清楚这是否会买很多东西:N > 1 已经很少见了。
我不知道具体情况,但据我了解合并和解决冲突的过程如下(假设版本库中只有一个文件,在两个分支中修改):
- 用户发出
git merge
命令。 - Git 应用一些 git 特定算法 自动合并两个修改后的文件。为此,它创建文件的 BASE、LOCAL、OTHER 和 BACKUP 版本。
- 然后将合并结果写入原始跟踪文件(称为 MERGED)。
- 假设存在冲突。 Git 使用 某种格式 来表示冲突(
<<<<<<<
、|||||||
、=======
、>>>>>>>
标记)。然后它将其状态设置为 'merging' 或类似的。 - 如果用户随后发出
git mergetool ...
配置的外部合并工具打开,参数指向 BASE、LOCAL、OTHER,当然还有 MERGED。
有几点我很困惑:
- 该工具是否总能理解 Git 的冲突格式?它是标准化的吗?
diff3
选项呢?它也被外部工具普遍理解吗? - 该工具会应用它自己的(可能不同)合并算法并完全丢弃Git的输出吗?
- 当Git需要执行递归合并(因为有多个合并基础)——中间合并产生冲突——它会把内部冲突标记当作普通的文本与任何其他非冲突文本一样吗?还是冲突格式本身是递归的?
我找不到任何真正讲述整个故事的解释。
合并工具不解析工作目录中带有冲突标记的文件。他们读取 git mergetool
从索引中创建的祖先文件、我们的文件和他们的文件,并为他们将文件放在磁盘上。
他们将使用自己的逻辑生成合并结果,并将覆盖 Git 创建的文件。
完整的答案很复杂。爱德华·汤姆森 (Edward Thomson) 的著作涵盖了大部分内容。这里有更多的细节。
让我们从这个开始:git mergetool
运行s——我应该说,你 运行 它—在之后git merge
的所有剩余部分都完成了。在 git merge
完成(并因冲突而失败)之前,您的合并工具甚至不会进入画面。这会大大改变您思考这些问题的方式。
合并(递归和解析)如何工作
The user issues a
git merge
command.
到目前为止一切顺利。
Git applies some git-specific algorithm to automatically merge the two modified files.
糟糕,不,我们已经出轨了,火车可能正驶向悬崖。 :-)
此时的第一步是选择合并策略。让我们选择默认 (-s recursive
) 策略。如果我们选择其他策略,下一步可能会有所不同(-s ours
完全不同,-s octopus
则有些不同,但其中 none 现在很有趣)。
下一步是找到所有的合并基地。运气好的话只有一个。我们稍后会回到递归问题。不过,可能有 no 合并基础。 Git 的旧版本使用空树作为假合并基础。较新的版本(2.9 或更高版本)要求您在此处添加 --allow-unrelated-histories
(然后以相同的方式继续)。对于空树,在 non-base 次提交中添加每个文件。
如果是一个合并基础,它可能与either分支提示相同。如果是这样,则没有要执行的合并。不过,这里也有两个 sub-cases。可能没有什么要合并的,因为合并基础是另一个提交,另一个提交是当前提交的"behind"(是祖先)。在这种情况下,Git 总是什么都不做。或者,另一个提交可能 领先于 (后代)当前提交。在这种情况下,Git 通常执行 fast-forward 操作,除非您指定 --no-ff
。在这两种情况下(fast-forward 或 --no-ff
),没有实际合并发生。相反,further-ahead 提交被提取。它要么 成为 当前提交(fast-forward 合并:无论你在哪个分支上,它现在都指向 further-ahead 提交),或者 Git 使使用该提交树的新提交,新提交成为当前提交。
真正的合并:将一个合并基础与两个提交合并
我们现在处于一个合并基础提交 B 和两个提交 L(本地或 left-side、--ours
) 和 R(远程或 right-side、--theirs
)。现在,两个正常的(-s recursive
和 -s resolve
)策略在启用重命名检测的情况下执行一对 git diff --name-status
操作,以查看 B[= 中是否有文件295=]-to-L change 即改名,如果有文件在B-to-R 改就是改名字。这也会发现 L 或 R 中是否有新添加的文件,以及 L[ 中是否有文件被删除=295=] 或 R。所有这些信息组合起来生成 文件标识 ,以便 Git 知道要组合哪些更改集。这里可能有冲突:一个文件,其路径在base中是PB,但现在都是PL 和 PR,有一个 rename/rename 冲突,例如。
此时的任何冲突——我称它们为高级冲突——都在file-level合并的范围之外:它们将 make Git 以冲突结束此合并过程,无论发生什么情况。但是,与此同时,正如我上面所说,我们最终得到 "identified files",但没有完全定义它。粗略地说,这意味着仅仅因为某些路径 P 发生了变化,并不意味着它是一个 new 文件。如果在基本提交 B 中有一个文件 base
,它现在在 L 中被称为 renamed
但仍然被称为base
in R, Git 将使用新名称,但比较 B:base 与 L:renamed and B:base with R:base when Git goes to combine changes at the file level .
换句话说,我们在这个阶段计算的文件标识告诉我们(和Git)B[=295=中的哪些文件] 匹配 L and/or R 中的哪些文件。此身份不一定是路径名。这只是通常所有三个路径都匹配的情况。
您可以在第一个 diff
阶段插入一些小的调整:
重新规范化 (
merge.renormalize
):您可以 Git 应用来自.gitattributes
and/orcore.eol
设置的文本转换。.gitattributes
设置包括ident
滤镜和任何涂抹和清洁滤镜(尽管此处仅适用涂抹方向)。(我假设 Git 做了这个伯爵,因为它可能会影响重命名检测。不过,我还没有实际测试过,我只是查看了 Git 源代码,似乎 没有 在这个阶段使用它。因此,也许
merge.renormalize
不适用于此处,即使涂抹过滤器 可以 从根本上重写文件。例如,考虑一个加密和解密的 filter-pair。这可能是一个错误,尽管很小。幸运的是,EOL 转换对相似性指标值没有任何影响。)您可以设置相似度指数,以便 Git 考虑重命名文件,或完全禁用重命名检测。这是
-X find-renames=<em>n</em>
扩展策略选项,以前叫rename threshold.它与git diff -M
或--find-renames
选项相同。Git 目前无法将 "break" 阈值设置为 la
git diff -B
。这也会影响文件身份计算,但如果您不能 设置 它,那也没关系。 (您可能应该能够设置它:另一个小问题。)
合并单个文件
既然我们已经识别了文件并决定了哪些文件与其他文件相匹配,我们最终进入file-merging级别。请注意,如果您使用的是 built-in 合并驱动程序,则剩余的可设置差异选项将开始变得重要。
让我再次引用这一点,因为它是相关的:
Git applies some ... algorithm to automatically merge the two modified files. For this purpose it creates a BASE, LOCAL, OTHER and BACKUP version of the file.
此时涉及 三个(不是四个)文件,但 Git 没有 创建 任何文件.它们是来自 B、L 和 R 的文件。这三个文件在存储库中作为 blob 对象 存在。 (如果 Git 正在重新规范化文件,它 确实 必须在此时将重新规范化的文件创建为 blob 对象,但是它们存在于存储库中,并且 Git只是假装它们在原始提交中。)
下一步非常关键,这就是索引发挥作用的地方。这三个 blob 对象的哈希 ID 是 HB、HL 和 HR。 Git 准备好将这三个哈希放入索引中,分别位于插槽 1、2 和 3 中,但现在使用 the git read-tree
documentation under the 3-Way Merge section:
- 如果所有三个散列值都相等,则文件已经合并并且什么也没有发生:散列值进入槽零。即使只有第二个和第三个哈希值相等,文件 still 已经合并:L 和 R 相对于 B 进行 same 更改。新哈希进入槽零并且 file-merge 完成。
- 若HB = HL且HB≠HR,右边(remote/other/
--theirs
)文件应该是结果。此散列进入槽零并且 file-merge 完成。 - 若HB≠HL且HB=HR,左边(local/
--ours
)文件应该是结果。此散列进入槽零并且 file-merge 完成。 - 这只剩下所有三个哈希都不同的情况。现在文件 确实 需要合并。 Git 将所有三个哈希放入三个索引槽中。
此时可以应用一些特殊情况,所有这些都与 higher-level 冲突有关。对于某些路径名,一个或两个索引槽可能留空,因为索引以一种使其与 work-tree 同步的方式被仔细管理(以便它可以发挥其作为 cache 大大加快了 Git 的速度)。但原则上,尤其是当我们关注合并驱动程序时,我们可以将其视为 "all three slots"——在重命名文件的情况下,它们可能只是分布在多个名称中的三个插槽。
调用合并驱动程序(.gitattributes
)
此时,我们要执行实际的 file-level 合并。我们有三个 input 文件。它们的 实际内容 作为 blob 对象存储在存储库中。它们的 哈希 ID 存储在索引中的插槽 1 到 3 中(通常是单个索引条目,但在重命名的情况下,可能使用多个索引条目)。我们现在可以:
使用 git 的内置文件合并(也可作为外部命令使用,
git merge-file
)。内置文件合并直接从索引工作(尽管如果我们想通过
git merge-file
运行 它,我们必须将 blob 提取到文件系统中)。它提取文件,合并它们,并有选择地——取决于 extended-strategy-options-X ours
或-X theirs
——也写入冲突标记。它将最终结果放入 work-tree,在 Git 选择的任何路径名下作为最终路径名,然后完成。使用合并河流(通过
.gitattributes
)。合并驱动程序是 run with arguments。但是,这些参数是通过 Git 提取 三个 blob 对象到三个临时文件来构造的。参数是从我们输入的
中的路径%O
、%A
、%B
、%L
和%P
扩展而来的。这些参数字母与我们一直使用的不完全匹配:%O
是 base 文件的名称,%A
是 left-side/local/--ours
版本,%B
是right-side/other/remote/--theirs
版本的名称,%L
是conflict-marker-size
设置(默认7),%P
是Git想要用来保存最终结果在work-tree.注意
%O
、%A
和%B
都是Git创建的临时文件的名称(来保存 blob 内容)。 None 个匹配%P
。 Git 期望合并驱动程序将合并结果留在路径%A
中(然后 Git 将自行重命名为%P
)。
在所有情况下,合并后的文件此时都会进入 work-tree。如果合并顺利,索引中的 higher-numbered 槽将被清除:Git,实际上,work-tree 文件上的 运行s git add
,写入将数据作为 blob 对象存入存储库,并获得一个哈希 ID,该哈希 ID 将进入槽零。如果合并因冲突而失败,higher-numbered 槽将保留在原位;插槽零留空。
所有这一切的最终结果是 work-tree 保存合并的文件,可能带有冲突标记,索引保存合并的结果,可能带有应该解决的冲突。
使用git mergetool
这与合并 驱动程序 的工作方式大致相同。除了 运行ning 仅 after 合并已完成,其结果在索引和 work-tree 中,但是,主要区别是:
git mergetool
将制作额外的文件副本(.orig
文件)。- 它确切地知道如何运行每个已知工具,即传递什么参数来使该工具执行有用的东西。例如,没有等同于驱动程序
%O
占位符的东西。 - 它可以运行命令所有某些目录中的as-yet-unmerged文件。
事实上,git mergetool
是一个很大的shell脚本:它使用git ls-files -u
查找未合并的索引条目,并使用git checkout-index
从索引中提取每个阶段.它甚至有更高级别冲突的特殊情况,例如 add/add 或 rename/delete.
每个已知工具都有一个额外的驱动程序 shell-script 片段:查看
$ ls $(git --exec-path)/mergetools
查看所有单独的工具驱动程序。这些被传递了一个标志,$base_present
,用于处理 add/add 冲突。 (它们是来源的,即 运行 和 . "$MERGE_TOOLS_DIR/$tool"
,因此它们可以覆盖脚本中定义的 shell 函数。)
对于未知工具,您使用shell的变量名$BASE
、$LOCAL
和$REMOTE
来知道脚本将从索引中提取的三个文件放在哪里,然后将结果写入 $MERGED
(实际上是文件的 work-tree 名称)。该脚本执行此操作:
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
在 sub-shell 中使用您的工具命令,这样您就无法像已知工具那样覆盖某些内容。
递归合并
When Git needs to perform a recursive merge ...
这个问题的大部分在这一点上都没有实际意义。合并工具根本不会看到这种情况,因为 git mergetool
在 之后被调用 Git 本身已经完成递归合并并将结果留在索引中并且 work-tree。但是,merge drivers 在这里有发言权。
当 -s recursive
合并 策略 合并 merge-bases 以创建新的 "virtual commit" 时,它会调用另一个 git merge
—好吧,更准确地说,只是递归地调用自己——在合并基础提交上(但见下文)。这个内部 git merge
知道它被递归调用,所以当它要应用 .gitattributes
合并驱动程序时,它会检查那里的 recursive =
设置。这决定了是再次使用合并驱动程序,还是使用其他一些合并驱动程序进行内部合并。对于 built-in 合并驱动程序,Git 关闭扩展策略选项,即 -X ours
和 -X theirs
均无效。
当内部合并完成时,其结果——如果这不是内部递归合并,将留在 work-tree 中的所有文件——实际上保存为 真实 提交。即使存在未解决的冲突也是如此。这些未解决的冲突甚至可能包含冲突标记。 None不过,这是新的 "virtual merge base" 提交,而且是真正的提交;它只是没有外部名称,您可以通过它找到它的提交哈希。
如果在这个特定级别有三个或更多合并基地,而不是只有两个合并基地,这个新的虚拟合并基地现在与下一个剩余的合并基地迭代合并。从逻辑上讲,Git 可以在这里使用 divide-and-conquer 策略:如果有 32 个合并bases 最初,它可以一次合并它们两个以产生 16 个提交,一次合并这两个以产生 8 个,依此类推。但是,除了执行 ceil(log2(N)) 合并而不是 N-1 合并之外,尚不清楚这是否会买很多东西:N > 1 已经很少见了。