git 合并与 git 合并冲突场景的变基

git merge vs git rebase for merge conflict scenarios

我想确保我看到的是正确的。

当我执行 git 合并导致冲突时,我看到存在冲突的文件为:

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

而当我 运行 由于 git 变基而陷入冲突时,我看到了相反的情况:

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

我在这里遗漏了什么吗?

当您执行合并时,Git 将目标更改视为您的本地源分支,并且这些更改首先出现在顶部。另一方面,由于 rebase 发生的顺序,被合并的分支首先发生,你的改变被重新应用到它。因此,在这种情况下,您的作品出现在底部。一个简单的图表将有助于解释变基期间发生的事情。

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

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

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

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

说你的好友,你使用当时的时间作为提交消息进行了如下更改。

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

现在看看 git merge 和 rebase 之间的区别,当你在你的分支上应用你的伙伴更改时。

合并:

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) 显示是否有冲突 else 继续变基,等等。

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

做 (1PM, 3PM, 5PM, 7PM) + (2PM) 显示是否有冲突,否则继续变基。

做 (HEAD + 4PM) 显示是否有冲突 else 继续变基,等等。

是对的,但我画的图有点不同。在您自己的(本地)Git 存储库中,您在开始时有一系列这样的提交:

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

也就是说,您已经做出了一个或多个自己的提交——在这里,我将它们标记为 IJ;他们的真名是一些丑陋的大哈希 ID,而您的分支名称 somebranch 指向(包含哈希 ID)您所做的这些新提交中的最后一个。

您然后 运行 git pull --rebase somebranch,或者(我的首选方法)两个单独的命令 git fetch,然后是 git rebase origin/somebranch。 two-step 序列是 git pull 为你做的:它 运行 有两个 Git 命令,第一个总是 git fetch,第二个是命令你提前选择,before 你会看到 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。因此,如果存在冲突,您将在 in-progress 合并中看到的 HEAD 是您来自 J 的代码,而您将在 L 中看到的是他们的代码].如果将 merge.conflictStyle 设置为 diff3,您将看到的基数是 H.1

中的内容

请注意,合并有 三个 输入。 提交 H 合并基础,提交 JLHEAD 和他们的)是涉及的两个分支提示。在这里做一个完整的合并操作的最终结果将是一个新的 merge commit M,它将指向它的 both两个直接输入:

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

merge M 中的 snapshot 是将 combined 更改应用于 commit [=30] 中的快照的结果=].即 Git 找到:

  • HJ的区别:你改变了什么;
  • HL的区别:他们改变了什么;

并尝试将它们单独组合起来。 Git 在合并它们时遇到了问题——合并冲突——然后放弃并强迫 you 合并它们。一旦你这样做了,并使用 git merge --continue 完成了这个过程,Git 从合并的结果中得到了 M

(提交M 记得直接提交H。Git可以re-discover合并基础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 做的所有相同工作。不同之处在于对合并的输入提交,当你完成时,最终提交 不是 合并提交:它只是一个常规的、普通的、日常的提交,与一个 parent.

所以,既然 Git 已经完成了 git checkout --detach origin/somebranch,那么 他们的 提交 L 就是你的 当前 提交,它执行 git cherry-pick <hash-of-I> 复制提交 I。 cherry-pick 开始合并过程。此特定合并的三个输入是:

  • 合并基础,即提交 Git 的 parent 被告知 cherry-pick:那是 H ;
  • --ours 提交,它始终是 HEAD,在本例中是提交 L他们的 提交;和
  • --theirs 提交,即 Git 提交给 cherry-pick:那是 I,也就是 你的提交。

所以te --theirs 合并操作的提交是 你的 提交,合并操作的 HEAD--ours 提交是他们的 提交 L这就是这个 apparent 逆转的来源。 Git 正在做一个 cherry-pick, 是合并的一种形式--ours 输入是他们的提交,--theirs 输入是您的提交。

解决任何合并冲突后,您将 运行 git rebase --continue。 (如果你有 运行 和 git cherry-pick 你自己,你会 运行 git cherry-pick --continuegit rebase 会为你做这件事。)这将有 cherry-pick 完成,它通过 ordinary commit:

                    I'  <-- HEAD
                   /
...--G--H------K--L   <-- origin/somebranch
         \
          I--J   <-- somebranch

分离的 HEAD 现在直接指向这个新的普通提交,即原始提交 I 的副本 I'。请注意,提交 I' 是 "just like" 提交 I 除了:

  • 它有一个不同的 parent 提交,L;和
  • 它有一个不同的 快照I' 中的快照是将 HI 之间的差异(即您更改的内容)与 合并 之间的差异与HL 之间的区别。

唉,因为这是 git rebase 而不是 git merge,我们还没有完成。现在我们也必须复制提交 J,就像 git cherry-pick <hash-of-J> 一样。我们的情况仍然是分离的 HEAD 指向新的提交 I'this 合并的三个输入是:

  • 合并基础:J的parent,即I
  • HEAD 提交为 --ours:提交 I',我们刚刚创建的那个;和
  • to-be-copied 提交为 --theirs:提交 J,即您的第二次提交。

对于合并,Git 将合并基础中的快照与两个提示提交中的每一个进行比较。所以 Git:

  1. 将您的 I 快照与您自己的 I' 快照进行比较,看看您更改了什么:那是您通过提交 [=] 引入的 他们的 代码33=]。如果存在冲突,那将显示在 <<<<<<< HEAD 中。
  2. 将您的快照从您的 I 与您自己的 J 进行比较,以查看 "they" 发生了什么变化:那是 您的 发生的变化J。如果存在冲突,这就是 >>>>>>> theirs 中显示的内容。

这次,HEAD 只是他们的代码,现在是他们的代码你的代码的混合体, 在冲突的 --ours 一方。同时,任何冲突的 --theirs 方面仍然是他们的代码。解决冲突并使用 git rebase --continue 后,Git 将进行新的普通提交 J',如下所示:

                    I'-J'  <-- HEAD
                   /
...--G--H------K--L   <-- origin/somebranch
         \
          I--J   <-- somebranch

这里 J'J 的 cherry-picked 副本。

因为这些是必须复制的所有提交,Git 现在通过将名称 somebranch 从提交 J 中拉出来并将其附加到新的来完成 rebase提交 J',然后 re-attaching HEAD 到名称 somebranch:

                    I'-J'  <-- somebranch (HEAD)
                   /
...--G--H------K--L   <-- origin/somebranch
         \
          I--J   [abandoned]

变基完成。 运行 git log 将显示您的新副本,并且不再显示原始提交 IJ。原始提交最终将被回收和销毁(通常是在 30 天后的某个时间)。

这就是使变基从根本上比合并更棘手的原因。变基涉及重复 cherry-picks,每个 cherry-pick 一个合并。如果您必须复制十次提交,那么您正在执行 ten 合并。 Git 通常可以自动完成它们,并且 Git 通常可以正确完成它们,但是 每个 合并只是 Git 愚蠢地应用一些简单的 text-difference-combining 规则,因此 每个 合并都是出错的机会。您必须仔细检查 and/or 测试结果。理想情况下,您应该检查 and/or 测试所有十份副本,但如果最后一份是好的,可能其他所有副本也是如此。