【问题标题】:cherry-picking commit - is commit a snapshot or patch?挑选提交 - 提交是快照还是补丁?
【发布时间】:2015-03-01 20:28:39
【问题描述】:

我有一个与挑选提交和冲突有关的问题。

“Pro Git”书 explains 提交是一种快照,而不是补丁/差异。

但是挑选提交的行为可能就像一个补丁一样。


下面的例子,简而言之:

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

  2. 将分支重置为首次提交

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

  4. 测试 2:尝试选择第二次提交(OK)


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 失败(如果提交是补丁,我可以想象答案,但快照?)

【问题讨论】:

  • 第一次测试出现合并冲突。由于您在第二次进行了重置,因此它是成功的。
  • 先生。 Polywhirl,感谢您的回答,但是:1)不确定为什么您破坏了原始帖子的格式 2)在两次测试之前都进行了硬重置
  • 堆栈溢出在检测代码块时可能有点敏感。 @Mr.Polywhirl 实际上并没有尝试更改任何内容,只是将其设置为编号列表,正如您在修订历史记录中看到的那样,通过单击并排降价。虽然可以在编辑预览中发现格式问题,但我从经验中知道这是多么容易被忽略。

标签: git cherry-pick


【解决方案1】:

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 F1create all-new-file F2 。或者,在某些情况下,我们可以用 rename F1 to F2 替换 delete-file-F1-create-F2-instead,可选地进行其他更改。 Git 最复杂的差异使用所有这些。1

这为我们提供了一组简单的定义,这些定义不仅适用于 Git,也适用于许多其他系统。事实上,在 Git 之前有 diffpatch。另见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 都由一组文件组成。从 ViVj 的差异会产生一个(机器可读的,即补丁)配方将版本 i 转换为版本 j。无论 ij 的相对方向如何,这都有效,即,当 >j ≺ i(时髦的小花号是 precedes 符号,它允许使用 Git 样式的哈希 ID 以及像 SVN 这样的简单数字版本)。

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

  • 对于每个补丁的更改(并假设更改是“面向行的”,因为它们在这里):
    • Vk中的文件名对应Vi中的文件对 与 Vj 的变化?也就是说,也许我们正在修复一些函数 f(),但在版本 ij 中,函数 f() 在文件 file1.ext 和版本 中k它在文件file2.ext中。
    • Vk中的哪些对应于改变的行?也就是说,即使f() 没有切换文件,它也可能因为f()f() 的大量删除或插入而被向上或向下移动了很多。李>

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


1Git 的 diff 也有一个“查找副本”选项,但它没有用于合并和挑选,而且我自己从来没有发现它有用。我认为它在内部有点不足,即,这是一个至少有一天需要更多工作的领域。


正则合并

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

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

每个大写字母代表一个提交。分支名称br1br2 分别选择提交JL,并且从这两个分支提示提交向后工作的历史汇集在一起​​——在提交H 处合并,它位于两个分支。

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

  • 保留我们的更改,但也
  • 添加他们的更改

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

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

但目前对我们来说更重要的是M 中的快照M 中的快照保留我们的更改 ,即拥有我们在br1 中所做的一切,并添加它们的更改,即,获取提交KL 中发生的任何功能或错误修复。

樱桃采摘

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

...--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 中所做的任何更改通常已经KL 中,这这就是它不太常见的原因。但是,有可能有人还原... 部分之一中故意甚至错误地提交C。无论出于何种原因,我们现在希望再次进行这些更改.)

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

PL 的差异实际上意味着保留我们所有的差异。与真正合并中的 H-vs-K 示例一样,我们将在最终提交中保留所有更改。所以一个新的“合并”提交M 将有我们的变化。但是 Git 会添加P-vs-C 中的更改,所以我们也会选择补丁更改。

PL 的差异提供了有关f() 已移动到哪个文件 函数的必要信息(如果它已移动)。从PL 的差异也提供了有关修补函数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.extfile3.ext 在修补提交时@987654417 @,Git 不会注意到这一点。这有点太愚蠢了。此外,git diff 找到匹配的 lines: 它不懂编程,如果存在虚假匹配,例如很多右括号或括号或其他任何东西,可能会抛出 Git 的差异,以便它找到错误的匹配行。

请注意,Git 的 存储系统 在这里很好用。 diff 不够聪明。让git diff 更智能,这些类型的操作——merge 和cherry-picks——也变得更智能。2 不过,就目前而言,diff 操作,以及因此的 merge 和 cherry-picks,才是最重要的它们是:某人和/或某物应该始终通过运行自动化测试、查看文件或您能想到的任何其他方式(或所有这些方式的组合)来检查结果。


2他们将需要机器读取来自差异传递的任何更复杂的指令。在内部,在 diff 中,这一切都在一个大的 C 程序中,diff 引擎的作用几乎就像一个库,但原理都是一样的。这里有一个难题——适应新的差异输出——以及这个新差异的格式是文本的,如在生成差异然后应用它的单独程序中,还是二进制的,如产生更改记录的内部类库函数,您在这里所做的只是“移动困难”,正如一位同事过去所说的那样。

【讨论】:

  • 对不起,我还是不明白。从你解释的最后一段 - 我的 test1 和 test2 中的 commit^1commit 是什么?
  • Git 有丰富的语法来处理提交图。您可以通过其原始 SHA-1 (c18b86734113... 等) 命名提交,但您也可以通过跟踪提交图的路径来命名它。每个提交记录其父提交,^ 语法选择一个特定的父提交。您将在第 3 章中看到更多关于提交图的信息,例如,git-scm.com/book/en/v2/…,以及在第 7 章中关于修订选择器的更多信息:git-scm.com/book/en/v2/Git-Tools-Revision-Selection
  • Torek,我以错误的方式提出了我的问题,我知道 SHA-1、^ 和 ~。让我们回到我的三个提交 A、B 和 C。在上一句中,您谈到了提交与其父级之间的差异,这是由 c-p 应用的。所以,我的问题是:在 test1 - 这些提交是 9d5dd4d (A) 和 c90c5c8 (C),对吗?所以他们的差异是“A\n”与“C\n”,这就是我不明白的一点。为什么这个樱桃选择失败而 test2 的差异为“A\n”与“B\n”成功?
  • 啊。不,当您要求挑选c90c5c8 时,git 会找到它的父级,即28ad28f。试试看:git rev-parse c90c5c8^git diff c90c5c8^ c90c5c8。一旦提交存在,它就永远不会改变。 git reset 不会更改任何现有的提交,它只会更改当前分支名称解析到的提交(再次,使用 git rev-parse 来证明这一点:git rev-parse master 将显示其当前的原始 SHA-1)。
  • Cherry-pick 将提交与其父级进行比较,因为它是这样定义的。您给 git 一个或多个提交 ID,git 会找到每个命名提交的父级(或 -m'th 父级,如果合并)并进行差异,然后尝试应用该差异。在这种情况下,差异是“将 B 更改为 C”并且没有 B,因此您会看到观察到的补丁应用程序错误。 (如果你想做其他事情,那么你不需要cherry-pick 命令。例如,如果你只想要某个文件的特定快照版本,请使用git checkout &lt;revspec&gt; -- &lt;path&gt;git show &lt;revspec&gt;:&lt;path&gt;。)
猜你喜欢
  • 1970-01-01
  • 2012-09-05
  • 2014-07-04
  • 2021-01-18
  • 1970-01-01
  • 2012-07-22
  • 2012-04-08
  • 1970-01-01
  • 2018-05-08
相关资源
最近更新 更多