解决冲突融合算法

我看一下看起来搞砸了的合并标记。 为了给你这种情况,让我们:

public void methodA() { prepare(); try { doSomething(); } catch(Exception e) { doSomethingElse(); } } 

现在进入合并(我使用SourceTree进行拉动)。 标记看起来像这样:

 <<<<<<<<>>>>>>> 9832432984384398949873ab } 

所以拉取提交的作用是完全删除methodA并添加methodB。

但是你注意到有些线路完全缺失。

根据我对该过程的理解,Git正在尝试所谓的自动合并,如果这个失败并且检测到冲突,则完整合并由标记为“<<>> * CommitID’并准备手动冲突解决方案。

那么为什么它会遗漏一些线条呢。 它看起来更像是一个bug。

我使用的是Windows7,安装的git版本是2.6.2.windows.1 。 虽然最新的版本是2.9,但我想知道是否有任何关于具有如此规模的合并问题的git版本的知识? 这不是我第一次经历这样的事情……

你是正确的:Git对语言一无所知,它的内置合并算法严格基于时间线比较。 你不必使用这种内置的合并算法 ,但大多数人都这样做,因为(a)它大部分只是起作用,而且(b)没有那么多的选择。

请注意,这取决于您的合并策略-s参数); 下面的文本是默认的recursive策略。 resolve策略非常类似于recursive ; octopus策略不仅适用于两次提交; 而ours策略完全不同(并且与-X ours一样)。 您还可以使用.gitattributes和“merge drivers”为特定文件选择其他策略或算法。 并且,这些都不适用于Git决定认为是“二进制”的文件:对于这些文件,它甚至不会尝试合并。 (我不会在这里介绍任何一个,只是默认的recursive策略如何处理文件。)

