虽然这已经得到了很好的回答,但还有另一种方式来看待这一切。这就是 Git 本身看待它的方式。所有四个操作——cherry-pick、merge、rebase 和 revert——都使用相同的机制,--ours 和 --theirs 标志为 git checkout,以及 -X ours 和 -X theirs 扩展选项,结束引用相同的事物,使用相同的内部代码。我喜欢将这种机制称为作为动词的合并,因为我们首先通过git merge 了解它,此时合并必须进行真正的合并。
合并案例
在进行真正的合并时,这些术语是有意义的。我们从可以用这种方式说明的内容开始:
I--J <-- ourbranch (HEAD)
/
...--G--H
\
K--L <-- theirbranch
在这里,名称ourbranch 选择提交J,这是我们在分支上的提交(在这种情况下,这是两个这样的提交之一,尽管仅在我们自己的分支上的提交数量只需 至少 1 以强制进行真正的合并)。名称 theirbranch 选择提交 L,这是他们在其分支上的提交(同样是两个提交之一,这里至少需要一个提交)。
Git 为完成此合并(将某些文件集作为动词合并)所做的是,对于所有三个提交中的每个文件H、J 和 @987654336 @,比较H 和J 中的文件,看看我们 改变了什么,比较H 和L 中的文件,看看他们 改变了。然后 Git 组合这两组更改,将组合的更改应用到 H 中的任何内容。
提交H 是合并基础 提交,提交J 是“我们的”提交,而提交L 是“他们的”提交。任何差异,无论是“我们添加”的新文件,还是“他们删除”的文件,或者其他什么,都与提交 H 有关。
为了通过合并机制运行合并,Git 对以下内容进行了略微优化的提前版本:
-
设置:
- 将合并基础提交 (
H) 读取到插槽 1 的索引中
- 将
ours commit (HEAD = J) 读入插槽 2 的索引
- 将
theirs commit (L) 读入插槽 3 的索引
-
识别“相同的文件”。请注意,对每个文件都重复第 2 步和第 3 步。
- 如果三个插槽中都有一个名为 F 的文件,那么它就是同一个文件
- 否则,如果插槽 1 中有任何内容,请尝试猜测重命名,这会将插槽 1 中的合并基础文件绑定到插槽 2 中 不同名称 的我们或他们的文件,并且/ 或插槽 3;如果找不到需要重命名的文件,则我们和/或他们的一方删除了该文件;这些情况也可能导致高级别冲突,例如重命名/修改或重命名/删除,我们声明冲突并继续进行而不执行第 3 步
- 否则(插槽 1 中没有内容,但插槽 2 和 3 中的内容)我们会发生添加/添加冲突:声明此特定文件存在冲突,然后继续进行而不执行第 3 步
-
短路简单案例,并通过低级合并处理困难案例:
- 如果槽 1、2、3 中的 blob 哈希 ID 都匹配,则三个副本都相同;使用其中任何一个
- 如果槽 1 中的 blob 哈希 ID 与槽 2 或 3 中的匹配,则说明有人没有更改文件而有人更改了;使用更改后的文件,即采用 不同 的文件
- 否则,所有三个插槽都不同:通过更改块执行更改块行,低级合并
- 如果在低级合并期间发生合并冲突,
-X ours 或 -X theirs 表示“使用我们的/他们的解决冲突”,其中我们是插槽 2 中的任何内容,而他们是插槽 3 中的任何内容
- 请注意,这意味着无论没有冲突,例如,只有一个“边”更改了第 42 行,
-X 扩展选项根本不适用,我们采取修改,不管是我们的还是他们的
在此过程结束时,任何完全解析的文件都将移回其正常的零槽位置,并删除槽 1、2 和 3 条目。任何未解析的文件都被所有三个索引槽占用(在删除冲突和添加/添加冲突中,一些槽是空的,但 一些 非零阶段号槽正在使用中,这将文件标记为冲突)。
因此 to merge 或 merge as a verb 在 Git 的索引中运行
上述所有操作都发生在 Git 的索引中,其副作用是将更新的文件留在工作树中。如果存在低级冲突,您的工作树文件将使用冲突标记和与索引槽 1(合并基)、2(我们的)或3(他们的)。
最终它总是归结为相同的等式:1 = 合并基础,2 = 我们的,3 = 他们的。即使加载索引的命令不是git merge 也是如此。
Cherry-pick 和 revert 使用合并机制
当我们运行 git cherry-pick 时,我们有一个如下所示的提交图:
...--P--C--...
\
...--H <-- somebranch (HEAD)
这里的字母 P 和 C 代表任何父子提交对。 C 甚至可以是一个合并提交,只要我们使用-m 选项来指定要使用哪个父级。 (对于三个提交在图中的位置没有真正的限制:我用H 绘制它是P 之前的某个提交的子项,但它可以在P-C 对之后,如@例如 987654365@,或者如果您有多个不相交的子图,P-C 和 H 提交之间可能根本没有关系。)
当我们运行时:
git cherry-pick <hash-of-C>
Git 将使用从C 回到P 的父链接自行定位提交P。 P 现在充当合并基础,并被读入索引槽 1。C 充当--theirs 提交,并被读入索引槽 3。我们当前的提交 H 是 --ours 提交,并被读入索引槽 2。合并机制现在运行,所以“我们的”提交是 HEAD,“他们的”提交是提交 C,如果我们将 merge.conflictStyle 设置为 @,则会显示合并基础987654380@,或者如果我们使用git mergetool 来运行合并工具——提交P。
当我们运行时:
git revert <hash-of-C>
同样的事情发生了,除了这一次,commit C 是 slot 1 中的合并基础,commit P 是 slot 3 中的 --theirs commit。 slot 2 中的 --ours commit 来自 @ 987654388@像往常一样。
请注意,如果您对一系列提交使用cherry-pick 或revert:
git cherry-pick stop..start
挑选工作首先使用拓扑上较旧的提交一次提交一个提交,而恢复工作一次提交一次使用拓扑上较新的提交。也就是说,给定:
...--C--D--E--...
\
H <-- HEAD
git cherry-pick C..E 先复制 D,然后是 E,但 git revert C..E 先还原 E,然后是 D。 (提交C 不起作用,因为两点语法排除了从两点表达式左侧可到达的提交。有关更多信息,请参阅the gitrevisions documentation。)
rebase 是重复的樱桃采摘
rebase 命令通过重复运行git cherry-pick 来工作,之后 使用git checkout --detach 或git switch --detach 进入分离的HEAD 模式。 (从技术上讲,它现在只是在内部执行此操作;在过去,git rebase 的一些基于 shell 脚本的版本确实使用了 git checkout,尽管哈希 ID 总是进入分离模式。)
当我们运行git rebase 时,我们从这样的内容开始:
C--D--E <-- ourbranch (HEAD)
/
...--B--F--G--H <-- theirbranch
我们跑:
git checkout ourbranch # if needed - the above says we already did that
git rebase theirbranch # or, git rebase --onto <target> <upstream>
第一个——嗯,第二个——这样做是进入分离 HEAD 模式,HEAD 提交是我们使用 --onto 参数选择的提交。如果我们没有使用单独的--onto 标志和参数,--onto 来自我们确实给出的一个参数,在这种情况下,theirbranch。如果我们不使用单独的 upstream 参数,那么我们提供的一个参数(在本例中为 theirbranch)用于这两个目的。
Git 也(首先,这就是为什么上面是第二个)列出了每个要复制的提交的原始哈希 ID。这个列表比乍看起来要复杂得多,但如果我们忽略额外的复杂性,它基本上是以下结果:
git rev-list --topo-order --reverse <hash-of-upstream>..HEAD
在这种情况下是提交 C、D 和 E 的哈希 ID:可以从 ourbranch 访问的三个提交也不能从 theirbranch 访问。
git rebase 已经生成了这个列表并进入了 detached-HEAD 模式,我们现在看起来像这样:
C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch, HEAD
现在 Git 运行一个 git cherry-pick。它的参数是提交C 的哈希ID,这是要复制的第一个提交。如果我们看看上面的cherry-pick是如何工作的,我们会看到这是一个merge-as-a-verb操作,合并基础是C的父级,即提交B,当前或@987654424 @commit 是commit H,而要复制的或--theirs commit 是commit C。所以这就是为什么我们的和他们的似乎相反。
一旦这个樱桃挑选操作完成,我们现在有:
C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch
\
C' <-- HEAD
Git 现在继续复制提交 D 和 git cherry-pick。合并基础现在是提交 C,--ours 提交是提交 C',--theirs 提交是 D。这意味着 ours 和 theirs 提交都是我们的,但这次“ours”提交是我们在几秒(或几毫秒)前刚刚构建的!
它基于现有的提交 H,这是他们的,但它是提交 C',这是我们的。如果我们遇到任何合并冲突,它们无疑是基于H 的结果,可能包括我们手动执行的某种冲突解决方案以生成C'。但是,从字面上看,所有三个输入提交都是我们的。索引槽#1 来自提交C,索引槽#2 来自提交C',索引槽#3 来自提交D。
一旦我们完成了这一切,我们的图片现在是:
C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch
\
C'-D' <-- HEAD
Git 现在在提交 E 的哈希上运行 git cherry-pick。合并基础是提交D,我们和他们的提交分别是D' 和E。所以再一次,在 rebase 期间,所有三个提交都是我们的——尽管合并冲突可能是在 H 上构建的结果。
当最后一个樱桃选择完成后,Git 通过将 name ourbranch 拉出旧提交 E 并将其粘贴到新提交 E' 来完成变基:
C--D--E [abandoned]
/
...--B--F--G--H <-- theirbranch
\
C'-D'-E' <-- ourbranch (HEAD)
我们现在又回到了正常的附加头工作模式,因为git log 从我们现在的位置开始——在提交E'——并且向后工作,它永远不会访问原始提交C,它看起来像尽管我们以某种方式修改了最初的三个提交。我们还没有:它们仍然存在,在我们的存储库中,可以通过特殊的伪引用 ORIG_HEAD 获得,也可以通过我们的 reflogs 获得。默认情况下,我们可以将它们取回至少 30 天,之后git gc 可以随意收割它们,然后它们就真的消失了。 (好吧,只要我们不将它们git push 发送到仍然保留它们的一些其他 Git 存储库。)