【问题标题】:Merge conflict in git parallel branches在 git 并行分支中合并冲突
【发布时间】:2017-12-27 16:06:32
【问题描述】:

我正在尝试在 git 中实现一个场景。 我从一个有四行的文本文件开始,然后创建了 4 个分支,每个分支在文本文件中更改了一行,并且它们与每个具有原始文件副本的分支并行工作,如图所示。 当我合并分支时,第一次合并总是这样成功:

Updating 0b18c93..274ba8c
Fast-forward
 t.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

但我在以后的合并中遇到合并冲突。

Automatic merge failed; fix conflicts and then commit the result.

有没有一种方法可以在不发生合并冲突的情况下实现这种情况?

【问题讨论】:

  • @TimBiegeleisen - 这是不正确的。 “他们没有更改 X 行”与“我们确实更改了 X 行”并不冲突。这些冲突的原因是“我们”更改了一行“太接近”了“他们”更改的行,而 git 将其解释为对同一块代码的不同更改。

标签: git merge parallel-processing branch


【解决方案1】:

要添加到jthill's answer(这是正确的,我已经赞成):您需要意识到您的第一次合并根本不是合并。这就是为什么 Git 说:

Fast-forward

合并过程,如jthill所说:

找到(并且在某些情况下 Git 的合并实际上会构建)公共基础并将该基础的差异与每个分支提示进行比较。重叠范围的任何差异都构成冲突。

但是对于第一个git merge 命令,公共基础分支提示之一。

第一个是免费的

让我们看看是怎么回事:

$ mkdir tt; cd tt; git init
Initialized empty Git repository in ...
$ cat << END > myfile.txt
01
02
03
04
END
$ git add myfile.txt
$ git commit -m initial
[master (root-commit) b1a22ca] initial
 1 file changed, 4 insertions(+)
 create mode 100644 myfile.txt
$ git checkout -b br1
Switched to a new branch 'br1'
$ ed myfile.txt
12
1s/$/ one/
w
16
q
$ git add myfile.txt
$ git commit -m 'change line 1'
[br1 b31f04a] change line 1
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --all --decorate --oneline --graph
* b31f04a (HEAD -> br1) change line 1
* b1a22ca (master) initial

(如果您不熟悉ed 编辑器,它是一个相当简单的纯文本编辑器,在启动时会打印输入文件中的字节数,并具有以下形式的命令:行号 操作。所以1s/$/ one/ 的意思是“用 one 替换第1 行的结尾。w 命令将文件写回,q退出编辑器。)

我们还没有必要创建额外的分支br2br3br4,但我们还是继续这样做,创建它们以便它们指向提交b1a22ca

$ git checkout master
Switched to branch 'master'
$ git branch br2 && git branch br3 && git branch br4
$ git log --all --decorate --oneline --graph
* b31f04a (br1) change line 1
* b1a22ca (HEAD -> master, br4, br3, br2) initial

此时,如果我们像 Git 那样水平地而不是垂直地绘制它,我们有一个看起来像这样的图表:

b1a22ca   <-- master (HEAD), br2, br3, br4
    \
   b31f04a   <-- br1

即分支名br1表示它的tip commit是b31f04a,而分支名master和其他三个brs都表示他们的tip commit是b1a22ca

现在让我们在 br2 上创建一个新的提交:

$ git checkout br2
Switched to branch 'br2'
$ ed myfile.txt
12
1,$p
01
02
03
04
2s/$/ two/
w
16
q
$ git add myfile.txt 
$ git commit -m 'change line 2'
[br2 805ea58] change line 2
 1 file changed, 1 insertion(+), 1 deletion(-)

看看git log --all --decorate --oneline --graph是怎么画的:

$ git log --all --decorate --oneline --graph
* 805ea58 (HEAD -> br2) change line 2
| * b31f04a (br1) change line 1
|/  
* b1a22ca (master, br4, br3) initial

在我首选的水平图表中——较新的提交在右边,而不是在上面——我们有:

   805ea58   <-- br2 (HEAD)
    /
b1a22ca   <-- master, br3, br4
    \
   b31f04a   <-- br1

如果我们现在运行 git checkout master &amp;&amp; git merge br1,Git 将为我们定位 merge base 提交。这是 both 分支 br1 master 上的“最近”提交。

