cherry-picking commit - 是提交快照还是补丁?

cherry-picking commit - is commit a snapshot or patch?

我有一个与 cherry-picking 提交和冲突相关的问题。

提交的 'Pro Git' 书 explains 是一种快照,而不是 patches/diffs。

但是 cherry-picking 提交可能表现为补丁。


下面的示例,简而言之:

  1. 创建 3 次提交,每次编辑文件的第一行(和单行)

  2. 将分支重置为第一次提交

  3. test1:尝试挑选第三次提交(冲突)

  4. 测试 2:尝试挑选第二次提交(确定)


mkdir gitlearn
cd gitlearn

touch file
git init
Initialized empty Git repository in /root/gitlearn/.git/

git add file

#fill file by single 'A'
echo A > file && cat file
A

git commit file -m A
[master (root-commit) 9d5dd4d] A
 1 file changed, 1 insertion(+)
 create mode 100644 file

#fill file by single 'B'
echo B > file && cat file
B

git commit file -m B
[master 28ad28f] B
 1 file changed, 1 insertion(+), 1 deletion(-)

#fill file by single 'C'
echo C > file && cat file
C

git commit file -m C
[master c90c5c8] C
 1 file changed, 1 insertion(+), 1 deletion(-)

git log --oneline
c90c5c8 C
28ad28f B
9d5dd4d A

测试 1

#reset the branch to 9d5dd4d ('A' version)
git reset --hard HEAD~2
HEAD is now at 9d5dd4d A

git log --oneline
9d5dd4d A

#cherry-pick 'C' version over 'A'
git cherry-pick c90c5c8
error: could not apply c90c5c8... C
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'

#the conflict:
cat file
<<<<<<< HEAD
A
=======
C
>>>>>>> c90c5c8... C

测试 2

#same for 'B' - succeeds
git reset --hard HEAD
HEAD is now at 9d5dd4d A

git cherry-pick 28ad28f
[master eb27a49] B
 1 file changed, 1 insertion(+), 1 deletion(-)

请解释为什么测试 1 失败(如果提交是补丁而不是快照,我可以想象答案?)

Pro Git 书是正确的:一次提交就是一个快照。

不过你也是对的:git cherry-pick应用了一个补丁。 (好吧,有点:请参阅下面的更多详细信息。)

怎么会这样?答案是,当您 cherry-pick 提交时,您还使用 -m <em>parent-number</em> 参数指定要考虑的 parent 提交。然后 cherry-pick 命令针对 parent 生成一个差异,以便现在可以应用结果差异。

如果你选择 cherry-pick 一个 non-merge 提交,只有一个 parent,所以你实际上没有传递 -m 并且命令使用 ( single) parent 生成差异。但是提交本身仍然是一个快照,它是 cherry-pick 命令找到 <i>commit</i>^1(第一个也是唯一的 parent)与 commit 的差异 并应用它。

可选阅读:这不是只是一个补丁

技术上,git cherry-pick 使用 Git 的 合并机制 进行 full-blown three-way 合并。要理解为什么这里有区别,以及它是什么,我们必须深入了解差异、补丁和合并的杂草。

两个文件(或许多文件的两个快照)之间的 差异 产生一种配方。按照说明不会给你烤蛋糕(没有面粉、鸡蛋、黄油等)。相反,它将获取 "before" 或 "left hand side" 文件或文件集,并生成 "after" 或 "right hand side" 文件或文件集作为其结果。然后,说明包括 "add a line after line 30" 或 "remove three lines at line 45".

等步骤

某些 diff 算法生成的精确指令集取决于该算法。 Git 最简单的差异仅使用两个:删除一些现有行在给定起点后添加一些新行。这对于 new 文件和 deleted 文件来说还不够,所以我们可以添加 delete file F1创建all-new-fileF2。或者,在某些情况下,我们可能会将 delete-file-F1-create-F2-replace 替换为 rename F1 to F2,可选地进行其他更改。 Git 最复杂的差异使用所有这些。1

这为我们提供了一组简单的定义,不仅适用于 Git,而且适用于许多其他系统。事实上,在 Git 之前还有 diff and patch. See also the wikipedia article on patch。两者的一个非常简短的总结定义如下:

  • diff:比较两个或多个文件。
  • 补丁:machine-readable 的 diff,适用于 machine-applying。

这些在 版本控制系统之外很有用,这就是它们早于 Git 的原因(尽管从技术上讲不是版本控制,它可以追溯到 1950 年代的计算,并且可能有数千年的概括:我敢打赌,有多个不同的草图,例如,亚历山大港的灯塔或左塞尔金字塔)。但是我们可能会遇到补丁问题。假设某人拥有某个程序的版本 1,并为它的问题打了补丁。后来,我们在版本 5 中发现了同样的问题。此时补丁很可能 apply,因为代码已经四处移动——甚至可能移动到不同的文件,但肯定是在文件内。 上下文 可能也已更改。

Larry Wall 的 patch 程序使用它所谓的偏移和 fuzz. See Why does this patch applied with a fuzz of 1, and fail with fuzz of 0? (This is very different from "fuzzing" in modern software testing 来处理这个问题。)但在真正的版本控制系统中,我们可以做得更好——有时甚至更好。这就是 三向合并 发挥作用的地方。

