通过 cherry-pick 添加行到不同位置导致冲突

Add line to differnent position cause conflict by cherry-pick

我的分支结构是:

0x1---->0x2---->0x3
/\              /\
|               |
master          dev

共同祖先是 0x1。

我挑选了一个要掌握的功能。

场景 1:

Master branch have a.txt file.
0x1 first commit.
a.txt content:
1

Then I create a branch dev, I add "2" to a.txt,
0x2 second commit
0x1 first commit

a.txt content:
1
2

Then I add "3" to a.txt
0x3 third commit
0x2 second commit
0x1 first commit

a.txt content:
3
1
2

I cherry-pick 0x3 to master:
master> git cherry-pick 0x3

没有冲突。

场景二: 但是我修改了添加的位置。

0x3 third commit
0x2 second commit
0x1 first commit

a.txt content:
1
2
3

我申请0x3为主。会有冲突。

场景一和场景二有什么区别?我很困惑!

您的 cherry-pick 发生合并冲突,因为相同的合并会发生合并冲突。这是因为 cherry-pick 合并操作。只是特意选择了cherry-pick的merge base,最终commit是普通的单亲commit,而不是双亲merge commit

我认为,当用 git merge 完成时,冲突本身更容易解释。假设我们有以下一系列提交:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

我们通过选择一个分支名称来选择一些提交,例如提交J,作为当前提交,例如branch1,成为当前分支。 Git 会将提交 J 中的快照提取到我们的工作树中:

git checkout branch1

我想通过将特殊名称 HEAD 附加到所选分支名称来表示这种情况,如下所示:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

如果我们现在运行 git merge branch2,Git将激活它的合并机制,从而执行合并过程。此合并机制需要 三个 提交:

  • 其中一个提交是我们当前的提交 J.
  • 其中一个提交是我们用 git merge 命名的提交:在这种情况下,branch2 选择提交 L
  • 第三个——或者,在某种意义上,第一个,因为Git必须首先使用它——提交是合并进程定位的那个。 merge 命令找到 最佳共享提交: 一个在两个分支上的提交,并且在某种意义上“最接近”两个分支提示提交。在这里,该提交显然是提交 H.

要执行实际的合并工作,Git现在:

  • 将基础 (H) 中的快照与当前提交中的快照进行比较 J;和
  • 将基础中的快照与其他提交中的快照进行比较 L

这会产生两个“差异”:两个用于更改出现在合并基础提交中的文件的方法。第一个秘诀说,如果您对 H 中的文件执行各种操作(例如添加 and/or 删除特定文件的特定行),您将获得 J 中的文件。第二个秘诀说,如果你做任何其他事情,你会得到 L.

中的文件

合并引擎现在合并了两个差异。这种合并会产生 合并冲突

何时 Git 发现合并冲突的问题有点奇怪。有一个案例就很清楚了。假设文件的基本版本在提交 H 中读取,例如:

The quick brown fox
jumps over
the lazy dog.

假设 JL 版本为:

The quick brown fox
sometimes jumps over
the lazy dog.

和:

The quick brown fox
has often jumped over
the lazy dog.

两个差异都以不兼容的方式更改了第 2 行。 Git 不知道要进行哪个更改,所以根据您的看法,它既不进行更改,也不进行更改,然后声明合并冲突并让您清理混乱。

您的精选

[注意:这已被修改为在更新的问题中使用提交图。我们现在创建存储库:

mkdir tcherry && cd tcherry && git init
echo 1 > a.txt && git add a.txt && git commit -m 0x1
git checkout -b dev
echo 2 >> a.txt && git add a.txt && git commit -m 0x2

然后在 a.txt 的顶部插入 3 或将其添加到 a.txt 的末尾以生成两种情况的设置:

printf "3\n1\n2\n" > a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev

或:

echo 3 >> a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev

第一个没有冲突;第二个给出了冲突。]