请注意,此时,提交b31f04a 仅在br1 上,而提交805ea58 仅在br2 上,但提交b1a22ca 在每个分支上。 em> 这是 Git 中的关键:分支“包含”提交,任何给定的提交都可以同时在多个分支上。

找到合并基础后,Git 现在必须合并两组更改:

  • 合并基b1a22camaster的尖端的变化:b1a22ca;
  • 从合并基础b1a22cabr1 的尖端的变化:b31f04a

但是b1a22ca b1a22ca。这里不可能有任何变化!

在这种情况下,Git 默认做的是快进。快进根本不是合并!这相当于切换到另一个提交,在本例中为 b31f04a,并将名称 master 向前拖动以指向该提交:

   805ea58   <-- br2
    /
b1a22ca   <-- br3, br4
    \
   b31f04a   <-- master (HEAD), br1

在此操作期间没有添加任何提交:唯一改变的是您的当前提交现在是b31f04a而不是b1a22ca,并且分支标签master 指向b31f04a。 (HEAD 这个词与 master 一起移动,因为 HEAD 被“附加到”master。Git 在 git log 输出中将其显示为 HEAD -&gt; master。)

如果您愿意,可以运行 git merge --no-ff br1。这迫使 Git 进行真正的合并。但是,当 Git 将 b1a22ca 与自身进行比较时,仍然没有发现任何变化,因此没有合并冲突。如果你这样做,你会得到一个新的提交,有两个父母。你没有,所以我就快进吧:

$ git checkout master
Switched to branch 'master'
$ git merge br1
Updating b1a22ca..b31f04a
Fast-forward
 myfile.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --all --decorate --oneline --graph
* 805ea58 (br2) change line 2
| * b31f04a (HEAD -> master, br1) change line 1
|/  
* b1a22ca (br4, br3) initial

第二次合并冲突

但是,当我们进行 second 合并时,情况确实有所不同。这一次,master(现在是b31f04a)和br2805ea58)之间共享的第一个提交是b1a22ca。所以 Git 会运行:

$ git diff --find-renames b1a22ca b31f04a   # what happened on master
diff --git a/myfile.txt b/myfile.txt
index cb3ff6d..8bc2250 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1,4 +1,4 @@
-01
+01 one
 02
 03
 04

然后 Git 将运行:

$ git diff --find-renames b1a22ca 805ea58   # what happened on br2
diff --git a/myfile.txt b/myfile.txt
index cb3ff6d..8bfe146 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1,4 +1,4 @@
 01
-02
+02 two
 03
 04

Git 现在去结合这两个变化。但在 Git 眼里,这两个变化是重叠的:第一个接触到第 1 行,第二个接触到第 2 行,第 1 行和第 2 行相互接触:这是一个重叠。这两个更改相同,因此不能将它们折叠为更改两条线的单个更改。因此,更改是合并冲突,这就是您所看到的。

一旦分支 br3br4 有适当的提示提交,其余的合并也会发生同样的事情。

