合并git并行分支中的冲突

Eha*_*hab 0 git parallel-processing merge branch

我正在尝试用git实现一个场景.我从一个带有四行的文本文件开始,然后制作了4个分支,每个分支在文本文件中更改了一行,它们并行工作,每个分支都有原始文件的副本,如图所示. 在此输入图像描述 合并分支时,第一次合并总是这样成功:

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

但是我在后来的合并中遇到了合并冲突.

Automatic merge failed; fix conflicts and then commit the result.
Run Code Online (Sandbox Code Playgroud)

有没有一种方法可以实现这种情况而不会出现合并冲突?

jth*_*ill 5

\n

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

\n
\n\n

长话短说:博士:不。

\n\n

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

\n\n

没有人找到一种有效的自动解决这些重叠的方法。我们有大量的现有历史可以使用\xc2\xb8,所以你可以尝试任何你想出的方法来合并linux或vim或libreoffice或git本身,或者你有什么,它不会是每个第一次看到问题的人都会错过一些东西,但我对“无法完成”的答案下了很大的赌注。

\n\n

糟糕的自动合并的成本非常高,这是痛苦/收益指标中的“痛苦”。好处很简单:许多冲突,比如你的冲突,对于人类来说很容易正确解决。熟练使用工具,简单的案例只需几秒钟。所以 Git 非常谨慎。

\n


tor*_*rek 5

添加到jthill的答案(这是正确的,我已经赞成它):你需要意识到你的第一次合并根本不是合并.这就是为什么Git说:

Fast-forward
Run Code Online (Sandbox Code Playgroud)

合并过程,正如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
Run Code Online (Sandbox Code Playgroud)