在你的例子中,你没有使用 git merge,你使用的是 git cherry-pick。但是挑选仍然是合并。而不是:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

你有:

  B--C   <-- dev
 /
A   <-- master (HEAD)

还有你运行git cherry-pick dev。请注意,dev 选择提交 C,而您当前的分支是 master,您当前的提交是 A

cherry-pick 命令调用 Git 的合并引擎,但是这次,合并基础 被强制成为您要提交的提交的父级采摘樱桃。所以这里的合并基础是commit B。 Git 现在将:

  • diff B vs A,以获得“我们的”改变;和
  • 差异 BC,以获得“他们的”更改。

BA 的区别在于您 删除了 a.txt 末尾的 2 行.以下(使用偷偷摸摸的 git rev-parse 找到正确的提交;参见 the gitrevisions documentation:/<text> 的描述)显示了这一点:

$ git diff :/0x2 :/0x1
diff --git a/a.txt b/a.txt
index 1191247..d00491f 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1 @@
 1
-2

AC 的区别取决于您使用的是这两种情况中的哪一种。

这是非冲突情况的差异:

$ git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..0571a2e 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
+3
 1
 2

这表示要在读取 1 的行之前添加 3,这是第 1 行变成第 2 行。所以 Git 应该 delete 行读取 2,这是合并基础版本中的第 2 行。 Git 可以安全地执行此操作,因为它知道在哪里执行此操作,并且行 1 本身未被触及:只有从 2 到文件末尾的行受到影响。 Git 也应该 插入 3 之前 1。这影响从文件的开头(“第 0 行”)到第 1 行。受影响的行范围不重叠,并且读取 1 的行位于它们之间,使它们也不会“接触”彼此。

现在让我们看看确实发生合并冲突的情况:

$ git reset --hard HEAD^          # discard the cherry-pick
HEAD is now at 2b4180d 0x1
$ git checkout -q dev
$ git reset --hard HEAD^          # discard commit 0x3
HEAD is now at 1160130 0x2
$ echo 3 >> a.txt && git add a.txt && git commit -m 0x3
[dev c546f74] 0x3
 1 file changed, 1 insertion(+)
$ cat a.txt
1
2
3
$ git checkout master
Switched to branch 'master'

同样,我们现在正在提交 A,我们将指示 Git 挑选提交 C(主题 0x3),其父项是B。从 BA 的差异仍然是“删除行读数 2”(在第 2 行)。这次让我们看看从 BC 的差异:

git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..01e79c3 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
 1
 2
+3

Git 现在应该结合删除行 2 和添加行 3。第一个触及第 2 行(通过完全删除它)。第二个在第 2 行之后添加一行。

Git当然可以结合这些,但是“这是冲突吗”算法不喜欢两个差异影响第2行的事实。这些差异不在两个更改之间有一条线,将更改彼此分开。所以我们遇到了冲突:

$ git cherry-pick dev
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
error: could not apply c546f74... 0x3
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

检查留下的 a.txt,我们看到:

$ cat a.txt
1
<<<<<<< HEAD
||||||| parent of c546f74... 0x3
2
=======
2
3
>>>>>>> c546f74... 0x3

请注意,我将 merge.conflictStyle 设置为 diff3,因此我得到了 ||||||| parent of ... 行,显示了冲突的原因。将其与默认样式进行比较:

$ git checkout -m --conflict merge a.txt
Recreated 1 merge conflict
$ cat a.txt
1
<<<<<<< ours
=======
2
3
>>>>>>> theirs

我个人觉得比较神秘。在像这样的樱桃选择的情况下,从 BA 的差异“向后”这一事实意味着我们删除了第 2 行。这形成了 ours冲突的一部分。从 merge 风格冲突来看一点也不明显;在 diff3 样式中,我可以查看它并看到合并基础版本有一行内容为 2,我已将其删除。他们在添加行 3.

保留了第 2 行