【讨论】:

    【解决方案2】:

    根据您在 cmets 中提出的问题,这里有一些额外的解释。

    首先你有一个开始状态,每行只有数字;我们将该提交称为A。这就是master 所指的地方。

    A <--(master)
                ^HEAD
    

    那么你有四个分支。每个分支都会更改不同的代码行。我们会将HEAD 保留在master 为合并做准备。

    A <-(master)
    |\         ^HEAD
    | B <-(branch1)
    |\
    | C <-(branch2)
    |\
    | D <-(branch3)
     \
      E <-(branch4)
    

    所以你做了你的第一次合并

    git merge branch1
    

    现在 git 将在“你正在合并的内容”(branch1 = B) 和 HEAD (master = A) 之间寻找一个合并基础。合并基础通常(在这种情况下)只是双方都可以访问的最新提交(即来自masterbranch1)。当然A 可以从master 访问,A 也可以从branch1 访问,因为AB 的父级。

    所以我们确定了 3 个版本。 “我们的”(我们正在合并的更改)是A。 “他们的”(正在合并更改的地方)是B。 “base”(“ours”和“theirs”之间的合并基)是A,恰好与“ours”相同。

    如果我们进行了完全合并,下一步就是将“ours”与“base”进行比较,并将“theirs”与“base”进行比较以创建两个补丁。然后我们结合这些补丁。只要补丁不影响相同的代码块,我们就可以将它们简单地组合起来(“我们将块 A 从 xxx 更改为 yyy;他们将块 B 从 zzz 更改为 www;所以合并的补丁可以完成这两件事” )。这大致就是非冲突合并的工作方式。在这种情况下,它非常简单;因为“我们的”和“基地”是同一个东西,“我们什么都没改变”;所以合并后的补丁等于“base”和“theirs”之间的补丁。

    事实上,默认情况下 git 会在此处使用快捷方式。一旦它意识到“我们的”是“他们的”的祖先,它就知道它可以进行“快进”而不是真正的合并。它可以将master 移动到branch1 所在的位置,而根本不合并任何东西。 (您可以通过为merge 命令提供--no-ff 选项来防止这种情况发生。)

    但关键是,即使没有快进,这也不会发生冲突,因为定义上的冲突意味着“我们”更改了“他们”更改的同一块代码,在这种情况下“我们”没有改变任何东西。

    所以现在在快进之后

    A -- B <-(branch1)(master)
    |\                       ^HEAD
    | C <-(branch2)
    |\
    | D <-(branch3)
     \
      E <-(branch4)
    

    注意master 不再CDE 的祖先;第一次合并将master 移至其他分支无法访问的提交。真正的合并也是如此;在这种情况下,master 将移至新的“合并提交”。因为快进,只是移动到B,但效果是一样的。

    所以现在你说

    git merge branch2
    

    当我们在“我们的”(master = B)和“他们的”(branch2 = C)之间寻找合并基础时,我们会找到A。所以基数再次是A,但这次“我们的更改”是“第 1 行从 '01' 更改为 '01 one'”。 “他们的变化”是“第 2 行从 '02' 更改为 '02 二'”。

    由于不满足快进的条件,我们必须使用这两个补丁进行完全合并。你可能会认为“我可以无冲突地组合这些补丁”导致“第 1 行从 '01' 更改为 '01 one' 并且第 2 行更改 frmo '02' 到 '02 two'”。如果补丁严格逐行比较,那将是正确的;但他们不是。

    他们不这样做是有原因的。 git 的目的是维护程序源代码的版本。可能性是第 1 行的代码与第 2 行的代码相关。如果更改没有相距可观的距离(不,我不知道该距离是多少 [1]),它们被认为会影响同一个大块头。

    因此,与其假设这些更改是独立正确的并且不会相互干扰,git 将其标记为冲突并要求您决定最终结果应该是什么。即使您最终不得不不必要地解决 100 个此类冲突,成本仍然低于 git 假设它知道该做什么并且是错误的一个实例。

    第 3 次和第 4 次合并的分析是一样的;在每种情况下,您都有两个影响相同代码块的补丁,因此需要手动干预。

    现在,如果您碰巧对某种特殊类型的文件进行源代码控制,并且您非常确信(实际上,您可能必须绝对确定)对一行的更改独立于更改到下一行,那么您可以编写自己的合并驱动程序。这不是微不足道的,请记住,您必须知道在添加行、删除行、文件的两个版本比您在示例中显示的偏差更大时要做什么等。

    接受有时你必须解决冲突的可能性会更好。如果您想编写模拟非冲突合并的“玩具”测试,最简单的方法是让每个分支更改不同的文件;但在一个文件中,更改不能靠得太近,否则会发生冲突。


    [1] 来自评论的更新 - 根据 torek 的说法,即使有一个没有人更改的中间行就足够了。我不认为那是正确的,但作为一项规则,如果 torek 告诉我一些我认为是错误的事情,我会运行另一个测试;根据那个测试,我会同意他的看法。所以事实上,例如,您似乎可以合并branch1branch3 而不会发生冲突。

    【讨论】:

    • 一个很好的答案,对 git 如何合并有一些很好的了解。
    • 应用差异数据所需的“距离”只是一行。如果一个大块共享一个边缘或任何实际内容,则一个大块“接触”另一个大块,“文件开头”和“文件结尾”也充当边缘。
    【解决方案3】:

    有没有一种方法可以在不发生合并冲突的情况下实现这种情况?

    tl;dr:没有。

    Merge 找到(并且在某些情况下 Git 的合并实际上会构建)公共基础,并将该基础的差异与每个分支提示进行比较。对重叠范围的任何更改差异都构成冲突。

    没有人找到自动解决这些重叠的有效方法。我们有大量的现有历史可供使用,因此您可以尝试在 linux 或 vim 或 libreoffice 或 git 本身的合并中提出的任何方法,或者您有什么,这不会是每个人第一次曾经看过问题的人错过了一些东西,但我会在“无法完成”的答案上打赌。

    不良自动合并的成本非常高,这是痛苦/收益指标中的“痛苦”。好处只是方便:许多冲突,就像你的一样,很容易被人类正确解决。熟练使用这些工具,简单的案例只需几秒钟。所以 Git 是相当谨慎的。

    【讨论】:

    • 那么,为什么第一次合并成功了?
    • 您可以看到 git 在合并后使用git diff $mergecommit^1 $mergecommit^2 查看的差异。我怀疑第一次合并只是将 branch1 合并到 master 中,master 上没有任何变化。否则,我会说这是更改的合并,它们之间的距离足以使大块不重叠。
    • 啊:你的第一次合并是快进:根本没有合并¸git merge branch1只是移动了master标签。
    • @jhill 说的是真的,但即使它没有实现为快进,第一次合并也不会发生冲突,因为 "我们的”合并的一方,冲突需要双方都进行更改。
    • 是的,我的第二条评论应该指出我这样做只是因为差异不起作用,因为根本没有第二个父母。
    【解决方案4】:

    我发现在分支 1 和 2 合并到 master 后,结帐到分支 3 并从 master 中提取并解决冲突然后推送到 master,然后分支 4 可以从 master 中提取所有已解决的冲突并合并然后推送掌握。

    【讨论】:

      【解决方案5】:

      有没有一种方法可以在不发生合并冲突的情况下实现这种情况?

      否(如其他答案中所述),但可以使用正确的工具自动解决它们。

      以下内容基于Torek's answer 中的所有命令。

      $ git merge br2
      Auto-merging myfile.txt
      CONFLICT (content): Merge conflict in myfile.txt
      Automatic merge failed; fix conflicts and then commit the result.
      $ git status
      On branch master
      You have unmerged paths.
        (fix conflicts and run "git commit")
        (use "git merge --abort" to abort the merge)
      
      Unmerged paths:
        (use "git add <file>..." to mark resolution)
      
              both modified:   myfile.txt
      
      no changes added to commit (use "git add" and/or "git commit -a")
      $ git ls-files -u
      100644 cb3ff6d73dab0ac586b87f6c5f222e37b85dd32d 1       myfile.txt
      100644 8bc2250b70fd06d951fd6ae1ea7ebf0b421ce2e3 2       myfile.txt
      100644 8bfe14645e97d71dd4036b5a51d73e29020cba55 3       myfile.txt
      $
      

      所以合并 br2 会导致冲突(您可以检查 ls-files 涉及的版本)。

      当相邻的两行被修改时,Git 的内部合并处理失败,而如果使用 KDiff3 进行合并,则不会(这通常是您想要的,但这样做不正确的风险比 for行距较远,因此需要权衡)。

      通过 mergetool 命令使用 KDiff3 很简单;

      $ git config merge.tool kdiff3
      $ git mergetool
      Merging:
      myfile.txt
      
      Normal merge conflict for 'myfile.txt':
        {local}: modified file
        {remote}: modified file
      $ git status
      On branch master
      All conflicts fixed but you are still merging.
        (use "git commit" to conclude merge)
      
      Changes to be committed:
      
              modified:   myfile.txt
      
      Untracked files:
      $ ..." to include in what will be committed)
      
              myfile.txt.orig
      
      $ rm myfile.txt.orig
      $ git diff --cached
      diff --git a/myfile.txt b/myfile.txt
      index 8bc2250..73e9fe0 100644
      --- a/myfile.txt
      +++ b/myfile.txt
      @@ -1,4 +1,4 @@
       01 one
      -02
      +02 two
       03
       04
      $ 
      

      KDiff3 是一个graphical 3-way merge 程序,但在这种情况下,当它自动解决问题时,它没有显示窗口。如果有必须手动解决的冲突,它就会有。

      【讨论】: