为什么 Git 知道它可以挑选一个还原的提交?

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 已经在这个分支中,所以不需要再次选择它。

然后我通过以下方式在批量提交中还原了 BC

git revert -n B^..C
git commit -a -m "xxx"

这将是一个新的大提交 D,它将恢复 BC,分支应该像 A <- B <- C <- D.

然后由于某些原因我需要重做BC。我试过了:

git cherry-pick B^..C

我看到两个新提交 B'C' 附加到分支:A <- B <- C <- D <- B' <- C'.

我的 第一个问题 是,Git 如何智能地 知道它应该创建 B'C'?我以为 Git 会发现 BC 已经在分支历史中,所以它可能会像我直接在 中挑选 'B' 一样跳过它们测试 A.

然后,在那之后,因为分支已经是A <- B <- C <- D <- B' <- C',所以我再次运行这个命令:

git cherry-pick B^..C

我预计 Git 可以识别这是一个空操作。但是这次Git抱怨冲突。我的第二个问题是,为什么Git这次无法识别并跳过这个操作?

cherry-pick 是合并,将您的 cherry-pick 的 parent 与 cherry-pick 的差异与您的 cherry-pick 的差异合并s parent 给你的 checked-out 小费。就是这样。 Git 不必知道更多。它不关心任何提交的“位置”,它关心合并这两组差异。

revert 是从你的恢复到它的 parent 的差异与从你的恢复到你的 checked-out 提示的差异的合并。就是这样。 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 :/2git diff :/1 :/3。当你在这里说 git cherry-pick :/2 时,这些就是差异 git 运行。第一个 diff 更改第 2 行,第二个提交更改第 2 行和第 4 行;第 4 行的更改与第一个 diff 中的任何更改无关,而第 2 行的更改在两者中都是相同的。没什么可做的,所有 :/1-:/2 的变化也在 :/1-:/3.

现在,在您开始接下来的内容之前,让我这样说:用散文解释这比只看更难。执行上面的示例序列并查看输出。通过查看它比阅读任何描述更容易 muchmuch 了解发生了什么。每个人都经历过一段,这太新了,也许一点点方向会有所帮助,这就是下面段落的目的,但同样:单独的散文比差异更难理解。 运行 差异,请尝试了解您正在查看的内容,如果您需要一点帮助,我保证是一个非常小的驼峰,请按照下面的文字进行操作。当它突然聚焦时,看看您是否至少没有在脑子里拍打自己的额头并想“哇,为什么这么难看?”,就像,好吧,几乎每个人都一样。

Git 的合并规则非常简单:接受对重叠或邻接线的相同更改 as-is。对已更改行的一个差异中没有更改的行的更改,或与已更改行相邻的行,在另一个差异中,按原样接受。 不同 对任何重叠或邻接线的更改,好吧,有大量的历史可供参考,但没有人找到一条规则来预测每次的结果应该是什么,所以git 声明更改冲突,将两组结果转储到文件中并让您决定结果应该是什么。

那么,如果您现在更改第 3 行会发生什么情况?

sed -si 3s,$,x, file; git commit -amx

运行 git diff :/1 :/2git diff :/1 :/x,你会看到,相对于 cherry-pick 的 parent,:/2 更改了第 2 行,您的提示更改了第 2,3 和 4 行。2 和 3 邻接,从历史上看,这对于自动精灵来说太近了,无法正确处理,所以是的,您可以这样做:git cherry-pick :/2 现在将声明冲突,向您展示第 2 行的更改以及第 3 行和第 4 行的两个不同版本(:/2 均未更改,您的提示均已更改,在此处的上下文中,很明显第 3 行和第 4 行的更改很好 as-is 但再次: 没有人想出可靠识别此类上下文的自动规则)。

您可以在此设置上更改以测试还原的工作原理。还可以存储 pops、合并和 git checkout -m,其中 运行 可以快速 ad-hoc 与您的索引合并。

您的 git cherry-pick B^..C 是两个提交 BC 的 cherry-pick。它一个接一个地执行它们,就像上面描述的那样。由于您已还原 BC,然后再次对它们进行 cherry-pick,这与应用 BC 以及然后 cherry-picking B(目的是 cherry-picking C)。我的结论是 BC 接触重叠或邻接的线,因此 git diff B^ B 将显示重叠或邻接 git diff B^ C' 的变化,这就是 Git'不会只给你挑,因为这里看起来对的,在其他情况下没有人能写出识别规则,一个 identical-looking 的选择就是错误的。所以 git 说这两组变化有冲突,你得解决它。

这扩展了

考虑像这样的历史记录中的常规合并:

a--b--c--d--e--f--g--h
       \
        r--s--t

Git 通过仅查看这些提交的内容来执行合并:

