Git:关于合并算法,冲突格式以及与mergetools的相互作用的混淆 [英] Git: Confusion about merge algorithm, conflict format, and interplay with mergetools

查看:278
本文介绍了Git:关于合并算法,冲突格式以及与mergetools的相互作用的混淆的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我不知道细节,但据我了解合并和冲突解决的过程如下(假设存储库中只有一个文件,并在两个分支中进行了修改):


  1. 用户发出 git merge 命令。

  2. Git使用一些特定于git的算法来自动合并两个修改后的文件。为此,它会创建一个BASE,LOCAL,OTHER和BACKUP版本的文件。
  3. 然后,它将合并结果写入原始跟踪文件(称为已合并)。
  4. 假设存在冲突。 Git使用某种格式来表示冲突(<<<<<< ||||||| ======= >>>> >>>< / code>标记)。然后,它将其状态设置为合并或类似。

  5. 如果用户发出 git mergetool ... 工具打开,参数指向BASE,LOCAL,OTHER,当然也是MERGED。

有几点我很困惑about:


  • 该工具是否总是理解Git的冲突格式?它是标准化的吗?那么 diff3 选项怎么样?它是否也被外部工具所普遍理解?

  • 该工具是否会应用自己的(也许是不同的)合并算法并完全垃圾Git的输出?
  • li>
  • 当Git需要执行递归合并(因为有多个合并基础),并且中间合并创建冲突时,它会将内部冲突标记视为纯文本,就像任何其他非冲突文本?或者是冲突格式递归本身?



我找不到任何可以说明整个 的解释。故事。

解决方案

完整的答案很复杂。爱德华汤姆森的报道很多。这里有相当多的细节。



让我们从这个开始: git mergetool runs - 我应该说, git merge 完成了。直到 git merge 完成(并且由于冲突而失败),您的合并工具甚至不会输入图片。这会改变你对这些事情的很多方式。



合并作品的方式




用户发出 git merge 命令。



<


Git使用一些 git专用算法自动合并两个修改过的文件。




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



这一步的第一步是选择合并策略。让我们选择默认的( -s递归)策略。如果我们选择其他策略,下一步可能会有所不同(对于 -s我们完全不同),对于 -s章鱼,但无论如何,这些都不是很有趣)。



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



如果一个合并基底,它可能是与相同分支提示。如果是这样,则不会执行合并。不过,这里也有两个子例子。可能没有什么可以合并,因为合并基础是另一个提交,而另一个提交是当前提交的后面(是其祖先)。在这种情况下,Git总是什么都不做。或者,另一个提交可能在当前提交的(后代)之前。在这种情况下,除非您指定 - no-ff ,否则Git通常会执行快进操作。在这两种情况下(快进或 - no-ff ),都不会发生实际的合并。相反,提前进行的提交会被提取。它或者成为当前的提交(快进合并:你所在的任何分支,它现在指向进一步提交),或者Git使用该提交的树进行新的提交,而新的提交成为当前提交。



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



在我们有一个单一的合并基础提交 B ,和两个提交 L (本地或左侧, - 我们 c $ c>)和 R (远程或右侧, - 他们的)。现在,两个正常的( -s递归 -s resolve )策略执行一对 git diff --name-status 在启用重命名检测的情况下查看是否存在 B -to- L 中的文件改变更改它们的名称,并且如果在 B -to- R 更改中有文件更改其名称。这还会发现是否在 L R 中新添加了文件,并且在 L R中删除了文件。所有这些信息结合起来生成文件标识,以便Git知道要组合哪些更改。这里可能存在冲突:一个文件的路径是 B ,但现在是

L P R ,例如有一个重命名/重命名冲突。



我将它们称为高级冲突 -lie在文件级合并领域之外:它们将 使Git以冲突结束此合并进程,而不管发生任何其他事情。但与此同时,如上所述,我们最终得到已识别的文件,但没有对其进行定义。松散地说,这意味着仅仅是因为一些路径 P 发生了变化,并不意味着它是一个新的文件。如果在基本提交 B 中存在 base 文件,现在它被称为已重命名 L 中,但仍然在 R 中调用 base ,Git将使用新名称,但比较 B:当Git结合文件级别的更改时,使用 L:renamed B:base R:base / p>

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



