为什么 git cherry-pick 冲突的额外变化?
Why extra changes in git cherry-pick conflict?
对于 git cherry-pick
导致的冲突,为什么 Git 建议的更改不仅仅是来自给定的提交?
示例:
-bash-4.2$ git init
Initialized empty Git repository in /home/pfusik/cp-so/.git/
-bash-4.2$ echo one >f
-bash-4.2$ git add f
-bash-4.2$ git commit -m "one"
[master (root-commit) d65bcac] one
1 file changed, 1 insertion(+)
create mode 100644 f
-bash-4.2$ git checkout -b foo
Switched to a new branch 'foo'
-bash-4.2$ echo two >>f
-bash-4.2$ git commit -a -m "two"
[foo 346ce5e] two
1 file changed, 1 insertion(+)
-bash-4.2$ echo three >>f
-bash-4.2$ git commit -a -m "three"
[foo 4d4f9b0] three
1 file changed, 1 insertion(+)
-bash-4.2$ echo four >>f
-bash-4.2$ git commit -a -m "four"
[foo ba0da6f] four
1 file changed, 1 insertion(+)
-bash-4.2$ echo five >>f
-bash-4.2$ git commit -a -m "five"
[foo 0326e2e] five
1 file changed, 1 insertion(+)
-bash-4.2$ git checkout master
Switched to branch 'master'
-bash-4.2$ git cherry-pick 0326e2e
error: could not apply 0326e2e... five
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'
-bash-4.2$ cat f
one
<<<<<<< HEAD
=======
two
three
four
five
>>>>>>> 0326e2e... five
我期待的只是冲突标记之间的 "five" 线。
我可以将 Git 切换为我预期的行为吗?
嗯...在我看来这是一个边缘案例,生活在 "cherry-picking an insertion that was made in the source branch at a line that doesn't exist in the target branch" 和 "progression of changes that all are deemed one 'hunk' because each is adjacent to the one before" 的交叉点。
这些问题使 git 不确定您的意图,因此它以最广泛的方式询问 "what should I do",为您提供做出正确决定可能需要的所有代码。
如果您将文件初始化为
a
b
c
d
e
f
g
h
i
然后创建类似
的更改
x -- A <--(master)
\
C -- E -- G <--(branch)
(其中标记为 A
的提交将文件中的相应字母大写,等等),这种情况的行为将更像您期望的那样 - 因为一切看起来都非常清楚 git当你说到 cherry-pick E
到 master
时,它只是做了显而易见的事情。 (它不仅不会将不相关的更改粘贴到冲突标记中,而且没有冲突标记;它只是有效。)
现在要明确 - 我并不是说 cherry-pick 只会在像我这样的 clear-cut 情况下做正确的事情。但是正如您的测试用例是一种 "worst case" 的 cherry-pick 场景一样,我的是一个理想的案例。介于两者之间的情况很多,实际上人们通常似乎从 cherry-pick 中得到了他们想要的结果。 (如果这听起来像是对 cherry-pick 命令的认可,让我澄清一下:我认为它是套件中最多的 over-recommended 命令,我从来没有真正使用过它。但它确实有效,在它的范围内定义的边界。)
基本上记住两件事:
1) 如果你在源分支上插入一个新行,并且在目标分支中无法明确确定相应的插入点,你就会发生冲突,它可能会包括 "context" 来自源分支
2) 如果两个变化是相邻的——即使它们不重叠——它们仍然可以被认为是冲突的。
在我们继续之前,让我们绘制提交图:
A <-- master (HEAD)
\
B--C--D--E <-- foo
或者,为了便于比较,下面是Git的绘制方式:
$ git log --all --decorate --oneline --graph
* 7c29363 (foo) five
* a35e919 four
* ee70402 three
* 9a179e6 two
* d443a2a (HEAD -> master) one
(请注意,我已将您的问题转换为命令序列,并已附加;我的提交哈希值当然与您的不同。)
Cherry-pick 是 merge
的一种特殊形式
您在这里看到有些病态行为的原因是 git cherry-pick
实际上正在执行合并操作。最奇怪的部分是选择的合并基础。
正常合并
对于正常的合并,您检查一些提交(通过检查一些检查该分支的尖端提交的分支)和 运行 git 合并 <em>其他</em>
。 Git 定位由 other
指定的提交,然后使用提交图定位合并基础,这在图中通常非常明显。例如,当图形如下所示时:
o--o--L <-- ours (HEAD)
/
...--o--B
\
o--o--R <-- theirs
合并基础只是提交B
(对于基础)。
为了进行合并,Git 然后创建两个 git diff
,一个从合并基到我们本地提交 L
在左边,他们的提交 R
在右侧(有时称为 远程 提交)。即:
git diff --find-renames B L # find what we did on our branch
git diff --find-renames B R # find what they did on theirs
Git 然后可以组合这些更改,将组合的更改应用到 B
,以创建一个新的合并提交,其第一个父级是 L
,第二个父级是 R
.最后的合并提交是 merge commit,它使用 "merge" 作为形容词。我们通常只是称它为a merge,它使用"merge"这个词作为名词。
要获得此 merge-as-a-noun,但是,Git 必须 运行 合并机制,以合并两组差异。这是合并的过程,用"merge"这个词作为动词
一个cherry-pick合并
做一个 cherry-pick,Git 运行s 合并机制——合并为一个动词,正如我喜欢说的那样它——但选择了一个特殊的合并基础。 cherry-pick 的合并基础只是被 cherry-picked.
提交的 parent
在您的情况下,您正在 cherry-pick 提交 E
。所以 Git 正在合并(动词)提交 D
作为合并基础,提交 A
作为 left/local L 提交,并且提交 E
作为 right-side R 提交。 Git 生成两个差异列表的内部等效项:
git diff --find-renames D A # what we did
git diff --find-renames D E # what they did
我们所做的是删除四行:阅读two
、three
和four
。他们所做的是 添加 一行:读作 five
.
使用merge.conflictStyle
如果我们将 merge.conflictStyle
设置为 diff3
,那么这一切都会变得更清楚一些——好吧,也许 会更清楚一些。现在,Git 不仅向我们展示了被 <<<<<<<
等包围的我们的和他们的部分,还添加了 merge base 版本,标记为 |||||||
:
one
<<<<<<< HEAD
||||||| parent of 7c29363... five
two
three
four
=======
two
three
four
five
>>>>>>> 7c29363... five
我们现在看到 Git 声称我们从基础中删除了三行,而他们保留了这三行并添加了第四行。
当然,我们需要了解这里的合并基础是提交 E
的父级,如果有的话 "ahead of" 我们当前的提交 A
。我们删除 三行并不是真的。事实上,我们 一开始就没有这三行。 我们只需要处理 Git 显示事物 就好像 我们删除了一些行。
附录:生成冲突的脚本
#! /bin/sh
set -e
mkdir t
cd t
git init
echo one >f
git add f
git commit -m "one"
git checkout -b foo
echo two >>f
git commit -a -m "two"
echo three >>f
git commit -a -m "three"
echo four >>f
git commit -a -m "four"
echo five >>f
git commit -a -m "five"
git checkout master
git cherry-pick foo
对于 git cherry-pick
导致的冲突,为什么 Git 建议的更改不仅仅是来自给定的提交?
示例:
-bash-4.2$ git init
Initialized empty Git repository in /home/pfusik/cp-so/.git/
-bash-4.2$ echo one >f
-bash-4.2$ git add f
-bash-4.2$ git commit -m "one"
[master (root-commit) d65bcac] one
1 file changed, 1 insertion(+)
create mode 100644 f
-bash-4.2$ git checkout -b foo
Switched to a new branch 'foo'
-bash-4.2$ echo two >>f
-bash-4.2$ git commit -a -m "two"
[foo 346ce5e] two
1 file changed, 1 insertion(+)
-bash-4.2$ echo three >>f
-bash-4.2$ git commit -a -m "three"
[foo 4d4f9b0] three
1 file changed, 1 insertion(+)
-bash-4.2$ echo four >>f
-bash-4.2$ git commit -a -m "four"
[foo ba0da6f] four
1 file changed, 1 insertion(+)
-bash-4.2$ echo five >>f
-bash-4.2$ git commit -a -m "five"
[foo 0326e2e] five
1 file changed, 1 insertion(+)
-bash-4.2$ git checkout master
Switched to branch 'master'
-bash-4.2$ git cherry-pick 0326e2e
error: could not apply 0326e2e... five
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'
-bash-4.2$ cat f
one
<<<<<<< HEAD
=======
two
three
four
five
>>>>>>> 0326e2e... five
我期待的只是冲突标记之间的 "five" 线。 我可以将 Git 切换为我预期的行为吗?
嗯...在我看来这是一个边缘案例,生活在 "cherry-picking an insertion that was made in the source branch at a line that doesn't exist in the target branch" 和 "progression of changes that all are deemed one 'hunk' because each is adjacent to the one before" 的交叉点。
这些问题使 git 不确定您的意图,因此它以最广泛的方式询问 "what should I do",为您提供做出正确决定可能需要的所有代码。
如果您将文件初始化为
a
b
c
d
e
f
g
h
i
然后创建类似
的更改x -- A <--(master)
\
C -- E -- G <--(branch)
(其中标记为 A
的提交将文件中的相应字母大写,等等),这种情况的行为将更像您期望的那样 - 因为一切看起来都非常清楚 git当你说到 cherry-pick E
到 master
时,它只是做了显而易见的事情。 (它不仅不会将不相关的更改粘贴到冲突标记中,而且没有冲突标记;它只是有效。)
现在要明确 - 我并不是说 cherry-pick 只会在像我这样的 clear-cut 情况下做正确的事情。但是正如您的测试用例是一种 "worst case" 的 cherry-pick 场景一样,我的是一个理想的案例。介于两者之间的情况很多,实际上人们通常似乎从 cherry-pick 中得到了他们想要的结果。 (如果这听起来像是对 cherry-pick 命令的认可,让我澄清一下:我认为它是套件中最多的 over-recommended 命令,我从来没有真正使用过它。但它确实有效,在它的范围内定义的边界。)
基本上记住两件事:
1) 如果你在源分支上插入一个新行,并且在目标分支中无法明确确定相应的插入点,你就会发生冲突,它可能会包括 "context" 来自源分支
2) 如果两个变化是相邻的——即使它们不重叠——它们仍然可以被认为是冲突的。
在我们继续之前,让我们绘制提交图:
A <-- master (HEAD)
\
B--C--D--E <-- foo
或者,为了便于比较,下面是Git的绘制方式:
$ git log --all --decorate --oneline --graph
* 7c29363 (foo) five
* a35e919 four
* ee70402 three
* 9a179e6 two
* d443a2a (HEAD -> master) one
(请注意,我已将您的问题转换为命令序列,并已附加;我的提交哈希值当然与您的不同。)
Cherry-pick 是 merge
的一种特殊形式您在这里看到有些病态行为的原因是 git cherry-pick
实际上正在执行合并操作。最奇怪的部分是选择的合并基础。
正常合并
对于正常的合并,您检查一些提交(通过检查一些检查该分支的尖端提交的分支)和 运行 git 合并 <em>其他</em>
。 Git 定位由 other
指定的提交,然后使用提交图定位合并基础,这在图中通常非常明显。例如,当图形如下所示时:
o--o--L <-- ours (HEAD)
/
...--o--B
\
o--o--R <-- theirs
合并基础只是提交B
(对于基础)。
为了进行合并,Git 然后创建两个 git diff
,一个从合并基到我们本地提交 L
在左边,他们的提交 R
在右侧(有时称为 远程 提交)。即:
git diff --find-renames B L # find what we did on our branch
git diff --find-renames B R # find what they did on theirs
Git 然后可以组合这些更改,将组合的更改应用到 B
,以创建一个新的合并提交,其第一个父级是 L
,第二个父级是 R
.最后的合并提交是 merge commit,它使用 "merge" 作为形容词。我们通常只是称它为a merge,它使用"merge"这个词作为名词。
要获得此 merge-as-a-noun,但是,Git 必须 运行 合并机制,以合并两组差异。这是合并的过程,用"merge"这个词作为动词
一个cherry-pick合并
做一个 cherry-pick,Git 运行s 合并机制——合并为一个动词,正如我喜欢说的那样它——但选择了一个特殊的合并基础。 cherry-pick 的合并基础只是被 cherry-picked.
提交的 parent在您的情况下,您正在 cherry-pick 提交 E
。所以 Git 正在合并(动词)提交 D
作为合并基础,提交 A
作为 left/local L 提交,并且提交 E
作为 right-side R 提交。 Git 生成两个差异列表的内部等效项:
git diff --find-renames D A # what we did
git diff --find-renames D E # what they did
我们所做的是删除四行:阅读two
、three
和four
。他们所做的是 添加 一行:读作 five
.
使用merge.conflictStyle
如果我们将 merge.conflictStyle
设置为 diff3
,那么这一切都会变得更清楚一些——好吧,也许 会更清楚一些。现在,Git 不仅向我们展示了被 <<<<<<<
等包围的我们的和他们的部分,还添加了 merge base 版本,标记为 |||||||
:
one
<<<<<<< HEAD
||||||| parent of 7c29363... five
two
three
four
=======
two
three
four
five
>>>>>>> 7c29363... five
我们现在看到 Git 声称我们从基础中删除了三行,而他们保留了这三行并添加了第四行。
当然,我们需要了解这里的合并基础是提交 E
的父级,如果有的话 "ahead of" 我们当前的提交 A
。我们删除 三行并不是真的。事实上,我们 一开始就没有这三行。 我们只需要处理 Git 显示事物 就好像 我们删除了一些行。
附录:生成冲突的脚本
#! /bin/sh
set -e
mkdir t
cd t
git init
echo one >f
git add f
git commit -m "one"
git checkout -b foo
echo two >>f
git commit -a -m "two"
echo three >>f
git commit -a -m "three"
echo four >>f
git commit -a -m "four"
echo five >>f
git commit -a -m "five"
git checkout master
git cherry-pick foo