尽管已经有很好的回答了,但还有一种方法可以看待这个问题。这就是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
和
L
中的每个文件,Git会将
H
与
J
中的文件进行比较以查看我们改变了什么,将
H
与
L
中的文件进行比较以查看他们改变了什么。然后,Git组合这两组更改,将组合的更改应用于
H
中的任何内容。
提交
H
是合并基础提交,提交
J
是“我们”的提交,提交
L
是“他们”的提交。任何“差异”,无论是我们“添加的新文件”,还是他们“删除的文件”,或者其他任何差异,都是相对于提交
H
而言的。
为了通过合并机制运行合并,Git执行了以下略微优化的预处理:
初始化:
- 将基础合并提交(
H
)读入到索引的第一个插槽中
- 将我们的提交(
HEAD
= J
)读入到索引的第二个插槽中
- 将他们的提交(
L
)读入到索引的第三个插槽中
识别"相同文件"。注意步骤2和3对每个文件都要重复执行。
- 如果所有三个插槽中都有名为F的文件,则它是相同的文件
- 否则,如果插槽1中有任何东西,则尝试猜测重命名,这会将位于插槽1中的合并基文件与位于插槽2和/或3中的不同名称的我们或他们的文件联系起来;如果找不到重命名的文件,则我们的和/或他们的一方删除了此文件;这些情况可能导致高级冲突,例如重命名/修改或重命名/删除,在这种情况下,我们宣布存在冲突并继续执行步骤3
- 否则(插槽1中没有任何内容,但插槽2和3中有内容),我们有一个添加/添加冲突:宣布此特定文件存在冲突,并继续执行步骤3
短路易于处理的情况,并使用低级别合并处理困难情况:
- 如果插槽1、2和3中的blob哈希值都匹配,则所有三个副本相同;使用任何一个
- 如果插槽1中的blob哈希值与插槽2或3中的哈希值匹配,则有人没有更改该文件,而有人更改了该文件;使用更改的文件,即使用不同的文件
- 否则,所有三个插槽都不同:按块更改进行按块低级别合并
- 如果在低级别合并期间出现合并冲突,则
-X ours
或-X theirs
表示"使用ours/theirs解决冲突",其中ours是插槽2中的内容,theirs是插槽3中的内容
- 请注意,这意味着无论是否存在冲突,例如只有一侧更改了第42行,扩展选项
-X
都根本不适用,我们将采用修改,无论其是否属于我们或他们
在此过程结束时,任何完全解决的文件都将移回其正常的零插槽位置,插槽1、2和3的条目将被删除。任何未解决的文件都保留了所有三个索引插槽(在删除冲突和添加/添加冲突中,某些插槽为空,但使用某些非零阶段号插槽,这标记该文件存在冲突)。
因此,合并或作为动词的合并操作在Git的索引中进行
所有上述操作都发生在Git的索引中,带来的副作用是将更新的文件留在您的工作目录中。如果存在低级冲突,则会使用冲突标记标记您的工作树文件以及对应于位于索引槽1(合并基础)、2(我们的)或3(他们的)文件副本所在行的各个部分。
最终,这总是归结为相同的公式:1 = 合并基础,2 = 我们的,3 = 他们的。即使加载索引的命令不是git merge
,这也适用。
Cherry-pick和revert使用合并机制
当我们运行git cherry-pick
时,我们拥有如下提交记录图:
...--P--C--...
\
...--H <-- somebranch (HEAD)
P
和
C
代表任何父子提交对。即使是合并提交,只要使用
-m
选项来指定使用哪个父节点也可以。(在图形中三个提交的位置没有实际的限制:我将其画为
H
是在某个在
P
之前的提交的子节点,但它可以在
P-C
对之后,例如
...-E-P-C-F-G-H
,或者可能根本没有
P-C
和
H
提交之间的关系,如果您有多个不相交的子图。)
当我们运行以下命令时:
git cherry-pick <hash-of-C>
Git会自行查找提交记录P
,使用从C
到P
的父链接。现在,P
充当合并基础,并被读入索引槽1。C
充当--theirs
提交,并被读入索引槽3。我们当前的提交H
是--ours
提交,并被读入索引槽2。现在运行合并机制,所以“我们”的提交是HEAD
,“他们”的提交是提交C
,并且合并基础 - 如果将merge.conflictStyle
设置为diff3
或使用git mergetool
来运行合并工具,则会显示- 是提交P
。
当我们运行以下命令时:
git revert <hash-of-C>
同样的事情发生了,只不过这一次,提交C
是槽1中的合并基础,提交P
是槽3中的--theirs
提交。槽2中的--ours
提交与通常一样来自HEAD
。
请注意,如果您在一系列提交上使用cherry-pick或revert:
git cherry-pick stop..start
挑选特定提交(cherry-picking)是逐个提交进行操作,优先选择拓扑上较老的提交;而还原撤销(reverting)是逐个提交进行操作,优先选择拓扑上较新的提交。也就是说,假设有如下提交:
...--C--D--E--...
\
H <-- HEAD
git cherry-pick C..E
会先复制D
,然后再复制E
,但git revert C..E
会先撤销E
,然后再撤销D
。(因为两个点语法排除了从两个点表达式左侧可达的提交。有关更多信息,请参阅gitrevisions文档。)
变基是重复使用cherry-pick
变基命令通过在进入分离头状态之前运行git checkout --detach
或git switch --detach
,然后重复运行git cherry-pick
来工作。(实际上,现在它只是在内部这样做;在旧版本的一些基于Shell脚本的git rebase
中,确实使用了git checkout
,不过哈希ID总是进入分离模式)。
当我们运行git rebase
时,我们从类似下面这样的东西开始:
C
/
...
我们运行:
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
生成此列表并进入分离 HEAD 模式后,现在我们看起来像这样:
C
/
...
现在Git运行一个`git cherry-pick`命令。其参数是要复制的第一个提交的哈希值C。如果我们看一下cherry-pick的工作方式,我们会发现这是一个将合并作为动词操作,其中合并基础是提交C的父节点,即提交B;当前或`--ours`提交是提交H;将要复制或`--theirs`提交是提交C。所以这就是为什么我们的和他们的似乎被颠倒了。
然而,一旦此cherry-pick操作完成,我们现在具有:
C
/
...
\
C' <-- HEAD
现在 Git 使用 git cherry-pick
命令复制提交 D
。合并基础现在是提交 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
。因此,在变基期间,所有三个提交都是我们的——尽管合并冲突可能是建立在H
的基础上的结果。
当最后一个cherry-pick完成后,Git会通过将名称为ourbranch
从旧的提交E
中拔出,并粘贴到新的提交E'
上来完成变基:
C
/
...
\
C'-D'-E' <-- ourbranch (HEAD)
我们现在回到了普通的头部附着模式,因为git log
从我们当前的提交E'
开始向后工作,从未访问原始提交C
,所以似乎我们已经修改了原来的三个提交。但实际上并不是这样,他们仍然存在于我们的存储库中,并通过特殊的伪引用ORIG_HEAD
和reflogs可用。默认情况下,至少可以在30天内恢复它们,之后git gc
将可以自由地清除它们,然后它们就真的被删除了。(只要我们没有将它们git push
到其他仍在保留它们的Git存储库中。)
git show some_commit_hash
命令查看添加/删除的文件。2)将它们与当前的git status
相对应(添加和删除相反,因为你正在还原)。3)获得收益。 - 0x5453revert
的部分。 - pkamb