c--h   <-- theirs
 \
  t    <-- ours
^
|
base

仅此而已。请注意,在概念层面上,哪一方被称为“我们的”,哪一方是“他们的”是完全无关的;它们完全可以互换。 (唯一会有所不同的是当存在冲突时 Git 必须决定如何为用户将边标记为“他们的”和“我们的”。)(我将省略标签“base” ,下表中的“他们的”和“我们的”。)

在你的历史中

A--B--C

第一个 git cherry-pick B 后面的合并操作查看了以下提交:

A--B
 \
  C

这里选择了A,因为它是B、a.k.a、B^的父级。显然,从 AC 的变化也包含从 AB 的变化,合并机制产生一个无变化合并结果,并产生cherry-pick is now empty 留言。

然后你通过还原 BC 创造了这个历史:

A--B--C--R

然后下一个 git cherry-pick B 查看了这些提交:

A--B
 \
  R

这一次,从 AR 的更改不再包含从 AB 的更改,因为它们已被还原。因此,合并不再产生空结果。

一个小弯路:当你在你的历史中做 git revert B 时,合并机制会查看这些提交:

B--A
 \
  C

请注意,与 git cherry-pick B.

相比,只有 BB、a.k.a、A 的父级交换了

(我描述的是单次提交逆转,因为我不确定多次提交逆转是如何工作的。)

让我们退后十英尺,在脑海中更清晰地了解 Git 是什么。

Git 提交是所有文件 的快照。它基本上代表了您的整个项目。这与差异无关。这是一个绝妙的架构,因为它速度极快并且有效地可靠。任何提交都可以完全恢复项目的状态,kaboom,只需签出即可;无需“思考”。

然而,Git 可以 使 两次提交之间存在差异,这就是它实现我们所谓的“合并逻辑”的方式。每个合并都包含同时应用两个差异。 [好吧,它可能不止两个,但假装不是。] 合并、cherry pick、rebase、revert 都是这种意义上的合并——它们都使用“合并逻辑”来形成一个提交,表达应用两个差异的结果。诀窍是知道在两个差异的构造中谁是比较对象。

  • 当你要求一个真实的 git merge 时,比如两个分支,Git 计算出这些分支最后分叉的位置。这称为合并基础。 comparands 是:branch1 的合并基础和尖端,以及 branch2 的合并基础和尖端。这两个差异都应用于合并基础,结果用于与两个父项(分支提示)形成提交。第一个分支名称然后向上滑动一个,以指向该新提交。

  • 当您请求 cherry-pick 时,合并基础是所选择提交的父级。比较对象是:合并基础和头部,以及合并基础和选择的提交。这两个差异都应用于合并基础,结果用于与一个父(头)形成提交。头分支名称然后向上滑动一个,以指向该新提交。 [rebase 只是一系列的精选!]

  • A revert 使用合并逻辑。正如 jthill 所解释的,这只是形成差异之一的问题 backwards。合并基础是您要撤销的提交。比较对象是:合并基础及其父级(在那个方向),以及合并基础和头部。这些差异应用于合并基础并用于形成其父级为头部的提交。头分支名称然后向上滑动一个,以指向该新提交。如果这向您暗示还原基本上是一种向后的樱桃选择,那么您是绝对正确的。


很酷的是,一旦你知道这一点,你就可以预测当你给出这些命令之一时会发生什么,因为你可以提取那些相同的差异你自己git diff。 Git 的合并逻辑本质上是对您开放的。剩下的只是了解 Git 在操作中间停止的情况,因为它无法在没有进一步明确指示的情况下继续进行。这被称为(不幸的)冲突,它主要有两种产生方式:

  • 同一文件中的同一行在两个差异中以两种不同的方式进行了更改。 Git 关于什么构成同一条线的想法比你想象的要广泛得多;这让初学者感到惊讶。

  • 同一个文件,qua 文件,以两种不兼容的方式处理:例如,一个 diff 将其删除,而另一个 diff 保留并编辑它。


我应该再补充一个事实来解释很多行为,包括您所问的部分内容。这可能看起来很明显,但值得明确说明:在 diff 中,"nothing" 不是一个东西。 我的意思是这个。假设一个差异改变了一行,而另一个差异对该行没有任何影响。那么执行两个差异的方法是:更改行。什么都不做不是事:它不会“争吵”改变。

这一点值得一提,尤其是因为初学者往往不理解它。前几天有一个问题,用户抱怨在第二个分支删除文件的合并中,即使第一个分支保留了文件,该文件确实最终被删除了。该用户认为“不要删除文件”是一件事情,而且确实是一件主要的事情。但事实并非如此。两个diff默认权重相等,所以一个branch什么都不做,一个branch删文件,什么都不做,结果就是删文件。