(如果您不熟悉ed编辑器,它是一个相当小的纯文本编辑器,它在打开时打印输入文件中的字节数,并具有以下形式的命令:line-number(s) 操作.所以1s/$/ one/表示"替换第1行的结尾 one.该w命令将文件写回,然后q退出编辑器."

我们没有创造额外的分支机构br2,br3以及br4还没有,但让我们继续这样做,无论如何,创造他们,使他们指向承诺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
Run Code Online (Sandbox Code Playgroud)

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

b1a22ca   <-- master (HEAD), br2, br3, br4
    \
   b31f04a   <-- br1
Run Code Online (Sandbox Code Playgroud)

也就是说,分支名称br1表示它的提示是b31f04a,而分支名称master和其他三个br都表示它们的提示是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(-)
Run Code Online (Sandbox Code Playgroud)

并看看如何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
Run Code Online (Sandbox Code Playgroud)

在我首选的水平图形绘图中 - 新的提交是在右边,而不是在上面 - 我们有:

   805ea58   <-- br2 (HEAD)
    /
b1a22ca   <-- master, br3, br4
    \
   b31f04a   <-- br1
Run Code Online (Sandbox Code Playgroud)

如果我们现在运行git checkout master && git merge br1,Git将为我们找到合并基础提交.这是"最近"犯这是在两个分支br1 master.

请注意,此时,commit b31f04a仅在on br1,并且commit 805ea58仅在on br2,但commit b1a22ca在每个分支上. 这是Git中的关键:分支"包含"提交,任何给定的提交可以同时在许多分支上.

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

  • 从合并基础b1a22ca到提示的变化master:b1a22ca;
  • 从合并基础b1a22ca到提示的变化br1:b31f04a.

不过b1a22ca b1a22ca.这里不可能有任何变化!

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

   805ea58   <-- br2
    /
b1a22ca   <-- br3, br4
    \
   b31f04a   <-- master (HEAD), br1
Run Code Online (Sandbox Code Playgroud)

在此操作期间,不会添加任何提交:唯一改变的是您当前的提交现在b31f04a而不是b1a22ca分支标签master指向的b31f04a.(HEAD移动的单词是master因为HEAD"附加到" master.Git HEAD -> mastergit log输出中显示此内容.)

如果你愿意,你可以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
Run Code Online (Sandbox Code Playgroud)

第二次合并是冲突的

然而,当我们进入第二次合并时,情况确实不同.这次,master(now b31f04a)和br2(805ea58)之间共享的第一个提交是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
Run Code Online (Sandbox Code Playgroud)

然后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
Run Code Online (Sandbox Code Playgroud)

Git现在将这两个变化结合起来.但在Git的眼中,两个变化重叠:第一个接触第1行,第二个接触第2行,第1行和第2行相互接触:这是重叠.这两个更改不相同,因此无法将其折叠为更改两行的单个更改.因此,更改是合并冲突,这是您所看到的.

对于剩余的合并,一旦分支br3br4具有适当的提示提交,也会发生同样的事情.


Mar*_*ger 5

根据您在评论中提出的问题,这里有一些补充说明.

首先,你有一个起始状态,每行只有数字; 我们称之为提交A.这就是master积分.

A <--(master)
            ^HEAD
Run Code Online (Sandbox Code Playgroud)

那你有四个分支.每个分支都会更改不同的代码行.我们会继续HEADmaster在为合并做准备.

A <-(master)
|\         ^HEAD
| B <-(branch1)
|\
| C <-(branch2)
|\
| D <-(branch3)
 \
  E <-(branch4)
Run Code Online (Sandbox Code Playgroud)

所以你做第一次合并

git merge branch1
Run Code Online (Sandbox Code Playgroud)

现在git将在"你正在合并"(branch1= B)和HEAD(master= A)之间寻找合并基础.合并基础通常(并且在这种情况下)仅是从两侧(即从masterbranch1)可到达的最近提交.当然A可以从master,因为是父母,A也可以到达.branch1AB

所以我们确定了3个版本."我们的"(我们正在合并的变化)是A."他们的"(改变的地方)是B."base"("我们的"和"他们的"之间的合并基础)A,恰好与"我们的"相同.

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

事实上,默认情况下git在这里采用了一个捷径.一旦它意识到"我们的"是"他们的"的祖先,它就知道它可以做一个"快进"而不是真正的合并.它可以移动master到哪里branch1,而不是合并任何东西.(您可以通过为命令提供--no-ff选项来阻止此操作merge.)

但问题是,即使没有快进,这也不会发生冲突,因为根据定义,冲突意味着"我们"改变了"他们"改变了相同的代码块,在这种情况下,"我们"没有改变任何东西.

所以现在快进之后你就拥有了

A -- B <-(branch1)(master)
|\                       ^HEAD
| C <-(branch2)
|\
| D <-(branch3)
 \
  E <-(branch4)
Run Code Online (Sandbox Code Playgroud)

注意,master不再的祖先C,DE; 第一个合并移动master到其他分支无法访问的提交.真正的合并也是如此; 在这种情况下,master将转移到新的"合并提交".由于快进,它只是移动到B,但效果是一样的.

所以现在你说

git merge branch2
Run Code Online (Sandbox Code Playgroud)

当我们寻找"我们的"(master= B)和"他们的"(branch2= C)之间的合并基础时,我们会发现A.所以再次基数是A,但这次"我们的变化"是"第1行从'01'变为'01''"."他们的变化"是"第2行从'02'变为'02两'".

由于不满足快进条件,我们必须使用这两个补丁进行完全合并.而你可能会认为"我可以将这些补丁组合在一起而不会发生冲突",导致"第1行从'01'变为'01'并且第2行从'02'变为'02''." 如果补丁严格逐行比较,那就是真的; 但他们不是.

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

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

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

现在,如果您碰巧是源控制某些特殊类型的文件,您对此非常有信心(实际上,您可能必须绝对确定),对一行的更改与下一行的更改无关,那么您可以编写拥有合并驱动程序 这不是微不足道的,请记住,当添加行时,删除行时,当文件的两个版本比您在示例中显示的更偏离时,您必须知道要做什么,等等.

接受有时你必须解决冲突的可能性更大.如果你想编写模拟非冲突合并的"玩具"测试,最简单的方法是让每个分支改变一个不同的文件; 但是在一个文件中,变化不能太紧密,否则冲突就会发生.


[1] 评论的更新 - 根据torek的说法,即使只有一条干预线也没有人改变就足够了.我不认为这是正确的,但作为一项规则,如果托雷克告诉我一些我认为错误的事情,我会进行另一项测试; 根据那个测试,我会同意他的观点.事实上,例如,你似乎可以合并branch1branch3不会发生冲突.