git merge如何工作(当使用默认的-s recursive

  • 合并从两个提交开始:当前的一个(也称为“我们的”,“本地”和HEAD ),以及一些“其他”提交(也称为“他们的”和“远程”)
  • Merge在这些提交之间找到合并基础
    • 通常,这只是另一个提交:隐含分支1加入的第一个点
    • 在一些特殊情况下(多个合并基础候选者),Git必须发明一个“虚拟合并基础”(但我们在这里会忽略这些情况)
  • 合并运行两个差异: git diff base localgit diff base other
    • 这些已启用重命名检测
    • 您可以自己运行这些相同的差异,以查看合并将看到的内容

您可以将这两个差异视为“我们做了什么”和“他们做了什么”。 合并的目标结合 “我们做了什么”和“他们做了什么”。 差异是基于行的,来自最小的编辑距离算法, 2并且实际上只是Git 猜测我们做了什么,以及他们做了什么。

一个 diff(base-vs-local)的输出告诉Git哪些基本文件对应于哪些本地文件,即如何将当前提交中的名称跟回到基础。 然后,Git可以使用基本名称来查找其他提交中的重命名或删除。 在大多数情况下,我们可以忽略重命名和删除问题,以及新文件创建问题。 请注意,Git版本2.9默认为所有差异启用重命名检测,而不仅仅是合并差异。 (您可以通过将diff.renames配置为true来在早期的Git版本中diff.renames此function;另请参阅diff.renamesgit config设置。)

如果文件只在一侧 (从基础到本地,或从基础到其他)进行更改,Git只需更改这些更改。 当双方都改变文件时,Git只需要进行三向合并。

为了执行三向合并 ,Git基本上遍历两个差异(从基础到本地和从基础到另一个),一次一个“差异大块”,比较变化的区域。 如果每个hunk影响原始基本文件的不同部分 ,Git只会占用该块。 如果某些hunk影响基本文件的一部分,Git会尝试获取该更改的一个副本。

例如,如果本地更改显示“添加一个紧密支撑线”并且远程更改显示“添加(相同位置,相同缩进)紧支撑线”,Git将只采用一个闭合支撑的副本。 如果两者都说“删除一个支撑线”,Git将只删除一次。

只有两个差异冲突 -eg,一个说“添加一个紧密的支撑线缩进12个空格”而另一个说“添加一个紧密的支撑线缩进11个空格”将Git声明冲突。 默认情况下,Git将冲突写入文件,显示两组更改 – 如果将merge.conflictstyle设置为diff3还会显示文件的合并基础版本的代码

任何非冲突的差异,Git适用。 如果存在冲突,Git通常会将文件保留为“冲突合并”状态。 然而,两个-X参数( -X ours-X theirs )修改了这个:使用-X ours Git在冲突中选择“我们的”差异块,然后将这种变化放入,忽略“他们的”变化。 使用-X theirs Git选择“他们的”差异并将这种变化放入,忽略“我们的”变化。 这两个-X参数保证Git毕竟不会声明冲突。

如果Git能够自己解决此文件的所有问题,它会这样做:在工作树和索引/登台区域中获取基本文件,本地更改以及其他更改。

如果Git无法自行解决所有问题,它会使用三个特殊的非零索引槽将文件的基本版本,其他版本和本地版本放入索引/登台区域。 工作树版本始终是“Git能够解决的问题,以及各种可配置项目指示的冲突标记。”

每个索引条目都有四个插槽

诸如foo.java的文件通常在插槽0中暂存。 这意味着它现在可以进入新的提交了。 根据定义,其他三个插槽是空的,因为有一个插槽零条目。

在冲突合并期间,插槽零保留为空,插槽1-3用于保存合并基础版本,“本地”或 – – --ours版本,以及其他或 – --theirs版本。 工作树保存正在进行的合并。

您可以使用git checkout提取任何这些版本,或者使用git checkout -m重新创建合并冲突。 所有成功的git checkout命令都会更新文件的工作树版本。

一些git checkout命令使各个插槽不受干扰。 一些git checkout命令写入插槽0,擦除插槽1-3中的条目,以便文件准备好提交。 (要知道哪些人做了什么,你只需要记住它们。我的错误,在我脑海里,已经有一段时间了。)

在清除所有未合并的插槽之前,您无法运行git commit 。 您可以使用git ls-files --unmerged来查看未合并的插槽,或者使用git status来获得更人性化的版本。 (提示:使用git status 。经常使用它!)

成功合并并不意味着良好的代码

即使git merge成功自动合并所有内容,也不意味着结果是正确的! 当然,当它因冲突而停止时,这也意味着Git无法自动合并所有内容,而不是它自己合并的内容是正确的。 我喜欢将merge.conflictstyle设置为diff3以便我可以看到Git认为基础是什么,然后用合并的两边替换了这个“基本”代码。 通常会发生冲突,因为差异选择了错误的基础 – 例如一些匹配的括号和/或空行 – 而不是因为必须存在实际的冲突。

使用“耐心”差异可以保持基础选择不佳,至少在理论上如此。 我自己没有尝试过这个。 Git 2.9中新的“压缩启发式”很有希望,但我也没有对此进行过实验。

您必须始终检查和/或测试合并的结果。 如果已经提交了合并,你可以编辑文件,构建和测试, git add更正的版本,并使用git commit --amend将之前的(不正确的)合并提交推出,并放入一个不同的提交同父母。 ( git commit --amend--amend部分是虚假广告。它不会更改当前提交本身,因为它不能;相反,它使用与当前提交相同的父ID进行新提交,而不是使用当前提交的ID作为新提交的父级的常规方法。)

您还可以使用--no-commit禁止合并的自动--no-commit 。 在实践中,我发现没有必要这样做:大多数合并主要只是工作,并且快速观察git show -m和/或“它编译并通过unit testing”会遇到问题。 但是,在冲突或--no-commit合并期间,一个简单的git diff将为你提供一个组合差异(与你提交合并之后没有-m git show相同的那种),这可能会有所帮助,或者可能更令人困惑。 您可以运行更具体的git diff命令和/或检查三个(基本,本地,其他)插槽条目,正如Gregg在评论中指出的那样 。

看看Git会看到什么

除了使用diff3作为merge.conflictstyle ,您还可以看到git merge将看到的差异。 你需要做的就是运行两个git diff命令 – 与git merge运行的两个命令相同。

要做到这些,你必须找到 – 或者至少告诉git diff找到 – 合并基础 。 您可以使用git merge-base ,它可以找到(或所有)合并库并将其打印出来:

 $ git merge-base --all HEAD foo 4fb3b9e0570d2fb875a24a037e39bdb2df6c1114 

这表示在当前分支和分支foo ,合并基础是提交4fb3b9e... (并且只有一个这样的合并基础)。 然后我可以运行git diff 4fb3b9e HEADgit diff 4fb3b9e foo 。 但是有一种更简单的方法,只要我可以假设只有一个合并基础:

 $ git diff foo...HEAD # note: three dots 

这告诉git diff (并且只有 git diff )来找到fooHEAD之间的合并基础,然后比较那个commit-that merge base-to commit HEAD 。 和:

 $ git diff HEAD...foo # again, three dots 

做同样的事情,找到HEADfoo之间的合并基础 – “合并基础”是可交换的,所以这些应该是相同的,如7 + 2和2 + 7都是9 – 但这次是合并基于提交foo1

(对于其他命令 – 不是git diff的东西 – 三点语法产生对称差异 :所有提交的集合在任一分支上,但不在两个分支上。对于具有单个合并基础提交的分支,这是“合并基础之后的每个提交,在每个分支上”:换句话说,两个分支的并集,不包括合并基本身和任何早期的提交。对于具有多个合并基础的分支,这会减去所有合并基础。 git diff我们假设只有一个合并基础,而不是减去它和它的祖先,我们用它作为差异的左边或“前面”。)


1在Git中,分支名称标识一个特定的提交,即分支的提示 。 实际上,这就是分支实际工作的方式:分支名称命名一个特定的提交,并且为了向分支分支添加另一个提交,这意味着提交链 -Git进行一个新的提交,其父级是当前的分支提示,然后将分支名称指向新提交。 “branch”一词可以指分支名称或整个提交链; 我们应该根据具体情况找出哪一个。

在任何时候,我们都可以命名一个特定的提交,并将其视为一个分支,通过获取该提交及其所有祖先 :其父级,其父级的父级,依此类推。 当我们在这个过程中遇到一个包含两个或更多父项的合并提交 – 提交时,我们会接受所有父提交,以及他们父母的父母,等等。

2该算法实际上是可选择的。 默认的myers基于Eugene Myers的算法,但Git还有其他一些选项。

在合并中,仅标记包含冲突的更改。

Rev A中的更改和Rev B中的不同更改直接合并。只有Rev A和Rev B在同一位置的更改才会标记为冲突。 通知用户文件中存在冲突并需要解决。

当您解决冲突时,合并的文件已经具有Rev A和Rev B的独立更改,以及冲突部分的冲突标记。