cherry-picking commit - 是提交快照还是补丁?
cherry-picking commit - is commit a snapshot or patch?
我有一个与 cherry-picking 提交和冲突相关的问题。
提交的 'Pro Git' 书 explains 是一种快照,而不是 patches/diffs。
但是 cherry-picking 提交可能表现为补丁。
下面的示例,简而言之:
创建 3 次提交,每次编辑文件的第一行(和单行)
将分支重置为第一次提交
test1:尝试挑选第三次提交(冲突)
测试 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 都包含一些文件集。从 Vi 到 Vj 进行比较会产生将版本 i 转换为版本 j 的(machine-readable,即补丁)配方。这与 i 和 j 的相对方向无关,即我们可以 "back in time" 到 older 版本 j ≺ i(时髦的卷曲 less-than 是 precedes 符号,它允许 Git-style 哈希 ID 以及像 SVN 这样的简单数字版本。
现在假设我们通过比较 Vi 和 Vj。我们想应用补丁p到第三个版本,Vk 。我们需要知道的是:
- 对于每个补丁的变化(并假设变化是 "line oriented",因为它们在这里):
- 什么文件名在Vk对应的file-pair在Vi vs Vj 这个变化?也就是说,也许我们正在修复一些函数
f()
,但在版本 i 和 j 中,函数 f()
在文件 file1.ext
和版本 k 它在文件 file2.ext
. 中
- Vk中的行对应修改后的行?也就是说,即使
f()
没有切换 文件 ,也可能由于大量删除或插入 以上 [=292] 而向上或向下移动了很多=] f()
.
有两种方法可以获取此信息。我们可以比较 Vi 和 Vk,或者比较 Vj 和 Vk。这两种方法都会为我们提供所需的答案(尽管 使用 的精确细节在某些情况下会有所不同)。如果我们选择——像Git那样——比较Vi和Vk,这给了我们两个差异。
1Git的diff也有一个"find copies"选项,但是在merge和cherry-pick中没有用到,我'我自己从来没有发现它有用。我认为它在内部有点不足,也就是说,这是一个——至少有一天——需要更多工作的领域。
常规合并
现在我们再做一个观察:在正常的 true Git 合并中,我们有这样的设置:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
其中每个大写字母代表一次提交。分支名称 br1
和 br2
select 分别提交 J
和 L
,并且从这两个 branch-tip 提交向后工作的历史汇集在一起 - 加入向上——在 H
提交时,它在 both 个分支上。
要执行 git merge br2
,Git 会找到 所有这三个提交 。然后它运行两个 git diff
s:一个比较 H
与 J
,以查看 we 在分支 br1
中更改了什么,以及第二个比较 H
与 L
,看看 他们 在分支 br2
中改变了什么。 Git 然后 合并更改 ,如果此合并成功,则进行新的合并提交 M
,从 H
中的文件开始,即:
- 保留我们的更改,而且
- 添加他们的更改
因此是正确的合并结果。提交 M
在 图中看起来像这样:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
但 M
中的 快照 目前对我们来说更重要:[=47 中的 快照 =] 保留我们的更改,即拥有我们在br1
中所做的一切,并且添加他们的更改,即获得任何功能或bug-fixes 发生在提交 K
和 L
中。
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
中所做的任何 更改 通常 已经在 K
和 L
,这就是它不太常见的原因。但是,有人 reverted 可能会在 C
之一 ...
部分,有意或什至是错误的。无论出于何种原因,我们现在都希望再次进行这些更改。)
运行 git cherry-pick
不 只是 比较 P
-vs-C
。它确实这样做了——这产生了我们想要的差异/补丁——但它随后继续比较 P
与 L
。因此,提交 P
是 合并基础 git merge
样式比较。
从 P
到 L
的差异实际上意味着 保持我们所有的差异。与真正合并中的 H
-vs-K
示例一样,我们将 将所有更改 保留在最终提交中。因此,新的 "merge" 提交 M
将包含我们的更改。但是 Git 将 添加到此 P
-vs-C
中的更改,因此我们也会选择补丁更改。
从 P
到 L
的差异提供了有关 文件 功能 f()
已移动到哪个的必要信息,如果它已移动.从 P
到 L
的差异提供了有关修补函数 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
中的changes与P
-vs中的changes相同-C
,除了可能需要的行偏移和文件名的任何更改。
现在,这里有一些注意事项。特别是,git diff
不识别来自某些合并库的 多个 派生文件。如果 P
-vs-C
中有适用于 file1.ext
的更改,但这些更改需要 拆分为两个文件 file2.ext
和 file3.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",正如一位同事曾经说过的那样。
我有一个与 cherry-picking 提交和冲突相关的问题。
提交的 'Pro Git' 书 explains 是一种快照,而不是 patches/diffs。
但是 cherry-picking 提交可能表现为补丁。
下面的示例,简而言之:
创建 3 次提交,每次编辑文件的第一行(和单行)
将分支重置为第一次提交
test1:尝试挑选第三次提交(冲突)
测试 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 都包含一些文件集。从 Vi 到 Vj 进行比较会产生将版本 i 转换为版本 j 的(machine-readable,即补丁)配方。这与 i 和 j 的相对方向无关,即我们可以 "back in time" 到 older 版本 j ≺ i(时髦的卷曲 less-than 是 precedes 符号,它允许 Git-style 哈希 ID 以及像 SVN 这样的简单数字版本。
现在假设我们通过比较 Vi 和 Vj。我们想应用补丁p到第三个版本,Vk 。我们需要知道的是:
- 对于每个补丁的变化(并假设变化是 "line oriented",因为它们在这里):
- 什么文件名在Vk对应的file-pair在Vi vs Vj 这个变化?也就是说,也许我们正在修复一些函数
f()
,但在版本 i 和 j 中,函数f()
在文件file1.ext
和版本 k 它在文件file2.ext
. 中
- Vk中的行对应修改后的行?也就是说,即使
f()
没有切换 文件 ,也可能由于大量删除或插入 以上 [=292] 而向上或向下移动了很多=]f()
.
- 什么文件名在Vk对应的file-pair在Vi vs Vj 这个变化?也就是说,也许我们正在修复一些函数
有两种方法可以获取此信息。我们可以比较 Vi 和 Vk,或者比较 Vj 和 Vk。这两种方法都会为我们提供所需的答案(尽管 使用 的精确细节在某些情况下会有所不同)。如果我们选择——像Git那样——比较Vi和Vk,这给了我们两个差异。
1Git的diff也有一个"find copies"选项,但是在merge和cherry-pick中没有用到,我'我自己从来没有发现它有用。我认为它在内部有点不足,也就是说,这是一个——至少有一天——需要更多工作的领域。
常规合并
现在我们再做一个观察:在正常的 true Git 合并中,我们有这样的设置:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
其中每个大写字母代表一次提交。分支名称 br1
和 br2
select 分别提交 J
和 L
,并且从这两个 branch-tip 提交向后工作的历史汇集在一起 - 加入向上——在 H
提交时,它在 both 个分支上。
要执行 git merge br2
,Git 会找到 所有这三个提交 。然后它运行两个 git diff
s:一个比较 H
与 J
,以查看 we 在分支 br1
中更改了什么,以及第二个比较 H
与 L
,看看 他们 在分支 br2
中改变了什么。 Git 然后 合并更改 ,如果此合并成功,则进行新的合并提交 M
,从 H
中的文件开始,即:
- 保留我们的更改,而且
- 添加他们的更改
因此是正确的合并结果。提交 M
在 图中看起来像这样:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
但 M
中的 快照 目前对我们来说更重要:[=47 中的 快照 =] 保留我们的更改,即拥有我们在br1
中所做的一切,并且添加他们的更改,即获得任何功能或bug-fixes 发生在提交 K
和 L
中。
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
中所做的任何 更改 通常 已经在 K
和 L
,这就是它不太常见的原因。但是,有人 reverted 可能会在 C
之一 ...
部分,有意或什至是错误的。无论出于何种原因,我们现在都希望再次进行这些更改。)
运行 git cherry-pick
不 只是 比较 P
-vs-C
。它确实这样做了——这产生了我们想要的差异/补丁——但它随后继续比较 P
与 L
。因此,提交 P
是 合并基础 git merge
样式比较。
从 P
到 L
的差异实际上意味着 保持我们所有的差异。与真正合并中的 H
-vs-K
示例一样,我们将 将所有更改 保留在最终提交中。因此,新的 "merge" 提交 M
将包含我们的更改。但是 Git 将 添加到此 P
-vs-C
中的更改,因此我们也会选择补丁更改。
从 P
到 L
的差异提供了有关 文件 功能 f()
已移动到哪个的必要信息,如果它已移动.从 P
到 L
的差异提供了有关修补函数 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
中的changes与P
-vs中的changes相同-C
,除了可能需要的行偏移和文件名的任何更改。
现在,这里有一些注意事项。特别是,git diff
不识别来自某些合并库的 多个 派生文件。如果 P
-vs-C
中有适用于 file1.ext
的更改,但这些更改需要 拆分为两个文件 file2.ext
和 file3.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",正如一位同事曾经说过的那样。