从另一个从 master 变基的分支变基

Rebase a branch from another branch that is rebased from master

我有这个场景:

o---o---o---o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

如上所述,我在 master 上做了一些提交,我想更新其他两个分支并且有:

o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

我已经在 next 分支上做了 git rebase master,没想到 topic 现在还没有 next 分支的基础提交。

我现在可以做什么?我立即对中间分支进行变基是错误的吗?我该怎么办?

这个问题有两个相对简单的解决方案。一种是尝试 rebase,这可能会奏效。但如果没有,它仍然很容易。这是第二种,更强大的方法,其次是 why:

git checkout topic; git rebase --onto next next@{1}

发生了什么:变基如何工作

我认为,如果您以不同的方式绘制图形,视图会更清晰。这是原始的,略有重绘:

A---B---C---D---E                   <-- master
     \
      F---G---H---I---J             <-- next
                       \
                        K---L---M   <-- topic

这是 git checkout next; git rebase master 之后的结果:

A---B---C---D---E                   <-- master
     \           \
     |            F'-G'-H'-I'-J'    <-- next
     \
      F---G---H---I---J             [was "next", but not anymore]
                       \
                        K---L---M   <-- topic

这是因为变基是通过复制 提交来工作的,有一些轻微的(或主要的)变化。新提交 F' 是原始 F 的副本,G'G 的副本,依此类推。

如果您现在 运行 git checkout topic; git rebase next,它 通常 就可以正常工作,但并非总是如此。这样做的原因是,默认情况下,rebase 选择 提交复制 从单个参数复制后 .这有点复杂,所以让我们分两步进行。

当你在这里说 git rebase next 时,唯一的参数是 next:副本将在 next 的 tip 提交之后(现在是 J')。这部分非常简单:一个标识符 next 找到附加副本的提交 --onto。 (我拼写 --onto 而不是 "onto" 是有原因的。:-) )

复制的内容部分比较棘手,有两个方面。首先,next指定复制什么,而当前分支(topic)指定复制什么。 Git 获取可从 topic 访问的所有提交的列表——即提交 ABFGH,以此类推一直到 L。然后它 排除 所有可从 next 到达的提交,即提交 ABCDEF'(但不是 F)、G'(但不是 G),依此类推直到 J'。一些被排除的提交不在包含列表中并不重要:我们只是排除那些在包含列表中

不幸的是,这将提交 FGHIJ 留在待提交的一堆中复制。正如您所注意到的,这就是出错的地方。但不知何故,大部分时间,这仍然有效。工作时发生了什么?为什么它 总是 有效?

答案是变基排除"patch-equivalent commits"。这里的一般想法是,当你变基时,Git 扫描 "exclude" 列表以查看是否有任何这些提交 做同样的事情 作为提交在 "include" 列表中。由于 F'F 的副本,它很可能是 "the same" 作为 F,至少在补丁等效意义上是这样。 GG'HH' 等也是如此。

只要提交与其重新定位的版本具有相同的补丁,它们就会被排除在外。

如果这是真的——通常是这样——你只需 git checkout topic; git rebase next 就可以了。当它 不是 为真时——当 FH 中的一些在被复制时被修改 "too much",并且它们的副本不再是补丁等效的-它失败了。

作为一种经验法则,1 如果 Git 能够独自完成第一次变基,那么它也能够完成第二次变基本身也是。主要是当它因冲突而停止并让你手动修复它时,补丁就会变成 "not-patch-equivalent"。但是处理这个很容易,因为 git rebase 有更长的形式。

而不是:

git rebase next

我们可以这样写:

git rebase --onto next <something>

根据我们绘制的图表,此处使用的 <something> 任何指定提交 J 的内容。也就是说,我们需要找到next的地方。 next 指向什么提交, 我们 rebase 之前?它现在指向 J',但它过去指向 J。我们需要 Git 排除的提交是 next 曾经 达到的提交:ABFGHIJ。如果 Git 自己找不到这些,通过等价补丁,我们可以 告诉 它。

这仍然留下了如何拼写标识提交的名称的问题 J。一种方法是使用原始哈希 ID,它总是有效,但有点麻烦(查找和复制,尽管剪切和粘贴对于复制哈希非常有效)。但是再看看重新绘制的图形,上面写着 was "next", but not anymore。如果我们能告诉 Git 查找 next.

previous 值就好了

但是等等! Git 有 个引用日志 !我们可以告诉它这样做! next 的先前值拼写为 next@{1}。 (每次我们更改标签 next 时,数字都会增加,例如通过向其添加新的提交,但我们刚才所做的只是 rebase,所以它仍然是 next@{1}.) 所以在这一点上我们可以这样做:

git checkout topic; git rebase --onto next next@{1}

(取决于您的 shell,您可能需要在带有大括号的内容周围使用引号,例如 'next@{1}'next@\{1\} 或类似的)。


1有一些例外。例如,在这种情况下,如果 git diff E F' 不会产生与 git diff B F 相同的变更集,因为 diff 选择了不同的半不相关的匹配项,例如空行或大括号行,那将造成问题。但是,处理这些的答案与处理由于冲突而手动应用的 rebase 的答案相同。