[英]Why does Git know it can cherry-pick a reverted commit?
例如,在一个分支中,有 3 个提交: A <- B <- C
。 如果我直接挑选B
(测试 A ),Git 说:
The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:
git commit --allow-empty
我可以理解,因为B
已经在这个分支中,所以无法再次挑选它。
然后我通过以下方式在批量提交中恢复了B
和C
:
git revert -n B^..C
git commit -a -m "xxx"
这将是一个新的大提交D
,它恢复B
和C
,分支应该像A <- B <- C <- D
。
然后由于某种原因我需要重做B
和C
。 我试过了:
git cherry-pick B^..C
我看到两个新的提交B'
和C'
附加到分支: A <- B <- C <- D <- B' <- C'
。
我的第一个问题是,Git 如何智能地知道它应该创建B'
和C'
? 我认为 Git 会发现B
和C
已经在分支历史中,所以它可能会像我直接在Test A中选择“B”一样跳过它们。
然后,在那之后,由于分支已经是A <- B <- C <- D <- B' <- C'
,我再次运行这个命令:
git cherry-pick B^..C
我预计 Git 可以识别这是一个无操作操作。 但是这次 Git 抱怨冲突。 我的第二个问题是,为什么Git这次无法识别并跳过这个操作?
cherry-pick 是从您的cherry-pick 的父母到cherry-pick 的差异的合并,以及从您的cherry-pick 的父母到您的签出提示的差异。 而已。 Git 不需要知道更多。 它不关心任何提交的“位置”,它关心合并这两组差异。
revert 是从您的 revert 到其父级的差异与从您的 revert 到您的签出提示的差异的合并。 而已。 Git 不用再知道了。
在这里:试试这个:
git init test; cd $_
printf %s\\n 1 2 3 4 5 >file; git add .; git commit -m1
sed -si 2s,$,x, file; git commit -am2
sed -si 4s,$,x, file; git commit -am3
运行git diff:/1:/2
和git diff:/1:/3
。 当您在此处说git cherry-pick:/2
时,这些是 git 运行的差异。 第一个 diff 更改了第 2 行,第二个 commit 更改了第 2 行和第 4 行; 第 4 行更改不与第一个差异中的任何更改相邻,并且第 2 行更改在两者中都是相同的。 没有什么可做的,所有:/1
- :/2
更改也在:/1
- :/3
中。
现在,在你开始接下来的内容之前,让我这样说:用散文来解释比仅仅看到更难。 执行上面的示例序列并查看 output。 通过查看它比阅读它的任何描述更容易看到正在发生的事情。 每个人都经历了一段太新的时期,也许一点方向会有所帮助,这就是下面的段落的目的,但同样:单独的散文比差异更难理解。 运行差异,试着理解你在看什么,如果你需要一点帮助我 promise 是一个非常小的驼峰跟随在下面的文本中。 当它聚焦时,看看你是否至少在精神上拍打你的额头并想“哇,为什么这么难看到?”,就像,嗯,几乎每个人。
Git 的合并规则非常简单:对重叠或相邻线的相同更改按原样接受。 对已更改行的一个差异中没有更改的行的更改,或在另一行中与更改的行相邻的行,按原样接受。 对任何重叠或邻接线的不同更改,嗯,有很多历史可以查看,没有人找到一个规则来预测每次结果应该是什么,所以 git 声明更改冲突,转储两组结果到文件中,并让您决定结果应该是什么。
那么如果你现在更改第 3 行会发生什么?
sed -si 3s,$,x, file; git commit -amx
运行git diff:/1:/2
和git diff:/1:/x
,您会看到,相对于cherry-pick的父级, :/2
更改了第 2 行,并且您的提示更改了第 2,3 行和4. 2 和 3 邻接,这在历史上太接近自动精灵无法正确处理,所以是的,你可以这样做: git cherry-pick:/2
现在将声明冲突,向您显示对第 2 行的更改和这两个第 3 行和第 4 行的不同版本 (:/2 都没有改变,你的提示都改变了,在这里的上下文中,很明显第 3 行和第 4 行的更改按原样很好,但又一次:没有人想出可靠识别此类上下文的自动规则)。
您可以在此设置上响铃更改以测试还原的工作方式。 还存储弹出和合并,以及git checkout -m
与您的索引运行快速临时合并。
您的git cherry-pick B^..C
是两个提交B
和C
的精选。 就像上面描述的那样,它一个接一个地执行它们。 由于您已恢复B
和C
,然后再次选择它们,这与应用B
和C
然后选择樱桃B
的效果完全相同(目的是然后选择樱桃采摘C
)。 I conclude that B
and C
touch overlapping or abutting lines, so git diff B^ B
will show changes that overlap or abut changes in git diff B^ C'
, and that's what Git's not going to just pick for you, because whatever looks right在这里,在其他情况下,没有人可以编写识别规则,看起来相同的选择将是错误的。 所以 git 说这两组更改冲突,你可以解决它。
这扩展了@jthill 的答案。
考虑这样的历史记录中的常规合并:
a--b--c--d--e--f--g--h
\
r--s--t
Git 通过仅查看这些提交的内容来执行合并:
c--h <-- theirs
\
t <-- ours
^
|
base
没有别的了。 请注意,在概念层面上,哪一方是“我们的”,哪一方是“他们的”是完全无关的; 它们是完全可以互换的。 (唯一不同的是当存在冲突时,Git 必须决定如何将侧面标记为用户的“他们的”和“我们的”。)(我将省略标签“基地”、“他们的”和以下图表中的“我们的”。)
在你的历史
A--B--C
第一个git cherry-pick B
后面的合并操作查看了以下提交:
A--B
\
C
在这里,选择A
是因为它是B
的父级,也就是B^
。 显然,从A
到C
的更改也包含从A
到B
的更改,并且合并机制会产生 no-change-merge-result,这会产生cherry-pick is now empty
消息。
然后你通过恢复B
和C
创造了这个历史:
A--B--C--R
然后下一个git cherry-pick B
查看了这些提交:
A--B
\
R
这一次,从A
到R
的更改不再包含从A
到B
的更改,因为它们已被还原。 因此,合并不再产生空结果。
一个小弯路:当您在历史记录中执行git revert B
时,合并机制会查看这些提交:
B--A
\
C
请注意,与git cherry-pick B
相比,只有B
和B
的父级A
被交换。
(我描述的是单次提交撤销,因为我不确定多提交撤销是如何工作的。)
让我们退后大约 10 英尺,对 Git 是什么有一个更大的了解。
Git 提交是所有文件的快照。 它基本上代表了你的整个项目。 这与差异无关。 这是一个出色的架构,因为它非常快速且有效地可靠。 任何提交都可以完全恢复您的项目 kaboom 的 state,只需检查一下即可; 没有必要“思考”。
然而,Git 可以在两次提交之间产生差异,这就是它实现我们所谓的“合并逻辑”的方式。 每个合并都包括同时应用两个差异。 [嗯,它可能不止两个,但假装不是。] 从这个意义上说,合并、樱桃挑选、变基、恢复都是合并——它们都使用“合并逻辑”来形成表达应用两个差异的结果。 诀窍是要知道在两个差异的构造中谁是比较对象。
当您要求真正的git merge
时,比如说两个分支,Git 会计算出这些分支最后分歧的位置。 这称为合并基础。 比较对象是:branch1 的合并基数和尖端,以及 branch2 的合并基数和尖端。 这两个差异都应用于合并基础,结果用于与两个父级(分支提示)形成提交。 然后第一个分支名称向上滑动一个,以指向该新提交。
当您要求一个cherry-pick
时,合并基础是被挑选的提交的父级。 比较对象是:合并基础和头部,合并基础和选择的提交。 这两个差异都应用于合并基础,结果用于与一个父级(头部)形成提交。 然后头分支名称向上滑动一个,以指向该新提交。 [而变基只是一系列精选!]
revert
也使用合并逻辑。 正如 jthill 所解释的,这只是向后形成差异之一的问题。 合并基础是您尝试撤消的提交。 比较对象是:合并基础及其父级(在该方向上),以及合并基础和头部。 这些差异应用于合并基础并用于形成其父级为头部的提交。 然后头分支名称向上滑动一个,以指向该新提交。 如果这向您表明恢复基本上是向后的樱桃选择,那么您是绝对正确的。
很酷的是,一旦你知道了这一点,你就可以预测当你给出这些命令之一时会发生什么,因为你可以通过说git diff
自己提取这些相同的差异。 Git 的合并逻辑本质上是对你开放的。 剩下的只是了解 Git 在操作中间停止的情况,因为如果没有进一步的明确指示就无法继续。 这被称为(不幸的是)冲突,它可以通过两种主要方式出现:
同一文件中的同一行在两个差异中以两种不同的方式进行了更改。 Git 关于什么构成同一行的想法比你想象的要广泛得多。 这让初学者感到惊讶。
同一个文件 qua 文件以两种不兼容的方式处理:例如,一个 diff 删除它,而另一个 diff 保留并编辑它。
我应该再添加一个事实来解释很多行为,包括您所询问的部分内容。 这似乎很明显,但值得明确说明:在差异中, “无”不是事物。 我的意思是这个。 假设一个 diff 更改了一行,而另一个 diff 对该行没有任何作用。 然后制定两个差异的方法是:更改线路。 什么都不做不是一件事:它不会“争吵”反对变化。
这是值得一提的,尤其是因为初学者通常不掌握它。 前几天有一个问题,用户抱怨在第二个分支删除文件的合并中,该文件确实最终被删除,即使第一个分支保留了它。 该用户认为“不要删除文件”是一件事情,实际上是一件主要的事情。 但事实并非如此。 默认情况下,两个 diff 的权重是相等的,所以一个分支什么都不做,一个分支删除了文件,什么都不做也不是一件事,所以结果是删除了文件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.