我认为有助于认识到
git rebase
实际上是自动运行
git cherry-pick
的方法。但仅有这些还不够,你还需要明白
git cherry-pick
是合并的一种形式。这就是合并冲突的来源所在。
当查看常规合并时,可以更容易地理解这一点。让我们绘制一个提交图表,每个提交用单个大写字母代表,如下所示:
I--J <-- branch1 (HEAD)
/
...--G--H
\
K--L <-- branch2
如果我们运行
git merge branch2
,Git会找到
三个提交:
其中一个提交,也就是最后的#2,永远都是当前的或者HEAD
提交。由于HEAD
附加在branch1
名称上,当前提交就是由branch1
标识的那个:提交J
。
最后的#3提交是你指定的提交。通过使用branch2
这个名称,你告诉Git读取该名称并查看它指向提交L
。
#1提交是Git自己找到的。Git通过找到两个分支上“最佳”的提交来实现此目的。在branch1
上的提交包括...-G-H-I-J
。在branch2
上的提交包括...-G-H-K-L
。因此,提交G
位于两个分支上,但它比提交H
更早。提交I-J
仅出现在其中一个分支上,而K-L
仅出现在另一个分支上。这意味着提交H
是最好的共享提交。
Git现在可以执行合并。为此,Git实际上运行了两个git diff
命令:
现在,合并命令的工作是组合你的更改和他们的更改:
- 对于没有被修改的在
H
中的文件,保留那些文件。
- 对于在
H
中由你修改而他们没有修改的文件,采用你的版本。
- 对于在
H
中由他们修改而你没有修改的文件,采用他们的版本。
- 对于你们都修改过的文件,需要找出是否可能将你们的更改结合起来。
有一些棘手的情况,比如你重命名了一个文件和/或他们重命名了一个文件,或者你删除了一个文件而他们修改了它等等。但是大多数情况下,当你们都对某个文件进行更改,并且你们都更改了该文件的相同行,或进行了“接触”更改时,将发生合并冲突。如果您的更改和他们的更改不会“接触”,Git会认为保留两个更改都可以。否则,你就会遇到合并冲突。
在前几次处理此类情况时可能有点棘手,但是久而久之,它会感觉非常自然。例如,如果Alice将“红球”更改为“蓝球”,而Bob将“红球”更改为“红砖”,Git不知道该怎么做,并使您选择正确答案。
进入cherry-pick
git cherry-pick命令的作用是复制提交。也就是说,给定表示所有文件的完整快照的某个提交,我们要找出该文件中发生了哪些变化。
在Git中,很容易将两个相邻的提交——发生在另一个提交之后的两个快照——转换为一组更改。我们只需要求Git运行git diff来执行这项操作。Git会确定哪些文件相同,并对此保持沉默。它会确定哪些文件不同,并生成一份食谱——一组要添加和/或删除的行——以将较早提交的文件更改为较新提交的副本。如果我们使用--find-renames(自Git 2.9起默认开启),那么如果左侧消失了一个文件,而右侧出现了一个新文件,则Git还会确定是否表示文件重命名操作。
然后,想象一下我们有以下内容:
...--G--H--I--J <-- main
\
K--L <-- feature
如果我们要求从
H
到
K
的差异,我们将看到与
H
相比,在
K
中发生了什么变化。例如,可能会说“在
file.py
的第72行后添加此行”。
但是如果我们想将这些更改应用于提交
J
呢? 我们可以闭上眼睛,希望“在第72行后添加此行”有意义,但是如果原来的第72行现在是第75行,或者甚至更远怎么办呢? 我们可以搜索上下文,但是也许我们甚至可以做得更好。
与其盲目地应用此更改或检查上下文,不如先抓取第二个差异,即提交
H
与提交
J
之间的差异。 这将告诉我们它们所做的更改。 如果他们在第72行上面添加了3行,那么现在第72行就明确是第75行。 这告诉我们应该放置更改的位置。
但是等一下,这个“获取两个差异并组合起来”的想法正是
git merge
的工作方式!实际上,这正是
git cherry-pick
的工作方式:我们选择我们要复制的提交的父级,并假装它是合并基础。 我们得到两个差异,一个是从合并基础到我们要复制的提交-这些是“他们”的更改,另一个是从合并基础到我们现在正在工作的提交,即提交
J
,这些是“我们”的更改。 我们让Git将它们组合起来,使用运行
git merge
时使用的相同代码。
如果一切顺利,
git cherry-pick
将为我们创建一个新的提交。
git rebase
命令以Git称为“分离头”模式执行所有操作,因此现在的情况如下所示:
K' <-- HEAD
/
...--G--H--I--J <-- main
\
K--L <-- feature
我们将新提交称为
K'
,以表示它是原始提交
K
的复制品。现在是时候精选提交
L
了,因此Git将对比
K
与
L
,以查看“他们”(实际上是我们)更改了什么,并对比
K
与
K'
,以查看“我们”(实际上是之前的所有操作和包括之前的精选操作)更改了什么。然后,Git将尝试将这两组更改——“我们”的更改(来自
K
-vs-
K'
)和“Theirs”的更改(来自
K
-vs-
L
)结合起来。如果一切顺利,
git cherry-pick
将创建一个新提交
L'
。
K'-L' <-- HEAD
/
...--G--H--I--J <-- main
\
K--L <-- feature
如果在
git cherry-pick
步骤中出现问题,Git 将停止操作并要求我们解决冲突,与
git merge
操作完全相同。
一旦所有提交被复制,
git rebase
有一个最后的技巧:它会将旧位置上的名字
feature
拉下来,并将其贴到
HEAD
所指向的位置,然后 "重新附加"
HEAD
到分支名。在本例中,这样就产生了:
K'-L' <-- feature (HEAD)
/
...--G--H--I--J <-- main
\
K--L [abandoned]
如果你现在用
git log
查看提交记录,将完全看不到原始的
K-L
提交记录,而只能看到新的
K'-L'
提交记录。在
L
之前的下一个提交是
J
,特性分支已经被合并到主分支上并进行了重新设置基底。
任何合并冲突都是因为“你”和“他们”在合并奇怪的基底过程中接触到同一文件的相同或相邻行。当然,“他们”的提交实际上是“你”的提交 - 在重新设置基础时,你要重新安排自己的提交,并且“你”的提交最初往往是其他人的提交。最终,“你”的提交混合了你的提交和他们的提交,这非常令人困惑。
(我喜欢将
merge.conflictStyle
设置为
diff3
,以获取更多有关合并冲突的信息。)
git rebase
会重放您的更改。如果补丁无法应用,则必须在继续之前解决冲突。您遇到了什么样的冲突(添加的、在两个分支中都编辑过等)?然后查看主分支的历史记录,以了解是什么导致了冲突。 - AdamAAA
,但您尝试变基的提交具有增量AAB
->AAC
,这是一个冲突,需要选择AAA
还是AAC
。 - Alexey Larionov