我试图在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.
是否有方法可以实现此方案而不会发生合并冲突?
答案 0 :(得分:5)
要添加到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
编辑器,它是一个相当小的纯文本编辑器,它在打开时打印输入文件中的字节数,并具有以下形式的命令: em> line-number(s) operation 。所以1s/$/ one/
表示“用 one
替换第1行的结尾。w
命令写入文件返回,q
退出编辑器。)
我们还没有创建额外的分支br2
,br3
和br4
,但是让我们继续这样做,然后创建它们以便它们指向提交{ {1}}:
b1a22ca
此时,我们有一个看起来像这样的图表,如果我们水平绘制它而不是像Git那样垂直绘制:
$ 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
也就是说,分支名称b1a22ca <-- master (HEAD), br2, br3, br4
\
b31f04a <-- br1
表示其提示提交为br1
,而分支名称b31f04a
和其他三个master
都表示他们的提示提交是br
。
现在让我们继续在br2上创建一个新的提交:
b1a22ca
并了解$ 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将为我们找到合并基础提交。这是两个分支git checkout master && git merge br1
和 br1
上的“最近”提交。
请注意,此时,提交master
仅在b31f04a
上,提交br1
仅在805ea58
上,但提交{{} 1}}在每个分支上。 这是Git中的一个关键事项:分支“包含”提交,任何给定的提交可以同时在许多分支上。
找到合并基础后,Git现在必须结合两组更改:
br2
到b1a22ca
的提示的更改:b1a22ca
; master
到b1a22ca
的提示的更改:b1a22ca
。但br1
是 b31f04a
。这里不可能有任何变化!
对于这种情况,Git默认做的是执行快进。快进不是合并!它只相当于切换到另一个提交,在本例中为b1a22ca
,并将名称b1a22ca
向前拖动到指向该提交:
b31f04a
在此操作期间,不添加新的提交:唯一改变的是您的当前提交现在是master
而不是{{1} },并且分支标签 805ea58 <-- br2
/
b1a22ca <-- br3, br4
\
b31f04a <-- master (HEAD), br1
指向b31f04a
。 (单词b1a22ca
与master
一起移动,因为b31f04a
已“附加到”HEAD
。Git在master
输出中将其显示为HEAD
。)
如果您愿意,可以改为运行master
。这迫使Git进行真正的合并。但是,当Git将HEAD -> master
与自身进行比较时,仍然没有找到任何更改,因此没有合并冲突。如果你这样做,你会得到一个新的提交,有两个父母。你没有,所以我只是快速前进:
git log
但是,当我们进入第二次合并时,情况确实不同。这次,git merge --no-ff br1
(现在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
(master
)之间共享的第一个提交是b31f04a
。所以Git会运行:
br2
然后Git将运行:
805ea58
Git现在转到合并这两个变化。但是在Git的眼中,两个变化重叠:第一个接触第1行,第二个接触第2行,第1行和第2行相互接触:这是重叠。这两个更改不是相同,因此它们不能折叠为更改两行的单个更改。因此,更改是合并冲突,这是您所看到的。
一旦分支b1a22ca
和$ 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
有适当的提示提交,其余合并也会发生同样的事情。
答案 1 :(得分:5)
根据您在评论中提出的问题,这里有一些补充说明。
首先你有一个起始状态,每行只有数字;我们会调用该提交A
。这就是master
指向的地方。
A <--(master)
^HEAD
然后你有四个分支。每个分支都会更改不同的代码行。我们将HEAD
保留在master
以准备合并。
A <-(master)
|\ ^HEAD
| B <-(branch1)
|\
| C <-(branch2)
|\
| D <-(branch3)
\
E <-(branch4)
所以你做第一次合并
git merge branch1
现在git将寻找&#34;你正在合并的&#34;之间的合并基础。 (branch1
= B
)和HEAD
(master
= A
)。合并基础通常(在这种情况下)只是从双方可以到达的最近提交(即来自master
和branch1
)。当然A
可以从master
到达,A
也可以从branch1
到达,因为A
是B
的父级。
所以我们确定了3个版本。 &#34;我们&#34; (我们将更改合并到的内容)是A
。 &#34;他们&#34; (正在合并更改的地方)是B
。 &#34;基础&#34; (&#34;我们的&#34;和#34;他们的&#34;)之间的合并基础是A
,恰好与&#34;我们的&#34;相同。
如果我们完全合并,下一步就是比较我们的&#34;到&#34;基地&#34;并比较他们的#34;到&#34;基地&#34;创建两个补丁。然后我们组合这些补丁。只要这些补丁不会影响相同的代码,我们就可以将它们简单地结合起来(&#34;我们将大块A从xxx改为yyy;他们将大块B从zzz改为www;所以合并的补丁确实如此这些都是&#34;)。这大概是非冲突合并的工作方式。在这种情况下,它非常简单;因为&#34;我们的&#34;和&#34; base&#34;是一回事,&#34;我们没有改变任何事情&#34 ;;所以合并的补丁等于&#34; base&#34;之间的补丁。和他们的&#34;。
事实上,默认情况下git在这里采用了一个快捷方式。一旦它意识到&#34;我们的&#34;他们是&#34;他们的祖先&#34;,它知道它可以做一个&#34;快进&#34;而不是真正的合并。它可以将master
移动到branch1
所在的位置,而不会合并任何内容。 (您可以通过向--no-ff
命令提供merge
选项来防止这种情况。)
但重点是,即使没有快进,这也不会发生冲突,因为根据定义的冲突意味着&#34;我们&#34;改变了相同的代码块#34;他们&#34;改变,在这种情况下&#34;我们&#34;并没有改变任何事情。
所以现在快进后你已经
了A -- B <-(branch1)(master)
|\ ^HEAD
| C <-(branch2)
|\
| D <-(branch3)
\
E <-(branch4)
请注意master
不再是 C
,D
或E
的祖先;第一个合并将master
移动到其他分支无法访问的提交。真正的合并也是如此;在这种情况下,master
将转移到新的&#34;合并提交&#34;。由于快进,它只是移动到B
,但效果是一样的。
所以现在你说
git merge branch2
当我们寻找&#34;我们的&#34;之间的合并基础时(master
= B
)和&#34;他们的&#34; (branch2
= C
)我们会找到A
。所以基数是A
,但这次&#34;我们的变化&#34;是&#34;第1行是从&#39; 01&#39;到&#39; 01一个&#39;&#34;。 &#34;他们的变化&#34; &#34;第2行从&#39; 02&#39;到了&#39; 02两个&#39;&#34;。
由于不满足快进条件,我们必须使用这两个补丁进行完全合并。你可能会认为&#34;我可以在没有冲突的情况下组合这些补丁&#34;导致&#34;第1行从&#39; 01&#39;到了&#39; 01一个&#39; 01和第2行改变了frmo&#39; 02&#39;到了&#39; 02两个&#39;&#34;。如果补丁严格逐行比较,那就是真的;但他们不是。
他们没有理由。 git的目的是维护程序源代码的版本。可能性是第1行的代码与第2行的代码有关。如果变化没有相应的距离(不,我不知道距离是什么[1]),它们是被认为会影响同一个大块头。
因此,不要假设这些更改是独立正确的并且不会互相干扰,git会将其标记为冲突,并要求您决定最终结果应该是什么。即使你不得不不必要地解决100个这样的冲突,成本仍然低于git假设它知道该做什么和错误的一个实例。
第3次和第4次合并的分析将是相同的;在每种情况下,您有两个影响同一块代码的补丁,因此需要手动干预。
现在,如果您碰巧是源控制某些特殊类型的文件,您对此非常有信心(实际上,您可能必须绝对确定),对一行的更改与更改无关到下一行,然后你可以编写自己的合并驱动程序。它不是微不足道的,请记住,当添加行时,删除行时,当文件的两个版本偏离您在您的文件中显示的偏差时,您必须知道该怎么做例如,等等。
接受有时你必须解决冲突的可能性更大。如果你想要写作&#34;玩具&#34;模拟非冲突合并的测试,最简单的方法是让每个分支更改不同的文件;但是在一个文件中,变化不能太紧密,否则冲突就会发生。
[1] 评论更新 - 根据torek的说法,即使只有一条中间线也没有人改变就足够了。我并不认为这是正确的,但作为一项规则,如果托雷克告诉我一些我认为错误的事情,我会进行另一次测试;根据那个测试,我会同意他的观点。事实上,似乎你可以合并branch1
和branch3
而不会发生冲突,例如。
答案 2 :(得分:4)
是否有方法可以实现此方案而不会发生合并冲突?
tl;博士:没有。
合并发现(在某些情况下Git的合并将实际构建)公共基础并将该基础的差异与每个分支提示进行比较。重叠范围变化的任何差异都构成冲突。
没有人能找到一种自动解决这些重叠的方法。我们已经有大量的现有历史可供使用,因此您可以尝试在linux或vim或libreoffice或git本身的合并中提出的任何方法,或者你有什么,这不是第一次大家曾经看过一个问题的人错过了什么,但我对“无法做到”的答案打了很多。
不良自动注射的成本非常高,这是疼痛/增益指标的“痛苦”。收益只是方便:许多冲突与你的冲突一样,人类很容易正确解决。使用工具获得好处,简单的案例需要几秒钟。所以Git非常谨慎。
答案 3 :(得分:1)
我发现有用的是分支1&amp; 2被合并到master,checkout到分支3并从master拉出并解决冲突然后推送到master,然后分支4可以从master中拉出所有已解决的冲突并合并然后push to master。
答案 4 :(得分:0)
是否有方法可以实现此方案而不会发生合并冲突?
否(如其他答案中所述),但可以使用正确的工具自动解决这些问题。
以下内容基于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进行合并而不是它(这通常是你想要的但是风险更高)这是不正确的线,因此有一个权衡)。
使用mergeto命令使用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程序,但在这种情况下,当它自动解决问题时,它没有显示窗口。如果存在必须手动解决的冲突,则会有。