Tim Biegeleisen's answer是正确的,但我会稍微不同地绘制图表。当您开始时,在自己的(本地)Git存储库中,您有一系列如下的提交:
...--G--H <-- origin/somebranch
\
I--J <-- somebranch (HEAD)
也就是说,您已经进行了一个或多个自己的提交 - 在这里,我将它们标记为I
和J
; 它们的真实名称是一些丑陋的哈希ID - 并且您的分支名称somebranch
指向(包含)您所做的这些新提交中最后一个的哈希ID。
然后您运行git pull --rebase somebranch
,或者(我更喜欢的方法)两个单独的命令git fetch
,然后是git rebase origin/somebranch
。 这个两步序列是git pull
为您执行的:它运行两个Git命令,第一个总是git fetch
,第二个是您事先选择的命令,在您看到git fetch
执行的内容之前。 (我喜欢看看git fetch
做了什么,然后决定:我要重新设置基础,还是合并,还是等待,还是完全做其他事情?)
git fetch
步骤获取了其他人创建的新提交,使您拥有以下内容:
...--G--H------K--L <-- origin/somebranch
\
I--J <-- somebranch (HEAD)
再次强调,大写字母代表某个实际的哈希 ID,具体是什么无所谓。
使用git merge
时,Git会进行合并操作。可以稍微改变绘制图形来使它更加清晰:
I--J <-- somebranch (HEAD)
/
...--G--H
\
K--L <-- origin/somebranch
合并的公共起点是提交 H
;你的提交 HEAD
是提交 J
;而他们的提交当然是 L
。所以,如果有冲突,在正在进行的合并中,HEAD
中的代码是来自于 J
的代码,而他们的代码则来自于 L
。如果你将 merge.conflictStyle
设置为 diff3
,则你将看到的基础代码是 H
中的内容。1
请注意,合并有三个输入。提交 H
是合并基础,而提交 J
和 L
(即 HEAD
和他们的代码)则是涉及的两个分支的末端。在此执行完整合并操作的最终结果将是一个新的合并提交 M
,它将指向其两个直接输入的两个指针:
I--J
/ \
...--G--H M <-- somebranch (HEAD)
\ /
K--L <-- origin/somebranch
M
合并快照是将
H
提交的快照应用于已合并更改的结果。也就是说,Git 找到:
H
到J
的差异:你所做的更改;
H
到L
的差异:他们所做的更改。
然后试图自动将它们合并。Git 遇到了一些无法合并的问题——一个合并冲突,并放弃并强制让你来解决它们。一旦你这样做了,并使用 git merge --continue
完成了整个过程,Git 就从合并的结果创建了M
。
(提交M
不直接记住提交H
。如果需要,Git 可以使用与找到此次相同的过程重新发现合并基础H
。2)
1我喜欢设置此选项。这样,你不仅可以看到你和他们各自添加了什么,还可以看到合并基础提交中最初存在的内容。当你或他们删除所涉及的代码时,这尤其有用。
2这其实是一种错误,因为你可以使用修改事物的选项(包括在某些相对罕见的情况下使用的合并基础),来运行git merge
。合并命令应记录你使用的选项,以使合并真正可重复。
然而,当你使用git rebase
时,Git会逐个复制每个现有提交——在这种情况下是两个。此复制过程使用“分离头”(detached HEAD),其中HEAD
直接指向提交。Git首先将他们的提交L
检出为分离的 HEAD,如下所示:
...--G--H------K--L <-- HEAD, origin/somebranch
\
I--J <-- somebranch
从技术上讲,cherry-pick 是一种合并的形式,或者说,作为一个动词来描述,即合并的过程,但不会真正生成一个合并提交。也就是说,你仍然需要使用 git merge 所需的所有工作。不同之处在于合并的输入提交以及完成后的最终提交不是一个合并提交,而只是一个普通的提交,带有一个父提交。
所以现在,Git 已经执行了 git checkout --detach origin/somebranch
命令,使得他们的提交 L
成为你当前的提交,接着它执行 git cherry-pick <hash-of-I>
命令来复制提交 I
。这个 cherry-pick 命令开始了合并的过程。这个特定合并的三个输入分别是:
- 合并基础,即 Git 被告知要 cherry-pick 的提交的父提交:即为
H
;
--ours
提交,它始终为 HEAD
,在这种情况下是提交 L
:即为他们的提交;以及
--theirs
提交,即 Git 被告知要 cherry-pick 的提交:即为你的提交 I
。
因此,合并操作的 --theirs
提交是你的提交,而合并操作的 HEAD
或 --ours
提交是他们的提交 L
!这就是这个明显的反转来自何处。Git 正在进行 cherry-pick 操作,它是合并的一种形式。其中 --ours
输入是他们的提交,而 --theirs
输入是你的提交。
在解决任何合并冲突后,你将运行 git rebase --continue
命令。(如果你自己运行了 git cherry-pick
命令,你需要运行 git cherry-pick --continue
命令;git rebase
会为你处理这个过程。)这将使 cherry-pick 完成,最终生成一个普通提交:
I' <-- HEAD
/
...--G--H------K--L <-- origin/somebranch
\
I--J <-- somebranch
现在分离的 HEAD 直接指向这个新的普通提交,即原始提交 I 的副本 I'。请注意,提交 I'“就像”提交 I 一样,只是:
- 它有一个不同的父提交 L;和
- 它有一个不同的快照。I' 中的快照是通过获取 H 到 I(即您所更改的内容)之间的差异,并将该差异与 H 和 L 之间的差异进行合并所得到的。
不幸的是,由于这是 git rebase 而不是 git merge,我们还没有完成。现在我们必须复制提交 J,就好像使用 git cherry-pick <hash-of-J> 一样。 我们的情况仍然是分离的 HEAD 指向新提交 I'。此合并的三个输入为:
- 合并基础:J 的父级,即 I;
- HEAD 提交作为 --ours:刚刚创建的提交 I';和
- 要复制的提交作为 --theirs:J 提交,即您的第二个提交。
像所有合并一样,Git 将合并基础的快照与两个提示提交中的每个进行比较。Git 因此执行以下操作:
- 将您的 I 中的快照与您自己的 I' 进行比较,以查看您更改了什么:这是通过提交 L 带入的它们的代码。如果有冲突,则在 <<<<<< HEAD 中显示。
- 将您的 I 中的快照与您自己的 J 进行比较,以查看“他们”更改了什么:这是您制作 J 时所做的更改。如果有冲突,则在 >>>>>>> theirs 中显示。
这一次,HEAD 不再仅仅是他们的代码,而是在冲突的 --ours 一侧混合了他们的代码和您的代码。同时,任何冲突的 --theirs 一侧仍然是他们的代码。一旦您解决冲突并使用 git rebase --continue,Git 将创建一个新的普通提交 J',方法如下:
I'-J' <-- HEAD
/
...--G--H------K--L <-- origin/somebranch
\
I--J <-- somebranch
在这里,J'
是从 J
中挑选出来的内容副本。
由于这些都是要复制的提交,Git 通过将名称为 somebranch
的提交从提交 J
上删除并将其附加到新提交 J'
上,然后重新将 HEAD
附加到名称为 somebranch
的提交上,以完成 rebase 操作:
I'-J' <-- somebranch (HEAD)
/
...--G--H------K--L <-- origin/somebranch
\
I--J [abandoned]
重定向已完成。 运行git log
将显示您的新副本,不再显示原始提交I
和J
。 原始提交最终将被回收和销毁(通常在30天后的某个时间)。
这就是为什么重新定向在根本上比合并更棘手。 重新定向涉及重复的 Cherry-pick 操作,每个 Cherry-pick 都是一次合并。 如果您必须复制十个提交,则需要进行十次合并。 Git通常可以自动执行它们,并且Git通常可以正确完成它们,但每个合并只是Git愚蠢地应用一些简单的文本差异合并规则,因此每个合并都有出错的机会。 您必须仔细检查和/或测试结果。 理想情况下,您应该检查和/或测试这十个副本的所有副本,但如果最后一个副本是好的,那么可能其他所有副本也是好的。