对于导致冲突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
我期待的只是冲突标记之间的"五"线。 我可以将 Git 切换到我的预期行为吗?
在我们进一步讨论之前,让我们绘制提交图:
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
(请注意,我把你的问题变成了一个命令序列,我已经附加了这个序列;我的提交哈希当然与你的不同。
樱桃采摘是一种特殊的合并形式
您在此处看到有些病态行为的原因是git cherry-pick
实际上是在执行合并操作。 最奇怪的部分是选择的合并基础。
正常合并
对于普通合并,您签出某个提交(通过签出某个分支来签出该分支的提示提交)并运行git mergeother
。 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"作为形容词。 我们通常只是称它为合并,它使用">合并"一词作为名词。
但是,为了获得这个合并作为名词,Git 必须运行合并机制,以组合两组差异。 这是合并的过程,使用">合并"一词作为动词。
樱桃合并
为了做一个挑选,Git 运行合并机制——合并作为一个动词,正如我喜欢说的那样——但挑选了一个特殊的合并基础。 樱桃拣选的合并基只是被挑选的提交的父级。
在您的情况下,您正在挑选提交E
。 所以 Git 正在合并(动词),将提交D
作为合并基础,将提交A
作为左侧/本地L提交,并将 commitE
作为右侧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 不仅向我们展示了被<<<<<<<
等包围的我们和他们的部分,还添加了合并基础版本,标有|||||||
:
one
<<<<<<< HEAD
||||||| parent of 7c29363... five
two
three
four
=======
two
three
four
five
>>>>>>> 7c29363... five
我们现在看到 Git 声称我们从基底中删除了三行,而他们保留了这三行并添加了第四行。
当然,我们需要了解这里的合并基础是提交E
的父级,如果有的话,它"领先于"我们当前的提交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 不确定你的意图,所以它以最广泛的方式询问"我应该做什么",为你提供做出正确决定可能需要的所有代码。
如果将文件初始化为
a
b
c
d
e
f
g
h
i
然后创建类似
x -- A <--(master)
C -- E -- G <--(branch)
(其中 commit 标记为大写A
文件中的相应字母等),这种情况的行为将更像您的预期 - 因为 git 的所有内容看起来都非常清楚,当您说挑选E
master
时,它只是做了显而易见的事情。 (它不仅不会将不相关的更改粘贴到冲突标记中,而且没有冲突标记;它只是工作。
现在要明确一点 - 我并不是说樱桃采摘只会在像我这样明确的情况下做正确的事情。 但是,正如您的测试用例是樱桃挑选的"最坏情况"情况一样,我的测试用例也是理想的情况。 介于两者之间的情况很多,在实践中,人们通常似乎从挑选中得到了他们想要的结果。 (如果这听起来像是对 cherry-pick 命令的认可,让我澄清一下:我认为这是套件中最过度推荐的命令,我从未真正使用它。 但它确实在其定义的边界内有效。
基本上记住两件事:
1)如果您在源分支上插入新行,并且无法在目标分支中明确确定相应的插入点,则会遇到冲突,并且可能会包含来自源分支的"上下文">
2)如果两个变化相邻 - 即使它们不重叠 - 它们仍然可以被视为冲突。