何时会出现Git合并冲突?

8
我使用git跟踪LaTeX文档的更改。 我倾向于将合作者的反馈保留在一个单独的分支中,并稍后将其合并。 到目前为止,似乎一切顺利地合并,但我想知道何时确切地发生了合并冲突,以便我可以对合并过程产生真正的信任(当然,我不希望出现奇怪的文字)。
StackOverflow上有许多问题似乎在询问同样的事情,但没有一个答案非常具体。 例如这个答案指定如果对相同区域进行了更改,则会发生冲突,但这让我想知道那些区域到底是什么。 它只是对同一行进行的更改,还是考虑了一些上下文?

当两个提交修改同一文件的相同行,并且这些提交属于不同的分支时。在合并这些分支时,Git 将不知道该怎么做,因此您必须自己修复它。 - smarber
@kowsky,举个例子:这个回答指出了冲突发生的原因,你看到这个问题的这一部分了吗? - smarber
1个回答

7
这是一种基于逐行分析的翻译方式,答案有点“是”又有点“不是”,即上下文确实很重要,但它的重要性却很棘手。初看可能会高估或低估它的重要性。
可以先浏览相关问题的此回答,以了解背景信息。接下来,我们假设base是(唯一的)合并基础(例如,通过标记特定提交命名为base,如git tag base $(git merge-base HEAD other)),而HEAD是我们在b1分支上的提交,某个其他分支b2则命名为另一个提交。
接下来,我们查看两个差异:
git diff base HEAD
git diff base b2

如果我们发现文件F的三个版本都不同(因此在两个输出中均出现,且更改不同),那么我们必须本质上逐个差异块进行处理。在差异块重叠但产生不同更改的情况下,Git会声明冲突。但是——这似乎是您的问题——“产生不同更改”到底意味着什么呢?
我认为最好通过示例来说明。例如:
$ git checkout b1
[checkout messages here - but I was already on b1]
$ git diff base HEAD
diff --git a/basefile b/basefile
index df781c1..e4f9e4b 100644
--- a/basefile
+++ b/basefile
@@ -4,6 +4,7 @@
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions
 # are met:
+# added line in b1
 # 1. Redistributions of source code must retain the above copyright
 #    notice, this list of conditions and the following disclaimer.
 # 2. Redistributions in binary form must reproduce the above copyright

并且:

$ git diff base b2
diff --git a/basefile b/basefile
index df781c1..c96620e 100644
--- a/basefile
+++ b/basefile
@@ -4,7 +4,6 @@
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions
 # are met:
-# 1. Redistributions of source code must retain the above copyright
 #    notice, this list of conditions and the following disclaimer.
 # 2. Redistributions in binary form must reproduce the above copyright
 #    notice, this list of conditions and the following disclaimer in the

请注意,虽然这些更改没有涉及到同一行,但从某种意义上来说,它们也确实会涉及到同一行。我添加了第7行(将旧的第7行推到第8行),并删除了旧的第7行。这些显然是“同一”行。因此:
$ git merge b2
Auto-merging basefile
CONFLICT (content): Merge conflict in basefile
Automatic merge failed; fix conflicts and then commit the result.

让我们放弃这次合并,转而考虑分支b3的最新版本(在我的设置中,b1b3的合并基础与b1b2的合并基础相同)。

$ git merge --abort
$ git diff base b3
diff --git a/basefile b/basefile
index df781c1..e2b8567 100644
--- a/basefile
+++ b/basefile
@@ -5,7 +5,6 @@
 # modification, are permitted provided that the following conditions
 # are met:
 # 1. Redistributions of source code must retain the above copyright
-#    notice, this list of conditions and the following disclaimer.
 # 2. Redistributions in binary form must reproduce the above copyright
 #    notice, this list of conditions and the following disclaimer in the
 #    documentation and/or other materials provided with the distribution.
 $ git merge --no-edit b3
Auto-merging basefile
Merge made by the 'recursive' strategy.
 basefile | 1 -
 1 file changed, 1 deletion(-)

这次没有冲突,即使两个 diff hunks 涉及同一区域。第二个 diff 删除了一行没有“touching”添加的行,所以 Git 认为这是安全的。
如果你继续尝试,以同样的方式,你会发现哪些看似重叠的更改成功地合并在一起,哪些会导致冲突。显然,直接重叠的更改,例如,两者都删除原始行 42 并插入不同的新行 42,将会冲突。但所有更改都总是表示为“删除某些现有行(虽然可能没有)”,后跟“添加某些新行(虽然可能没有)”。一个更改 - 即使只是更改、添加或删除行内的一个单词 - 也会删除一个非零数量的现有行并添加一个非零数量的新行。纯删除(一个或多个完整行)添加零行,而纯插入删除零行。最终,问题变成:“我们和他们的更改是否都触及了相同的行号?”上下文几乎变得无关紧要,除了当删除零行或插入零行时,上下文本身“是”行,在某种意义上。(如果这个说法让人难以理解,那是我的错。;-))
(还要记住,如果你在工作时修改“到目前为止合并的”文件,你必须在查看一个更改是否触及“相同”的行时使用原始基础文件的行号。由于“我们”和“他们”都有相同的基本版本,这是一个容易的捷径。)

三方合并不是补丁

请注意,这与应用补丁不同,后者在没有公共基础版本的情况下完成。在补丁的情况下,上下文被更广泛地使用:diff hunk header 提供了搜索上下文的位置,但由于它可能被应用于文件的不同版本,上下文允许我们(和 Git)在不同的行上进行相同的更改,只要上下文仍然匹配。 patch 工具在这里使用不同的算法(一个“最大模糊”因素,向上 / 向下查找那么多行)。Git 不会做模糊因素;如果需要,它将一直搜索到文件的开头或结尾。但是,在决定上下文不匹配之前,它确实有调整空格的选项。

使用git apply应用补丁时,您可以添加-3--3way以允许Git读取index行,这些行提供了文件blob的部分或完整哈希ID。左侧的哈希ID是文件先前版本的哈希ID:请注意,在上面的所有差异中,“basefile”的“基础”版本具有ID df781c1。如果Git能够从该ID找到唯一的blob,则可以假装那是合并基础,并将一个合并基础与HEAD进行比较,将补丁本身视为另一个差异,并通过这种方式进行三方合并。这有时使得git apply能够成功,而patch则会失败。


非常感谢您详细的回答。如果我理解正确,相邻行的更改通常会导致冲突。此外,事情是纯粹基于逐行评估的,因此测试一些边缘情况不应该花费太多时间。您是否知道是否有选项可以调整上下文以考虑相邻的两行?再次感谢。 - Octaviour
在所有现有的合并策略和驱动程序中,都没有这样的选项。你可以编写自己的策略,但这样做并不简单;你可以编写自己的驱动程序,这更为直接(参见同一问题),但仍然不是非常容易的。 - torek
这很有帮助,但在我弄清HEAD指向b1和_other_实际上是示例开始时的b2之前,我有点困惑。这正确吗? - clapas
1
@clapas:是的,我可能应该在示例命令中的git merge b2之前添加一个git checkout b1 - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接