解决冲突融合算法

Mar*_*ten 3 java git merge conflict

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

public void methodA() {
    prepare();
    try {
      doSomething();
    }
    catch(Exception e) {
      doSomethingElse();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

<<<<<<<<< HEAD
    try {
      doSomething();
    }
    catch(Exception e) {
      doSomethingElse();
    }
============================
private void methodB() {
    doOtherStuff();
>>>>>>>> 9832432984384398949873ab
}
Run Code Online (Sandbox Code Playgroud)

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

但是你注意到有些线条完全缺失.

根据我对该过程的理解,Git正在尝试所谓的自动合并,如果这种情况失败并且检测到冲突,则完整合并由标记为"<<<*HEAD"+ +之前的部分表示==== '+ after +'>>>*CommitID'并准备手动冲突解决方案.

那么为什么它会遗漏一些线条呢.它看起来更像是我的错误.

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

tor*_*rek 8

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

请注意,这取决于您的合并策略(-s参数); 以下文字适用于默认recursive策略.该resolve策略非常类似于recursive; 该octopus策略不仅适用于两个提交; 而且ours策略完全不同(并且完全不同-X ours).您还可以使用.gitattributes和"合并驱动程序" 为特定文件选择其他策略或算法.并且,这些都不适用于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到以前的Git版本来自行调试true;另请参阅git config设置diff.renameLimit.)

如果文件只在一侧(从基础到本地,或从基础到其他)更改,Git只需更改这些更改.混帐只有当一个文件时,改变做了三路合并两个方面.

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

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

只有当两个差异冲突 -eg时,一个说"添加一个紧密的支撑线缩进12个空格",另一个说"添加一个紧密的支撑线缩进11个空格"将Git声明冲突.默认情况下,Git将冲突写入文件,显示两组更改 - 如果设置merge.conflictstylediff3,则显示文件的合并基础版本中的代码.

任何非冲突的差异,Git适用.如果存在冲突,Git通常会将文件保留为"冲突合并"状态.但是,这两个-X参数(-X ours-X theirs)修改了这个:-X oursGit在冲突中选择"我们的"差异,然后将这种变化放入,忽略"他们的"变化.随着-X theirsGit选择"他们的"差异并将这种变化放入其中,忽略了"我们的"变化.这两个-X论点保证了Git毕竟不会声明冲突.

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

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

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

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

在冲突合并期间,插槽零保留为空,插槽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将先前(不正确的)合并提交推出,并使用相同的父项进行不同的提交.(该--amend部分git commit --amend是虚假广告它不会改变当前的承诺本身,因为它.不能,而是它与同一个新的提交的父类标识为当前犯,而不是使用当前提交的ID的正常方法作为新提交的父母.)

您还可以禁止自动提交合并--no-commit.在实践中,我发现没有必要这样做:大多数合并主要只是工作,快速的眼球git show -m和/或"它编译和通过单元测试"捕获问题.但是,在冲突或--no-commit合并期间,一个简单的git diff将给你一个组合差异(在你提交合并后git show没有得到的同一种类-m),这可能会有所帮助,或者可能更令人困惑.您可以运行更具体的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
Run Code Online (Sandbox Code Playgroud)

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

$ git diff foo...HEAD   # note: three dots
Run Code Online (Sandbox Code Playgroud)

这告诉git diff(并且 git diff)在foo和之间找到合并基础HEAD,然后比较该提交 - 合并基础 - 提交HEAD.和:

$ git diff HEAD...foo   # again, three dots
Run Code Online (Sandbox Code Playgroud)

做同样的事情,找到HEAD和之间的合并基础foo- "合并基础"是可交换的,所以这些应该是相同的,如7 + 2和2 + 7都是9 - 但这次是合并基础反对提交foo.1

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


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

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

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