在第一个 diff phase:


  • 重整化( merge.renormalize ):你可以让Git从 .gitattributes 和/或 core.eol 设置应用文本转换。 .gitattributes 设置包括 ident 过滤器和任何涂抹和干净过滤器(尽管这里仅适用涂抹方向)。



    (我认为Git做得这么早,因为它可能会影响重命名检测,但我没有真正测试过这个,而且我只是查看了Git源代码, 在这个阶段使用它,所以也许 merge.renormalize 在这里不适用,即使一个涂抹过滤器可以从根本上重写一个文件,例如考虑一个加密和解密的过滤器对,这可能是一个小错误,幸运的是,EOL转换对相似性索引值完全没有影响。)


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


  • Git目前无法将break阈值设置为la git diff -B 。这也会影响文件标识计算,但是如果你不能设置它,它并不重要。 (您可能应该可以设置它:另一个小问题)。

  • b
    $ b

    现在我们已经确定了我们的文件并决定哪些文件与其他文件匹配,我们最后进入文件合并级别。请注意,在这里,如果您使用内置的合并驱动程序,剩下的可设置差异选项将开始有用。



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


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


    此时涉及三个(而不是四个)文件,但Git不会创建它们中的任何一个。它们是来自 B L R 的文件。这三个文件在存储库中以 blob对象存在。 (如果Git是重新规范化文件,那么 不得不在现在创建重新规格化的blob对象,但它们存在于存储库中,而Git只是假装它们在原始提交中。 )



    下一步是非常关键的,它是指数进入图片的地方。这三个blob对象的散列ID是H B ,H L 和H R 。 Git已准备好将这三个哈希分别放入索引中,分别位于第1,2和3槽中,但现在使用 3-Merge部分下的 git read-tree 文档


    • 如果所有三个哈希值相等,则文件已经合并,并且没有任何反应:哈希值进入时隙零。即使只有第二和第三个哈希值相等,文件仍然是 已合并: L R 使 / em>相对于 B 进行更改。新散列进入时隙零,文件合并完成。

    • 如果H = H 和H > B ≠H R 时,右侧(remote / other / - 他们的)文件应该是结果。该散列进入第零时隙并且文件合并完成。如果H B低于H L和H H, B = H R 时,左边(local / - 我们的)文件应该是结果。这个哈希进入第零时隙,文件合并完成。

    • 这只留下三种哈希不同的情况。现在需要合并文件 。 Git将所有三个哈希值放入三个索引槽中。



    有几种特殊情况可以在这个时候应用,都必须处理更高级别的冲突。对于某些路径名,可能会留有一个或两个索引槽为空,因为该索引是按照与工作树保持同步的方式进行仔细管理的(以便它可以扮演 cache 这会加快Git的速度)。但原则上,特别是当我们关注合并驱动程序时,我们可以将其视为所有三个插槽 - 它们可能是三个插槽,分布在多个名称中,在重命名的文件中。



    调用合并驱动程序( .gitattributes



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



    内置的文件合并直接从索引处理(尽管如果我们想通过 git merge-file 我们必须将blob提取到文件系统中)。它提取文件,做它的事情来合并它们,也可以选择 - 取决于扩展策略选项 -X我们的 -X他们的 - 也会写入冲突标记。它将最终结果放到工作树中,在Git选择的最终路径名称的任何路径名下,并完成。 使用合并驱动程序(通过 .gitattributes )。合并驱动程序是以参数运行。然而,这些参数是通过让Git将三个blob对象提取到三个临时文件来构造的。



    参数从我们放入的任何东西如%O %A %B %L %P 。这些参数字母与我们所用的不一致:%O base 文件的名称, %A 是左侧/ local / - 我们的版本的名称,%B 是右侧/ other / remote / - 他们的版本的名称,%L 冲突标记大小设置(默认为7),%P 是Git想要使用的路径请注意,%O %最终结果保存在工作树中。 一个%B 都是Git创建的临时文件的名称(用于存放blob内容)。没有一个匹配%P 。 Git希望合并驱动程序将合并结果保留在路径%A 中(Git将重命名为%P git add ,将数据写入该存储库作为一个blob对象,并获取一个散列ID到零插槽。如果合并因冲突而失败,则较高编号的时隙仍然存在;插槽零为空。



    所有这些的最终结果是工作树保存合并文件,可能带有冲突标记,并且索引包含合并,也许有应该解决的冲突。



    使用 git mergetool



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




    • git mergetool 会创建额外的文件副本( .orig 文件)。
    • >
    • 它完全知道如何运行每个已知的工具,即通过哪些参数来使该工具做一些有用的工作。例如,没有与驱动程序%O 占位符相当的功能。

    • 它可以在所有


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



    每个已知工具还有一个额外的驱动程序shell脚本片段:look in

      $ 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||返回1

    diff_cmd(){
    (eval $ merge_tool_cmd)
    }

    merge_cmd(){
    (eval $ merge_tool_cmd)


    eval 在子shell中使用你的工具命令,这样你就不能用已知工具的方式覆盖事物。

    递归合并




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

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


    $ b -s递归合并策略合并合并基础以创建新的虚拟提交,它调用另一个 git merge - 更精确地说,递归地 - 在合并基础上提交(但在下面看到)。这个内部 git merge 知道它是递归调用的,所以当它要应用 .gitattributes 合并驱动时,它在那里检查 recursive = 设置。这将确定是否再次使用合并驱动程序,或者是否将其他合并驱动程序用于内部合并。对于内置的合并驱动程序,Git关闭扩展策略选项,即既不是 -X也不是 -X他们的有效。

    当内部合并完成时,其结果 - 将留在工作树中的所有文件都不是内部递归合并 - 实际上被保存为一个 real 提交。即使存在未解决的冲突,情况也是如此。这些未解决的冲突甚至可能包含冲突标记。尽管如此,这是新的虚拟合并基础提交,它是一个真正的提交;它只是没有外部名称,通过它你可以找到它的提交散列。



    如果在这个特定级别有三个或更多的合并库,而不是两个合并库,这个新的虚拟合并基础现在与迭代的剩余合并基础合并。从逻辑上讲,Git可以在这里使用分治策略:如果最初有32个合并基地,它可以一次合并两个合并基准,以产生16个提交,每次合并这两个合并基准以产生8个,依此类推。除了执行ceil(log2(N))合并而不是N-1合并之外,目前尚不清楚这会大量购买:现在已经非常少见N> 1。

    I don't know the details, but as far as I understand the process of merging and conflict resolution, it goes as follows (assume there is only one file in the repository, modified in two branches):

    1. The user issues a git merge command.
    2. Git applies some git-specific algorithm to automatically merge the two modified files. For this purpose it creates a BASE, LOCAL, OTHER and BACKUP version of the file.
    3. It then writes the merge result into the original tracked file (call it MERGED).
    4. Assume there are conflicts. Git uses some format to represent the conflict (<<<<<<<, |||||||, =======, >>>>>>> markers). It then sets its status to 'merging' or similar.
    5. If the user then issues git mergetool ... the configured external merge tool opens, with arguments pointing to the BASE, LOCAL, OTHER and of course MERGED.

    There are a few points I'm confused about:

    • Will the tool always understand Git's conflict format? Is it standardized? What about the diff3 option? Is it also commonly understood by external tools?
    • Will the tool apply its own (and maybe different) merge algorithm and trash the output of Git entirely?
    • When Git needs to perform a recursive merge (because of several merge bases)—and the intermediate merge creates conflicts—will it treat inner conflict markers as plain text just as any other non-conflicting text? Or is the conflict format recursive itself?

    I couldn't find any explanation that really tells the whole story.

    解决方案

    The full answer is complicated. Edward Thomson's covers much of it. Here is considerably more detail.

    Let's start, though, with this: git mergetool runs—I should say, you run itafter all the rest of git merge is done. Your merge tools do not even enter the picture until git merge has completed (and failed due to conflicts). This changes a lot of the way you will think about these.

    How (recursive and resolve) merge works

    The user issues a git merge command.

    So far so good.

    Git applies some git-specific algorithm to automatically merge the two modified files.

    Whoops, no, we've already derailed and the train may be heading off the cliff. :-)

    The first step at this point is to choose a merge strategy. Let's pick the default (-s recursive) strategy. If we pick some other strategy, the next step may be different (it is entirely different for -s ours, and somewhat different for -s octopus, but none of those are interesting right now anyway).

    The next step is to find all the merge bases. With any luck there is only one. We'll come back to the recursion issue later. There could be no merge base, though. Older versions of Git used an empty tree as a fake merge base. Newer ones—2.9 or later—demand that you add --allow-unrelated-histories here (and then proceed in the same way). With an empty tree, every file is added, in both non-base commits.

    If there is one merge base, it might be the same as either branch tip. If so, there is no merge to perform. There are two sub-cases here as well, though. There may be nothing to merge, because the merge base is the other commit and the other commit is "behind" (is an ancestor of) the current commit. In this case, Git always does nothing. Or, the other commit may be ahead of (a descendant of) the current commit. In this case, Git normally does a fast-forward operation, unless you specify --no-ff. In both cases (fast-forward or --no-ff), no actual merging happens. Instead, the further-ahead commit gets extracted. It either becomes the current commit (fast-forward merge: whatever branch you are on, it now points to the further-ahead commit), or Git makes a new commit using that commit's tree, and the new commit become the current commit.

    A real merge: merging one merge base with two commits

    We are now at a phase where we have a single merge base commit B, and two commits L (local or left-side, --ours) and R (remote or right-side, --theirs). Now, the two normal (-s recursive and -s resolve) strategies do a pair of git diff --name-status operations with rename detection enabled, to see if there are files in the B-to-L change that change their names, and if there are files in the B-to-R change that change their names. This also finds out if there are newly added files in either L or R, and if files are deleted in either L or R. All of this information is combined to produce file identities, so that Git knows which sets of changes to combine. There may be conflicts here: a file whose path was PB in the base, but is now both PL and PR, has a rename/rename conflict, for instance.

    Any conflicts at this point—I call them high level conflicts—lie outside the domain of file-level merging: they will make Git end this merge process with a conflict, regardless of whatever else occurs. In the meantime, though, we end up with "identified files", as I said above, without quite defining it. Loosely, what this means is that just because some path P got changed, doesn't mean it's a new file. If there was a file base in the base commit B, and it's now called renamed in L but still called base in R, Git will use the new name, but compare B:base with L:renamed and B:base with R:base when Git goes to combine changes at the file level.

    In other words, the file identity we compute at this stage tells us (and Git) which files in B match which files in L and/or R. This identity is not necessarily by path name. It's just usually the case that all three paths match.

    There are a few small tweaks you can insert during this first diff phase:

    • Renormalization (merge.renormalize): you can make Git apply text conversions from .gitattributes and/or core.eol settings. The .gitattributes settings include the ident filter and any smudge and clean filters (though only the smudge direction applies here).

      (I assumed Git did this early, since it may affect rename detection. I have not actually tested this, though, and I just looked through the Git source and it seems to not use this at this stage. So perhaps merge.renormalize does not apply here, even though a smudge filter could radically rewrite a file. Consider a filter-pair that encrypts and decrypts, for instance. This is probably a bug, albeit a small one. Fortunately EOL conversion has no effect at all on similarity index values.)

    • You can set the similarity index for when Git will consider files to be renamed, or disable rename detection entirely. This is the -X find-renames=n extended strategy option, previously called the rename threshold. It is the same as the git diff -M or --find-renames option.

    • Git currently has no way to set the "break" threshold a la git diff -B. This also affects file identity computation, but if you can't set it, it doesn't really matter. (You probably should be able to set it: another minor buglet.)

    Merging individual files

    Now that we have our files identified and have decided which ones match up with which other ones, we finally proceed to the file-merging level. Note that here, if you are using the built-in merge driver, the remaining settable diff options will start to matter.

    Let me quote this bit again, as it's relevant:

    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.

    There are three (not four) files involved at this point, but Git does not create any of them. They are the files from B, L, and R. These three files exist as blob objects in the repository. (If Git is renormalizing files, it does have to create the renormalized ones as blob objects at this point, but then they live in the repository, and Git just sort of pretends they were in the original commits.)

    The next step is quite critical, and it is where the index comes into the picture. The hash IDs of those three blob objects are HB, HL, and HR. Git gets ready to place these three hashes into the index, in slots 1, 2, and 3 respectively, but now uses the rules described in the git read-tree documentation under the 3-Way Merge section:

    • If all three hashes are equal, the file is already merged and nothing happens: the hash goes into slot zero. Even if only the second and third hashes are equal, the file is still already merged: both L and R make the same change with respect to B. The new hash goes into slot zero and the file-merge is complete.
    • If HB = HL and HB ≠ HR, the right side (remote/other/--theirs) file should be the result. This hash goes into slot zero and the file-merge is complete.
    • If HB ≠ HL and HB = HR, the left side (local/--ours) file should be the result. This hash goes into slot zero and the file-merge is complete.
    • This leaves only the case where all three hashes differ. Now the files really do need to be merged. Git places all three hashes into the three index slots.

    There are a few special cases that can apply at this point, all having to do with higher-level conflicts. It's possible that one or two index slots are left empty for some path names, because the index is carefully managed in a way that keeps it synchronized with the work-tree (so that it can play its role as a cache that speeds up Git a lot). But in principle, especially when we are concerned with merge drivers, we can think of this as just "all three slots"—they just may be three slots spread across several names, in the case of renamed files.

    Invoking merge drivers (.gitattributes)

    At this point, we have an actual file-level merge to perform. We have three input files. Their actual contents are stored in the repository, as blob objects. Their hash IDs are stored in the index, in slots 1 through 3 (usually of a single index entry, but in the case of renames, maybe using more than one index entry). We may now:

    • Use git's built in file merge (which is also available as an external command, git merge-file).

      The built in file merge works directly from the index (though if we want to run it via git merge-file we must extract the blobs into the file system). It extracts the files, does its thing to merge them, and optionally—depending on extended-strategy-options -X ours or -X theirs—writes conflict markers as well. It drops its final result into the work-tree, under whatever path name Git chose as the final path name, and is finished.

    • Use a merge driver (via .gitattributes). A merge driver is run with arguments. However, these arguments are constructed by having Git extract the three blob objects to three temporary files.

      The arguments are expanded from whatever we put in as %O, %A, %B, %L, and %P. These argument letters don't quite match what we have been using: %O is the name of the base file, %A is the name of the left-side / local / --ours version, %B is the name of the right-side / other / remote / --theirs version, %L is the conflict-marker-size setting (default 7), and %P is the path that Git wants to use to save the final result in the work-tree.

      Note that %O, %A, and %B are all the names of temporary files that Git created (to hold the blob contents). None of them match %P. Git expects the merge driver to leave the result of the merge in the path %A (which Git will then rename to %P on its own).

    In all cases, the merged file goes into the work-tree, at this point. If the merge went well, the higher-numbered slots in the index are cleaned out: Git, in effect, runs git add on the work-tree file, writing the data into the repository as a blob object, and getting a hash ID that goes into slot zero. If the merge failed with conflicts, the higher-numbered slots remain in place; slot zero is left empty.

    The end result of all this is that the work-tree holds the merged files, perhaps with conflict markers, and the index holds the result of the merge, perhaps with conflicts that should be resolved.

    Using git mergetool

    This works much the same way as a merge driver. Aside from running only after the merge has completed with its results in the index and work-tree, though, the main differences are:

    • git mergetool will make extra copies of files (the .orig files).
    • It knows exactly how to run each known tool, i.e., what arguments to pass to make that tool do something useful. There is no equivalent to a driver %O placeholder, for instance.
    • It can run commands on all the as-yet-unmerged files in some directory.

    In fact, git mergetool is a big shell script: it uses git ls-files -u to find the unmerged index entries, and git checkout-index to extract each stage from the index. It even has special cases for the higher level conflicts such as add/add or rename/delete.

    There's an additional driver shell-script fragment per known tool: look in

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

    to see all the individual tool drivers. These are passed a flag, $base_present, for handling add/add conflicts. (They are sourced, i.e., run with . "$MERGE_TOOLS_DIR/$tool", so that they can override shell functions defined in the script.)

    For unknown tools, you use the shell's variable names $BASE, $LOCAL, and $REMOTE to know where the script has put the three files extracted from the index, and you write your result to $MERGED (which is in fact the work-tree name for the file). The script does this:

    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 )
            }
    }
    

    i.e., evals your tool command in a sub-shell, so that you cannot override things the way the known tools can.

    Recursive merge

    When Git needs to perform a recursive merge ...

    Most of this question is kind of moot at this point. A merge tool never sees this situation at all, because git mergetool is invoked after Git itself has finished the recursive merge and left the result in the index and work-tree. However, merge drivers do get a say here.

    When the -s recursive merge strategy is merging merge-bases to make a new "virtual commit", it invokes another git merge—well, more precisely, just calls itself recursively—on the merge base commits (but see below). This inner git merge knows that it's being invoked recursively, so when it is about to apply a .gitattributes merge driver, it checks the recursive = setting there. This determines whether the merge driver is used again, or some other merge driver is used for the inner merge. For the built-in merge driver, Git turns off the extended strategy options, i.e., neither -X ours nor -X theirs is in effect.

    When an inner merge completes, its result—all the files that would be left in the work-tree, were this not an inner, recursive merge—is actually saved as a real commit. This is true even if there were unresolved conflicts. These unresolved conflicts may even contain conflict markers. Nonetheless, this is the new "virtual merge base" commit, and it is a true commit; it just has no external name by which you can find its commit hash.

    If there are three or more merge bases at this particular level, rather than just two merge bases, this new virtual merge base is now merged with the next remaining merge base, iteratively. Logically, Git could use a divide-and-conquer strategy here: if there were 32 merge bases initially, it could merge them two at a time to produce 16 commits, merge those two at a time to produce 8, and so on. Aside from doing ceil(log2(N)) merges instead of N-1 merges, though, it's not clear that this would buy much: it's already quite rare to have N > 1.

    这篇关于Git:关于合并算法,冲突格式以及与mergetools的相互作用的混淆的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