首页 > 解决方案 > git:将滞后分支合并到当前分支

问题描述

通常,当我们必须在当前分支(mybranch)上合并一些其他分支(branch-to-merge)时,我们会这样做:

git checkout mybranch
git pull origin branch-to-merge

我理解它的工作方式是将 mybranch 指针移动到与分支合并的负责人相同的提交。如果它的线性场景,一切顺利,否则我们需要解决合并冲突。

但是如果分支合并在 mybranch 之后,它会如何发挥作用呢?我基本上想将分支合并中的更改(它是滞后的)集成到我当前的功能分支中,以利用分支合并中所做的代码更改。

标签: git

解决方案


TL;DR:什么都没有发生(Git 说Already up to date.并停止并成功退出)。

您的理解并没有完全错误,但它缺少一两个关键细节,这将在一瞬间引起问题:

git checkout mybranch
git pull origin branch-to-merge

... 会将 mybranch 指针移动到与分支合并的负责人相同的提交。

让我们在这里准确一点。git checkout mybranch步骤:

  • 当前提交中删除所有文件(如 Git 的索引aka staging area中所记);
  • 而是从名称指示的提交中提取文件mybranch;和
  • 创建mybranch当前分支。

(或者,当然,它可能会因各种错误情况而失败,但我们可以假设这些不会发生。它也可以在保留未提交的工作的同时成功,如Checkout another branch when there are uncommitted changes on the current branch 中所述:this当它可以跳过特定文件的删除和替换时发生,Git 通常出于速度原因尝试这样做,无论这些文件中是否有未提交的工作)。

完成此结帐后,然后运行git pull​​. 该pull命令是其他两个 Git 命令的组合:

  1. git pull运行git fetch。在旧版本的 Git 中,它确实是这样做的(它是一个 shell 脚本,它实际上使用各种参数运行)。 git fetch在当前的 Git 中,这是内置在pull程序中的,但效果是一样的。此步骤获取您的存储库缺少的任何提交(origin一个单独的 Git 存储库)。在正常情况下,此步骤还会更新origin/branch-to-merge您自己的存储库中的名称。

  2. git pull然后运行您编写的要运行的第二个命令。如果您没有设置任何内容,则默认为 using git merge,但您可以将其设置为 run git rebase。与 fetch 步骤一样,旧脚本确实运行了这些;新的 C 代码内置了它们,但效果是一样的。传递 git mergeor的参数git rebase有点复杂,但是如果我们假设你在git merge这里使用,并且其他都正常,我们就得到了运行的效果git merge origin/branch-to-merge(除了默认的提交消息)。但您也可以git merge --no-ff在这里使用,或者,现在,git merge --ff-only. 如果您使用git rebase作为您的第二个命令,则图片会更加复杂。

如果它的线性场景,一切顺利,否则我们需要解决合并冲突。

这有许多棘手的细节和极端情况。该git merge命令在多种场景下运行,并带有各种参数(--no-ff, --ff-only),并且每个参数都有不同的效果。但总的来说,我们可以将合并描述为以这种方式工作:

  • 首先,Git 确保我们有一个“干净”的工作树(一些合并案例不检查,但如果您使用这些奇怪的合并变体之一,确保自己是这种情况是一个非常好的主意)。然后,Git使用来自的当前分支名称或来自的原始哈希 ID 来定位当前提交哈希 ID 。如果我们遇到合并冲突,这是“HEAD”或“我们的”提交。HEADHEAD

  • Git 还会将参数指定的提交定位到git merge. 也就是说,origin/branch-to-merge转换为原始提交哈希 ID。Git 会找到这个特定的提交。

  • 然后 Git 使用提交图计算两个提交的合并基础(请参阅有向无环图的最低公共祖先)。就确定合并结果而言,这是在某些方面最重要的提交。

我们可以通过各种方式绘制合并基础。例如,假设我们有这个:

          o--L   <-- mybranch (HEAD)
         /
...--o--*
         \
          o--R   <-- origin/branch-to-merge

在这里,名称mybranch位于 commit L,“左侧”或“本地”或 HEAD 或ours提交。该名称origin/branch-to-merge位于 commit R:另一个,或“右侧”或“远程”或--theirs提交。标记的提交*是最好的共同祖先,所以提交*是合并基础。

这种合并需要真正的合并。它可能会产生合并冲突,但如果没有,并且允许真正的合并,则此合并的结果是新的合并提交M

          o--L
         /    \
...--o--*      M   <-- mybranch (HEAD)
         \    /
          o--R   <-- origin/branch-to-merge

因为这个提交是在“on”时进行的(mybranch也就是说),所以分支名称现在指向 merge commit 。git statuson branch mybranchmybranchM

