dom*_*min 10 git algorithm file-format mergetool git-merge-conflict
我不知道细节,但据我了解合并和冲突解决的过程,它如下(假设存储库中只有一个文件,在两个分支中修改):
git merge命令.<<<<<<<,|||||||,=======,>>>>>>>标记).然后它将其状态设置为"合并"或类似.git mergetool ...配置的外部合并工具打开,参数指向BASE,LOCAL,OTHER,当然还有MERGED.有几点我很困惑:
diff3选项怎么样?它是否也被外部工具普遍理解?我找不到真正讲述整个故事的任何解释.
完整的答案很复杂.爱德华汤姆森的大部分内容都是如此.这里有更多细节.
但是,让我们开始吧:git mergetool运行 - 我应该说,你运行它 - 在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启用了重命名检测的操作,以查看B- to- L更改中是否有更改其名称的文件,以及B- to- R中是否有文件改变他们的名字.这还会发现L或R中是否有新添加的文件,以及是否在L或R中删除了文件.所有这些信息被组合在一起以产生文件标识,因此Git知道要组合哪些更改集.这里可能存在冲突:例如,基本路径为P B的文件,但现在是P L和P R,具有重命名/重命名冲突.
在这个任何冲突点,我打电话给他们高层次矛盾 -lie文件级合并外域:他们会做的Git结束这一合并进程有冲突,无论任何其他人发生的.与此同时,我们最终得到了"已识别的文件",正如我上面所说,没有完全定义它.松散地说,这意味着只是因为某些路径P发生了变化,并不意味着它是一个新文件.如果有一个文件base在底座提交乙,而且它现在所谓renamed的大号,但仍称base在[R,Git会使用新的名称,但比较B:基带L:重命名和B:基础与R:基地时,Git的去组合文件级别的更改.
换句话说,我们在此阶段计算的文件标识告诉我们(和Git)B中的哪些文件匹配L和/或R中的哪些文件.此标识不一定是路径名.这只是一般,所有的三个路径匹配的情况.
在第一diff阶段可以插入一些小的调整:
重整化(merge.renormalize):您可以让Git应用文本转换.gitattributes和/或core.eol设置.该.gitattributes设置包括ident过滤器和任何污迹和清洁过滤器(虽然只涂抹方向在这里也适用).
(我假设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不会创建任何文件.它们是来自B,L和R的文件.这三个文件在存储库中作为blob对象存在.(如果Git重新规范化文件,那么它必须在此时创建重新规范化的文件作为blob对象,但是它们会存在于存储库中,而Git只是假装它们在原始提交中.)
下一步非常关键,它是指数进入图景的地方.这三个斑点对象的散列ID的是H 乙,H 大号和H [R .GIT中分别准备好放置这三个散列到索引,在时隙1,2和3,但现在使用中所描述的规则的git read-tree文档下的3路合并部分:
--theirs应该是右侧(远程/其他/ )文件.此哈希进入插槽零,文件合并完成.--ours)文件应为结果.此哈希进入插槽零,文件合并完成.此时可以应用一些特殊情况,这些都与更高级别的冲突有关.对于某些路径名,可能会有一个或两个索引槽为空,因为索引是以一种使其与工作树保持同步的方式进行仔细管理的(因此它可以充当缓存加速Git的角色)很多).但原则上,特别是当我们关注合并驱动程序时,我们可以将其视为"所有三个插槽" - 在重命名文件的情况下,它们可能只是分布在多个名称上的三个插槽.
.gitattributes)此时,我们要执行实际的文件级合并.我们有三个输入文件.它们的实际内容作为blob对象存储在存储库中.它们的散列ID存储在索引中,位于插槽1到3中(通常是单个索引条目,但在重命名的情况下,可能使用多个索引条目).我们现在可以:
使用git的内置文件合并(也可用作外部命令git merge-file).
内置文件合并直接从索引开始工作(但是如果我们想通过它运行它,git merge-file我们必须将blob提取到文件系统中).它提取文件,合并它们,并且可选地 - 取决于扩展策略选项-X ours或-X theirs写入冲突标记.它将最终结果丢弃到工作树中,在Git选择的任何路径名称作为最终路径名称,并完成.
使用合并驱动程序(via .gitattributes).使用参数运行合并驱动程序.但是,这些参数是通过让Git 将三个blob对象提取到三个临时文件来构造的.
参数被扩展,从无论我们把尽可能%O,%A,%B,%L,和%P.这些参数字母与我们使用的不完全匹配:%O是基本文件%A的名称,是左侧/本地/ --ours版本%B的名称,是右侧/其他/远程/ --theirs版本的名称,%L是conflict-marker-size设置(默认为7),%P是Git想要用于在工作树中保存最终结果的路径.
需要注意的是%O,%A和%B是所有的名字暂时了Git创建(持有BLOB内容)的文件.他们都不匹配%P.Git期望合并驱动程序将合并的结果留在路径中%A(然后Git将%P自己重命名为).
在所有情况下,合并文件都会进入工作树.如果合并顺利,则清除索引中编号较高的插槽:Git实际上git add在工作树文件上运行,将数据作为blob对象写入存储库,并获取进入的哈希ID槽零.如果合并失败并且存在冲突,则编号较高的插槽仍然存在; 插槽零留空.
所有这一切的最终结果是工作树保存合并的文件,可能包含冲突标记,索引保存合并的结果,可能包含应该解决的冲突.
git mergetool这与合并驱动程序的工作方式大致相同.除了在合并完成后运行其结果在索引和工作树中,但主要区别在于:
git mergetool将制作额外的文件副本(.orig文件).%O例如,没有与驱动程序占位符等效的东西.实际上,它git mergetool是一个很大的shell脚本:它用于git ls-files -u查找未合并的索引条目,并git checkout-index从索引中提取每个阶段.它甚至有更高级别冲突的特殊情况,例如添加/添加或重命名/删除.
每个已知工具都有一个额外的驱动程序shell脚本片段:查看
$ ls $(git --exec-path)/mergetools
Run Code Online (Sandbox Code Playgroud)
查看所有单个工具驱动程序.这些是传递一个标志,$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 )
}
}
Run Code Online (Sandbox Code Playgroud)
即,eval你的工具命令在子shell中,这样你就不能像已知工具那样覆盖事物.
当Git需要执行递归合并时 ......
大多数问题在这一点上都没有实际意义.合并工具根本不会看到这种情况,因为在 Git本身完成递归合并并将结果保留在索引和工作树之后git mergetool调用.但是,合并驱动程序确实在这里有发言权.
当-s recursive合并策略合并merge-bases以创建一个新的"虚拟提交"时,它会调用另一个git merge-well,更准确地说,只是在合并基础提交上递归调用它自己(但见下文).这个内部git merge知道它是以递归方式调用的,所以当它即将应用.gitattributes合并驱动程序时,它会检查recursive =那里的设置.这将确定是否再次使用合并驱动程序,或者是否使用其他合并驱动程序进行内部合并.对于内置的合并驱动程序,Git关闭了扩展策略选项,即既不生效-X ours也不-X theirs生效.
当内部合并完成时,其结果 - 将留在工作树中的所有文件,这不是内部的递归合并 - 实际上保存为实际提交.即使存在未解决的冲突,也是如此.这些未解决的冲突甚至可能包含冲突标记.尽管如此,这是一个新的"虚拟合并基础"提交,它是一个真正的提交; 它只是没有外部名称,您可以通过它找到其提交哈希.
如果在此特定级别存在三个或更多合并库,而不仅仅是两个合并库,则此新虚拟合并库现在将迭代地与下一个剩余的合并库合并.逻辑上,Git可以在这里使用分而治之的策略:如果最初有32个合并库,它可以一次合并两个以产生16个提交,一次合并这两个以产生8,依此类推.除了做ceil(log2(N))合并而不是N-1合并之外,还不清楚这会买得多:N> 1已经很少了.