Git合并与Git变基:冲突场景下的比较

5

我希望确认我的理解是否正确。

当我进行git合并时,如果发生冲突,我会看到有冲突的文件为:

<<<<<<<<<HEAD
my local changes first
=============
The remote github changes here.
>>>>>>>>>>

如果我因为使用git rebase遇到冲突,我将看到相反的结果:

<<<<<<<<<
The remote github changes here.
=============
my local changes first
>>>>>>>>>>

我有什么遗漏吗?
3个回答

13

Tim Biegeleisen's answer是正确的,但我会稍微不同地绘制图表。当您开始时,在自己的(本地)Git存储库中,您有一系列如下的提交:

...--G--H   <-- origin/somebranch
         \
          I--J   <-- somebranch (HEAD)

也就是说,您已经进行了一个或多个自己的提交 - 在这里,我将它们标记为IJ; 它们的真实名称是一些丑陋的哈希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合并基础,而提交 JL(即 HEAD 和他们的代码)则是涉及的两个分支的末端。在此执行完整合并操作的最终结果将是一个新的合并提交 M,它将指向其两个直接输入的两个指针:

          I--J
         /    \
...--G--H      M   <-- somebranch (HEAD)
         \    /
          K--L   <-- origin/somebranch
M合并快照是将H提交的快照应用于已合并更改的结果。也就是说,Git 找到:

  • HJ的差异:你所做的更改;
  • HL的差异:他们所做的更改。

然后试图自动将它们合并。Git 遇到了一些无法合并的问题——一个合并冲突,并放弃并强制让来解决它们。一旦你这样做了,并使用 git merge --continue完成了整个过程,Git 就从合并的结果创建了M

(提交M不直接记住提交H。如果需要,Git 可以使用与找到此次相同的过程重新发现合并基础H2


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 因此执行以下操作:

  1. 将您的 I 中的快照与您自己的 I' 进行比较,以查看您更改了什么:这是通过提交 L 带入的它们的代码。如果有冲突,则在 <<<<<< HEAD 中显示。
  2. 将您的 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将显示您的新副本,不再显示原始提交IJ。 原始提交最终将被回收和销毁(通常在30天后的某个时间)。

这就是为什么重新定向在根本上比合并更棘手。 重新定向涉及重复的 Cherry-pick 操作,每个 Cherry-pick 都是一次合并。 如果您必须复制十个提交,则需要进行次合并。 Git通常可以自动执行它们,并且Git通常可以正确完成它们,但每个合并只是Git愚蠢地应用一些简单的文本差异合并规则,因此每个合并都有出错的机会。 您必须仔细检查和/或测试结果。 理想情况下,您应该检查和/或测试这十个副本的所有副本,但如果最后一个副本是好的,那么可能其他所有副本也是好的。


4

假设你和你的朋友在一段时间内做了以下更改,使用该时间作为提交消息:

你: 下午1点、下午3点、下午5点、下午7点等。 你的朋友: 下午2点、下午4点、下午6点、下午8点等。

现在看看当你将你的朋友所做的更改应用到你的分支上时,git merge 和 git rebase 之间的区别。

合并(Merge):

git merge <otherLocal/remoteBranch> ## Always current branch changes takes top

检查(1PM, 3PM, 5PM, 7PM.. + 2PM, 4PM, 6PM, 8PM..)时间是否冲突,并显示。

变基:

git rebase <hisBranch> <yourBranch> ## His branch changes takes top

如果有冲突,就进行变基操作,否则将展示 (2PM, 4PM, 6PM, 8PM) 和 (1PM)。

如果有冲突,就进行变基操作,并展示 HEAD + 3PM,以此类推。

git rebase <yourBranch> <hisBranch> ## Your branch changes takes top

如果有冲突,则显示 (1PM, 3PM, 5PM, 7PM) 和 (2PM),否则保持变基。

如果有冲突,则显示 HEAD + 4PM,以此类推。


2
当你执行合并操作时,Git将目标更改视为你的本地源分支,并且这些更改首先出现在顶部。另一方面,由于rebase发生的顺序,被合并的分支首先发生,然后应用你的更改。因此,在这种情况下,你的工作出现在底部。一个简单的图表可以帮助解释rebase期间发生了什么。
remote: -- A -- B
            \
local:       C

在这里,您已经分支出远程并进行了一次提交C,而远程也自分支点以来有一个新的提交B。现在执行变基:

remote: -- A -- B
            \
local:       B -- C'

请注意,您的 C 提交重新应用的步骤是在您的本地分支已经有了 B 提交之后。因此,从Git的角度来看,您的本地 C 提交是从外部新进来的提交。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接