如果存在合并冲突,Git 在合并中间停止,所有三个提交都读入 Git 的索引,合并部分解决,部分未解决。作为人类操作 Git,你的工作是完成合并,清理 Git 留下的烂摊子;或者您可以选择运行git merge --abort以丢弃部分结果,通过本质上重置为 commit 来清理混乱L。(如果你开始一个“脏”合并然后使用git merge --abort,这通常会很糟糕,这就是为什么你不应该首先开始一个“脏”合并。)

如果需要真正的合并并且您指定了--ff-onlygit merge则甚至不会启动合并:它只是说快进是不可能的,并以错误终止。

此图说明了另一种情况:

...--o--L   <-- mybranch (HEAD)
         \
          o--R   <-- origin/branch-to-merge

L在这里,“合并基础”——可以从提交和提交中获得的最佳共享提交——是Rcommit L。这是您的线性场景:Git可以简单地“向前移动分支名称”,同时还可以检查 commit R,结果是:

...--o--L--o--R   <-- mybranch (HEAD), origin/branch-to-merge

--ff-only如果您指定或未使用,这是默认操作--no-ff。但是,如果您指定--no-ff了 ,Git 将继续执行完全合并:

...--o--L------M   <-- mybranch (HEAD)
         \    /
          o--R   <-- origin/branch-to-merge

Commit和以前一样M合并提交。commit的快照M将与 commit 的快照匹配R,因为合并提交快照是通过将合并基础 ( L) 到每个提示提交 (LR) 的更改组合而成的。从Lto的变化L的,所以这会产生从Lto找到的变化R。Git 将这些更改应用于合并库中的快照(即 in)L,从而生成与 commit 匹配的快照R。然后 Git 提交结果,因为没有合并冲突。

但是如果分支合并在 mybranch 之后,它会如何发挥作用呢?

请注意,在一种情况下,我们已经绘制了:

          o--L   <-- mybranch (HEAD)
         /
...--o--*
         \
          o--R   <-- origin/branch-to-merge

origin/branch-to-merge后面就是这种情况mybranch。只是,尽管落后了两次提交,但origin/branch-to-merge领先了两次提交。也就是说,有两个提交,沿着底线提交,它们是“on”(可访问)提交R,而不是“on”(不可访问)提交L

让我们画出另一种情况:

...--o--R   <-- origin/branch-to-merge
         \
          o--L   <-- mybranch (HEAD)

L在这里,和的合并基RR。合并是不可能的:从Rto的差异R是空的,并且应用差异从RtoL产生快照 in L。当 R 严格领先于 L 时, Git仍然可以执行相同的“强制合并”,但 Git 不会这样做。相反,它只是说它已经是最新的。

底线

合并命令:

  1. 定位要合并的提交(我们称它们LR两个提交的情况);
  2. 定位合并基础(我们称它为B);和
  3. 使用它来驱动其余的动作。

对于具有递归或解析的标准双头合并,有三种可能性,按此顺序考虑:

  • B= R:发出“最新”消息并成功终止。
  • B= L:考虑进行快进而不是合并。如果允许,检查 commit R,更新当前分支中存储的哈希 ID,并成功终止。
  • 否则,如果允许(否--ff-only),则进行完全合并。如果这是成功的(并且既--no-commit没有指定也没有--squash指定),则进行新的合并提交,否则停止。对于--squashR记录in的哈希 ID MERGE_HEAD;对于其他合并,R记录in的哈希 ID MERGE_HEAD

B= L=的特殊情况下R,我们首先进行B=R测试并说“最新”。该--no-ff选项只是强制中间测试失败,以便我们继续第三种情况。

--squash选项确保合并提交M只有一个父级。(它通过提前终止合并来实现这一点,就好像-n已经指定了一样,这有点愚蠢:git merge -n --squash如果你真的想要,你可以运行,并且它可以在不使用时将最终提交作为非合并提交-n,然后声明合并完成。)该-n选项确保 Git 不会立即进行最终提交,这允许您根据需要创建一个邪恶的合并

如何查看合并基础

如果您擅长观察 Git 图(请参阅Pretty Git 分支图),您也许可以通过查看来发现合并基础,但是很多图都非常纠结。您可以运行git merge-base --all

git fetch
git merge-base --all mybranch origin/branch-to-merge

第二个命令吐出哈希 ID。理想情况下,它只输出一个哈希 ID:一个哈希 ID 是合并基础,一切都从那里开始。

如果这会打印两个或更多哈希 ID,则说明您有一个复杂的合并。默认(-s recursive或 now, -s ort)策略将合并合并基础,从结果中创建一个新的但临时的提交,然后将结果用作合并基础。这在理论上很简单,但很难理解,更难描述好。它也非常罕见,所以你可能不会遇到它。


推荐阅读