解决冲突融合算法
我看一下看起来搞砸了的合并标记。 为了给你这种情况,让我们:
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 local
和git diff base other
- 这些已启用重命名检测
- 您可以自己运行这些相同的差异,以查看合并将看到的内容
您可以将这两个差异视为“我们做了什么”和“他们做了什么”。 合并的目标是结合 “我们做了什么”和“他们做了什么”。 差异是基于行的,来自最小的编辑距离算法, 2并且实际上只是Git 猜测我们做了什么,以及他们做了什么。
第一个 diff(base-vs-local)的输出告诉Git哪些基本文件对应于哪些本地文件,即如何将当前提交中的名称跟回到基础。 然后,Git可以使用基本名称来查找其他提交中的重命名或删除。 在大多数情况下,我们可以忽略重命名和删除问题,以及新文件创建问题。 请注意,Git版本2.9默认为所有差异启用重命名检测,而不仅仅是合并差异。 (您可以通过将diff.renames
配置为true
来在早期的Git版本中diff.renames
此function;另请参阅diff.renames
的git 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 HEAD
和git diff 4fb3b9e foo
。 但是有一种更简单的方法,只要我可以假设只有一个合并基础:
$ git diff foo...HEAD # note: three dots
这告诉git diff
(并且只有 git diff
)来找到foo
和HEAD
之间的合并基础,然后比较那个commit-that merge base-to commit HEAD
。 和:
$ git diff HEAD...foo # again, three dots
做同样的事情,找到HEAD
和foo
之间的合并基础 – “合并基础”是可交换的,所以这些应该是相同的,如7 + 2和2 + 7都是9 – 但这次是合并基于提交foo
。 1
(对于其他命令 – 不是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的独立更改,以及冲突部分的冲突标记。