假设我们有一些软件,在存储库 R 中有多个版本。每个版本 Vi 都包含一些文件集。从 ViVj 进行比较会产生将版本 i 转换为版本 j 的(machine-readable,即补丁)配方。这与 ij 的相对方向无关,即我们可以 "back in time" 到 older 版本 j ≺ i(时髦的卷曲 less-than 是 precedes 符号,它允许 Git-style 哈希 ID 以及像 SVN 这样的简单数字版本。

现在假设我们通过比较 ViVj。我们想应用补丁p到第三个版本,Vk 。我们需要知道的是:

  • 对于每个补丁的变化(并假设变化是 "line oriented",因为它们在这里):
    • 什么文件名Vk对应的file-pair在Vi vs Vj 这个变化?也就是说,也许我们正在修复一些函数 f(),但在版本 ij 中,函数 f() 在文件 file1.ext 和版本 k 它在文件 file2.ext.
    • Vk中的对应修改后的行?也就是说,即使 f() 没有切换 文件 ,也可能由于大量删除或插入 以上 [=292] 而向上或向下移动了很多=] f().

有两种方法可以获取此信息。我们可以比较 ViVk,或者比较 VjVk。这两种方法都会为我们提供所需的答案(尽管 使用 的精确细节在某些情况下会有所不同)。如果我们选择——像Git那样——比较ViVk,这给了我们两个差异。


1Git的diff也有一个"find copies"选项,但是在merge和cherry-pick中没有用到,我'我自己从来没有发现它有用。我认为它在内部有点不足,也就是说,这是一个——至少有一天——需要更多工作的领域。


常规合并

现在我们再做一个观察:在正常的 true Git 合并中,我们有这样的设置:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

其中每个大写字母代表一次提交。分支名称 br1br2 select 分别提交 JL,并且从这两个 branch-tip 提交向后工作的历史汇集在一起​​ - 加入向上——在 H 提交时,它在 both 个分支上。

要执行 git merge br2,Git 会找到 所有这三个提交 。然后它运行两个 git diffs:一个比较 HJ,以查看 we 在分支 br1 中更改了什么,以及第二个比较 HL,看看 他们 在分支 br2 中改变了什么。 Git 然后 合并更改 ,如果此合并成功,则进行新的合并提交 M,从 H 中的文件开始,即:

  • 保留我们的更改,而且
  • 添加他们的更改

因此是正确的合并结果。提交 M 图中看起来像这样:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

M 中的 快照 目前对我们来说更重要:[=47 中的 快照 =] 保留我们的更改,即拥有我们在br1中所做的一切,并且添加他们的更改,即获得任何功能或bug-fixes 发生在提交 KL 中。

Cherry-picking

我们的情况有点不同。我们有:

...--P--C--...   <-- somebranch

我们还有:

...--K--L   <-- ourbranch (HEAD)

其中 ... 部分可能与 somebranch P-C parent/child 提交对之前加入,或者可能加入在 P-C 提交对或其他任何东西之后。也就是说,这两个都是有效的,尽管前者往往更常见:

...--P--C--...   <-- somebranch
   \
    ...--K--L   <-- ourbranch (HEAD)

和:

...--P--C--...   <-- somebranch
             \
              ...--K--L   <-- ourbranch (HEAD)

(在第二个示例中,在 P-vs-C 中所做的任何 更改 通常 已经在 KL,这就是它不太常见的原因。但是,有人 reverted 可能会在 C 之一 ... 部分,有意或什至是错误的。无论出于何种原因,我们现在都希望再次进行这些更改。)

运行 git cherry-pick 只是 比较 P-vs-C。它确实这样做了——这产生了我们想要的差异/补丁——但它随后继续比较 PL。因此,提交 P 合并基础 git merge 样式比较。

PL 的差异实际上意味着 保持我们所有的差异。与真正合并中的 H-vs-K 示例一样,我们将 将所有更改 保留在最终提交中。因此,新的 "merge" 提交 M 将包含我们的更改。但是 Git 将 添加到此 P-vs-C 中的更改,因此我们也会选择补丁更改。

PL 的差异提供了有关 文件 功能 f() 已移动到哪个的必要信息,如果它已移动.从 PL 的差异提供了有关修补函数 f() 所需的任何 offset 的必要信息。因此,通过使用合并机制,Git 获得了将补丁应用到正确文件的正确行的能力。

当 Git 进行最后的 "merge" 提交 M 时,而不是 link 将其 both 输入children,Git link 返回 提交 L

...--P--C--...   <-- somebranch
   \
    ...--K--L--M   <-- ourbranch (HEAD)

即commit M 这次是一个普通的single-parent (non-merge) commit。 L-vs-M中的changesP-vs中的changes相同-C,除了可能需要的行偏移和文件名的任何更改。

现在,这里有一些注意事项。特别是,git diff 不识别来自某些合并库的 多个 派生文件。如果 P-vs-C 中有适用于 file1.ext 的更改,但这些更改需要 拆分为两个文件 file2.extfile3.ext 修补提交 L 时,Git 不会注意到这一点。这有点太愚蠢了。此外,git diff 找到匹配的 行: 它不理解编程,如果存在虚假匹配,例如大量的大括号或 parentheses 或其他,可以摆脱 Git 的差异,以便它找到 错误的 匹配行。

请注意 Git 的 存储系统 在这里就可以了。是 diff 不够聪明。让 git diff 更智能,这些类型的操作——合并和 cherry-picks——也变得更智能。2 不过现在,diff 操作,以及因此merges 和 cherry-picks,它们是什么:某人 and/or 某事应该 总是 检查结果,通过 运行 自动化测试,或查看文件,或您能想到的任何其他内容(或所有这些的组合)。


2他们将需要 machine-read diff pass 中的任何 more-complex 指令。在内部,在 diff 中,这一切都在一个大型 C 程序中,diff 引擎几乎就像一个库,但两种方式的原理都是相同的。这里有一个难题——适应新的差异输出——以及这个新差异的格式是文本的,就像在产生差异然后应用它的单独程序中一样,还是二进制的,就像在产生差异的内部 library-like 函数中一样更改记录,您在这里所做的只是 "moving the hard around",正如一位同事曾经说过的那样。