Pro Git 的书是正确的:提交就是快照。
不过,您也是正确的:git cherry-pick 应用了补丁。 (嗯,有点:请参阅下面的更多详细信息。)
这怎么可能?答案是,当您选择提交时,您还可以使用 -m <em>parent-number</em> 参数指定要考虑的 parent 提交。然后,cherry-pick 命令针对该父级生成一个差异,以便现在可以应用生成的差异。
如果您选择挑选非合并提交,则只有一个父级,因此您实际上不会传递 -m 并且该命令使用(单个)父级来生成差异。但是提交本身仍然是一个快照,它是 cherry-pick 命令找到 <i>commit</i>^1(第一个也是唯一的父级)与 commit 的差异并应用它。
可选阅读:它不只是一个补丁
从技术上讲,git cherry-pick 使用 Git 的 合并机制 进行了全面的三向合并。要了解这里为什么存在区别以及它是什么,我们必须深入了解差异、补丁和合并的杂草。
两个文件之间的diff——或者许多文件的两个快照——产生了一种配方。按照说明不会给你烤蛋糕(没有面粉、鸡蛋、黄油等)。相反,它将获取“之前”或“左侧”文件或文件集,并生成“之后”或“右侧”文件或文件集作为其结果。然后,这些说明包括“在第 30 行之后添加一行”或“在第 45 行删除三行”等步骤。
某些 diff 算法生成的精确指令集取决于该算法。 Git 最简单的差异仅使用两个:删除一些现有的行和在某个给定的起点之后添加一些新的行。这对于 new 文件和 deleted 文件来说还不够,所以我们可以添加 delete file F1 和 create all-new-file F2 。或者,在某些情况下,我们可以用 rename F1 to F2 替换 delete-file-F1-create-F2-instead,可选地进行其他更改。 Git 最复杂的差异使用所有这些。1
这为我们提供了一组简单的定义,这些定义不仅适用于 Git,也适用于许多其他系统。事实上,在 Git 之前有 diff 和 patch。另见the wikipedia article on patch。不过,这两者的一个非常简短的总结定义如下:
- diff:两个或多个文件的比较。
- 补丁:机器可读且适合机器应用的差异。
这些是在版本控制系统之外有用的,这就是它们早于 Git 的原因(尽管从技术上讲,版本控制可以追溯到 1950 年代的计算,并且在推广时可能有数千年的历史:我敢打赌,有多个不同的草图,例如,亚历山大的灯塔或 Djoser 金字塔)。但是我们可能会遇到补丁问题。假设有人拥有某个程序的第 1 版,并针对它的问题制作了补丁。后来,我们在版本 5 中发现了同样的问题。此时补丁可能应用,因为代码已经移动了——甚至可能移动到不同的文件,但肯定是在文件内。 上下文也可能发生了变化。
Larry Wall 的patch 程序使用所谓的偏移和fuzz 处理了这个问题。请参阅Why does this patch applied with a fuzz of 1, and fail with fuzz of 0?(这与"fuzzing" in modern software testing 非常不同。)但在真正的版本控制系统中,我们可以做得更好——有时会好很多。这就是三路合并的用武之地。
假设我们有一些软件,在存储库中有多个版本R。每个版本 Vi 都由一组文件组成。从 Vi 到 Vj 的差异会产生一个(机器可读的,即补丁)配方将版本 i 转换为版本 j。无论 i 和 j 的相对方向如何,这都有效,即,当 >j ≺ i(时髦的小花号是 precedes 符号,它允许使用 Git 样式的哈希 ID 以及像 SVN 这样的简单数字版本)。
现在假设我们通过比较 Vi 与 Vj 制作了补丁 p 。我们想应用补丁p到第三个版本,Vk。我们需要知道的是:
- 对于每个补丁的更改(并假设更改是“面向行的”,因为它们在这里):
-
Vk中的文件名对应Vi中的文件对 与 Vj 的变化?也就是说,也许我们正在修复一些函数
f(),但在版本 i 和 j 中,函数 f() 在文件 file1.ext 和版本 中k它在文件file2.ext中。
-
Vk中的哪些行对应于改变的行?也就是说,即使
f() 没有切换文件,它也可能因为f()f() 的大量删除或插入而被向上或向下移动了很多。李>
有两种方法可以获取此信息。我们可以比较 Vi 和 Vk,或者比较 Vj sub> 到 Vk。这两者都会为我们提供我们需要的答案(尽管在某些情况下使用答案的精确细节会有所不同)。如果我们像 Git 那样选择将 Vi 与 Vk 进行比较,这会给我们带来两个差异。
1Git 的 diff 也有一个“查找副本”选项,但它没有用于合并和挑选,而且我自己从来没有发现它有用。我认为它在内部有点不足,即,这是一个至少有一天需要更多工作的领域。
正则合并
现在我们再做一个观察:在正常的真正 Git 合并中,我们有这样的设置:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
每个大写字母代表一个提交。分支名称br1 和br2 分别选择提交J 和L,并且从这两个分支提示提交向后工作的历史汇集在一起——在提交H 处合并,它位于两个分支。
为了执行git merge br2,Git 找到所有这三个提交。然后它运行两个git diffs:一个比较H 与J,看看我们在分支br1 中发生了什么变化,第二个比较H 与L,以查看分支br2 中的他们 发生了什么变化。 Git 然后合并更改,如果合并成功,则从H 中的文件开始新的合并提交M,即:
因此是正确的合并结果。提交M 在图表中看起来像这样:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
但目前对我们来说更重要的是M 中的快照:M 中的快照保留我们的更改 ,即拥有我们在br1 中所做的一切,并添加它们的更改,即,获取提交K 和L 中发生的任何功能或错误修复。
樱桃采摘
我们的情况有点不同。我们有:
...--P--C--... <-- somebranch
我们还有:
...--K--L <-- ourbranch (HEAD)
... 部分可能与somebranch 联合之前 P-C 父/子提交对,或者可能联合之后 P-C提交对,或其他。也就是说,这两个都是有效的,尽管前者往往更常见:
...--P--C--... <-- somebranch
\
...--K--L <-- ourbranch (HEAD)
和:
...--P--C--... <-- somebranch
\
...--K--L <-- ourbranch (HEAD)
(在第二个示例中,在P-vs-C 中所做的任何更改通常已经在K 和L 中,这这就是它不太常见的原因。但是,有可能有人还原在... 部分之一中故意甚至错误地提交C。无论出于何种原因,我们现在希望再次进行这些更改.)
运行git cherry-pick 不只是比较P-vs-C。它确实做到了——这会产生我们想要的差异/补丁——但它会继续比较 P 和 L。因此,提交P 是git merge 样式比较中的合并基础。
从P 到L 的差异实际上意味着保留我们所有的差异。与真正合并中的 H-vs-K 示例一样,我们将在最终提交中保留所有更改。所以一个新的“合并”提交M 将有我们的变化。但是 Git 会添加P-vs-C 中的更改,所以我们也会选择补丁更改。
从P 到L 的差异提供了有关f() 已移动到哪个文件 函数的必要信息(如果它已移动)。从P 到L 的差异也提供了有关修补函数f() 所需的任何偏移 的必要信息。因此,通过使用合并机制,Git 获得了将补丁应用到正确文件的正确行的能力。
当 Git 进行最终“合并”提交 M 时,Git 并没有将其链接到 both 输入子项,而是将其链接回 only 以提交 @ 987654404@:
...--P--C--... <-- somebranch
\
...--K--L--M <-- ourbranch (HEAD)
即commit M这次是普通的单亲(非合并)commit。 L-vs-M 中的更改与P-vs-C 中的更改相同,除了行偏移的任何更改和可能需要的文件名。
现在,这里有一些警告。特别是,git diff 不会从某些合并库中识别 多个 派生文件。如果P-vs-C 中有适用于file1.ext 的更改,但这些更改需要拆分为两个文件 file2.ext 和file3.ext 在修补提交时@987654417 @,Git 不会注意到这一点。这有点太愚蠢了。此外,git diff 找到匹配的 lines: 它不懂编程,如果存在虚假匹配,例如很多右括号或括号或其他任何东西,可能会抛出 Git 的差异,以便它找到错误的匹配行。
请注意,Git 的 存储系统 在这里很好用。 diff 不够聪明。让git diff 更智能,这些类型的操作——merge 和cherry-picks——也变得更智能。2 不过,就目前而言,diff 操作,以及因此的 merge 和 cherry-picks,才是最重要的它们是:某人和/或某物应该始终通过运行自动化测试、查看文件或您能想到的任何其他方式(或所有这些方式的组合)来检查结果。
2他们将需要机器读取来自差异传递的任何更复杂的指令。在内部,在 diff 中,这一切都在一个大的 C 程序中,diff 引擎的作用几乎就像一个库,但原理都是一样的。这里有一个难题——适应新的差异输出——以及这个新差异的格式是文本的,如在生成差异然后应用它的单独程序中,还是二进制的,如产生更改记录的内部类库函数,您在这里所做的只是“移动困难”,正如一位同事过去所说的那样。