问题描述
比如说,在一个分支中有 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 是从您的恢复到其父级的差异与从您的恢复到签出提示的差异的合并。而已。 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,file; git commit -am3
运行 git diff :/1 :/2
和 git diff :/1 :/3
。当您在这里说 git cherry-pick :/2
时,这些是 git 运行的差异。第一个 diff 更改第 2 行,第二个提交更改第 2 行和第 4 行;第 4 行的更改不与第一个差异中的任何更改相邻,并且第 2 行的更改在两者中都是相同的。没什么可做的了,所有 :/1
-:/2
更改也在 :/1
-:/3
中。
现在,在您开始以下内容之前,让我这样说:这比散文更难解释。执行上面的示例序列并查看输出。 ,通过观察它比阅读它的任何描述更容易看到正在发生的事情。每个人都经历过一段时间,这太新了,也许一点点方向会有所帮助,这就是下面的段落的目的,但同样:散文,单独,比差异更难理解。运行差异,尝试了解您正在查看的内容,如果您需要一点帮助来解决我承诺的非常小的问题,请在下面的文本中进行操作。当它成为焦点时,看看你是不是至少在精神上拍了拍你的额头并想“哇,为什么这么难看?”,就像,好吧,几乎每个人。
Git 的合并规则非常简单:对重叠或邻接行的相同更改按原样接受。对更改行的一个差异没有更改的行更改,或与更改行相邻的行,按原样接受。 不同 对任何重叠或邻接线的更改,嗯,有很多历史可以查看,而且没有人找到可以预测每次结果应该是什么的规则,所以 git 声明更改冲突,将两组结果转储到文件中,并让您决定结果应该是什么。
那么如果您现在更改第 3 行会怎样?
sed -si 3s,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 行的更改原样很好,但同样:没有人想出自动规则来可靠地识别此类上下文)。
您可以在此设置上响铃更改以测试恢复的工作原理。还有 stash 弹出和合并,以及 git checkout -m
与您的索引运行快速临时合并。
您的 git cherry-pick B^..C
是两个提交的精选,B
和 C
。它一个接一个地执行它们,正如上面所描述的那样。由于您已还原 B
和 C
,然后再次挑选它们,因此这与应用 B
和 C
然后挑选 {{{}} 具有完全相同的效果{1}}(目的是挑选B
)。我得出的结论是 C
和 B
接触重叠或邻接线,所以 C
将显示在 git diff B^ B
中重叠或邻接的变化,这就是 Git 不会只是选择对你来说,因为无论这里看起来如何,在其他情况下没有人可以编写识别规则,看起来相同的选择将是错误的。所以 git 说这两组更改冲突,你可以解决它。
这会展开 @jthill's answer。
考虑像这样的历史记录中的常规合并:
a--b--c--d--e--f--g--h
\
r--s--t
Git 通过只查看这些提交的内容来执行合并:
c--h <-- theirs
\
t <-- ours
^
|
base
没有别的。请注意,在概念层面上,哪一方表示“我们的”,哪一方表示“他们的”是完全无关的;它们完全可以互换。 (唯一的区别是当存在冲突时,Git 必须决定如何为用户将双方标记为“他们的”和“我们的”。)(我将省略标签“base”,“theirs”以及以下图表中的“我们的”。)
在你的历史中
A--B--C
第一个 git cherry-pick B
后面的合并操作查看了以下提交:
A--B
\
C
这里选择 A
是因为它是 B
的父级,也就是 B^
。显然,从 A
到 C
的变化也包含从 A
到 B
的变化,并且合并机制产生一个无变化的合并结果,并产生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
请注意,与 B
相比,只有 B
和 A
的父级,也就是 git cherry-pick B
交换了。
(我描述的是单次提交逆转,因为我不确定多提交逆转是如何工作的。)
,让我们向后退约 10 英尺,对 Git 有一个更大的心理了解。
一次 Git 提交是所有文件的快照。基本上,它代表了您的整个项目。这与差异无关。这是一个出色的架构,因为它非常快速且有效地可靠。任何提交都可以完全恢复项目的状态,kaboom,只需检查一下即可;没有必要“思考”。
然而,Git 可以制作两次提交之间的差异,这就是它实现我们所谓的“合并逻辑”的方式。每次合并都包括同时应用两个差异。 [嗯,它可能不止两个,但假装不是。] 从这个意义上说,合并、樱桃选择、变基、还原都是合并——它们都使用“合并逻辑”来形成表达应用两个差异的结果。诀窍是要知道在构建两个差异时的比较对象是谁。
-
当你要求一个真正的
git merge
时,比如说有两个分支,Git 会找出这些分支最后分歧的地方。这称为合并基础。比较对象是:branch1 的合并基点和尖端,以及 branch2 的合并基点和尖端。这两个差异都应用于合并基础,结果用于形成具有两个父级的提交(分支提示)。然后第一个分支名称向上滑动,指向新的提交。 -
当你请求一个
cherry-pick
时,合并基础是被选择的提交的父级。比较对象是:合并基础和头部,以及合并基础和选择的提交。这两个差异都应用于合并基础,结果用于与一个父级(头部)形成提交。头分支名称然后向上滑动一个,指向新的提交。 [rebase 只是一系列的挑选!] -
A
revert
也 使用合并逻辑。正如 jthill 所解释的,这只是形成一个向后差异的问题。合并基础是您尝试反转的提交。比较对象是:合并基础及其父项(在那个方向),以及合并基础和头部。这些差异应用于合并基础并用于形成父项为头的提交。头分支名称然后向上滑动一个,指向新的提交。如果这向您表明回滚基本上是一个倒退的樱桃选择,那么您是绝对正确的。
很酷的事情是,一旦你知道了这一点,你就可以预测当你给出这些命令之一时会发生什么,因为你可以自己提取这些相同的差异说git diff
。 Git 的合并逻辑基本上对您开放。然后只需要了解 Git 在操作中间停止的情况,因为如果没有进一步的明确指令,它就无法继续。这被称为(不幸的是)冲突,它可能以两种主要方式出现:
-
在两个差异中,同一文件中的同一行以两种不同的方式进行了更改。 Git 对同一行的构成的想法比您想象的要广泛得多;这让初学者感到惊讶。
-
同一个文件 qua 文件被以两种不兼容的方式处理:例如,一个 diff 删除它,而另一个 diff 保留并编辑它。
我应该再补充一个事实来解释很多行为,包括您所询问的部分内容。这似乎很明显,但值得明确说明:在差异中,“nothing”不是一个东西。我的意思是这个。假设一个 diff 更改了一行,而另一个 diff 对该行没有任何影响。然后制定两个差异的方法是:更改行。无所作为不是一件事:它不会与变化“争吵”。
这是值得一提的,尤其是因为初学者往往不掌握它。前几天有一个问题,用户抱怨在第二个分支删除文件的合并中,即使第一个分支保留了该文件,该文件确实最终被删除。该用户认为“不要删除文件”是一件事情,实际上是一件主要的事情。但事实并非如此。两个diff默认权重相等,所以一个分支什么都不做,一个分支删除了文件,什么都不做也不是一回事,所以结果就是删除了文件。