父级被压扁并提交后,如何根据母版重新设置分支?

How to rebase branch against master after parent is squashed and committed?

我在 Gitlab 中使用了一个乐观的工作流程,它假设我的大部分合并请求都将被接受而无需更改。流程如下所示:

  1. 提交分支的合并请求 cool-feature-A
  2. 基于 cool-feature-A 创建一个名为 cool-feature-B 的新分支。开始在此分支上开发。
  3. 一位同事批准了我对 cool-feature-A 的合并请求。
  4. 我将 cool-feature-B 重新设置为 master(无痛)并继续开发。

如果同事在第 3 步进行挤压合并,就会出现问题。这会重写 cool-feature-B 中存在的大量历史记录。当我到达第 4 步时,我面前出现了合并痛苦的世界。

如何避免这种情况发生?

本质上,必须告诉Git:我想根据master变基cool-feature-B,但我想要复制一组不同于您通常在此处计算的提交集。 最简单的方法是使用 --onto。也就是说,通常你运行,如你所说:

git checkout cool-feature-B
git rebase master

但你需要做:

git rebase --onto master cool-feature-A

之前你删除自己的branch-name/标签cool-feature-A.

你总是可以这样做,即使他们使用正常的合并。这样做不会有什么坏处,除了你必须输入更多并记住(无论你喜欢什么)你会想要这个 --onto,稍后需要正确的名称或哈希 ID。

(如果你可以获取 cool-feature-A 指向的提交的哈希 ID 到 cool-feature-B 上游的 reflog 中,你可以使用 --fork-point 功能使 Git 稍后自动为您计算,前提是相关的 reflog 条目尚未过期。但一般来说,这可能比手动执行此操作更难。另外,它具有整个 "provided" 部分。 )

为什么会这样

像往常一样,让我们​​从图表开始。最初,您在自己的存储库中有此设置:

...--A   <-- master, origin/master
      \
       B--C   <-- cool-feature-A
           \
            D   <-- cool-feature-B

一旦您拥有 运行 git push origin cool-feature-A cool-feature-B,您将拥有:

...--A   <-- master, origin/master
      \
       B--C   <-- cool-feature-A, origin/cool-feature-A
           \
            D   <-- cool-feature-B, origin/cool-feature-B

请注意,我们在这里所做的只是添加两个 origin/ 名称(两个 remote-tracking 名称):在 他们的 存储库中,在 origin,他们获取了提交 BCD,并将 他们的 cool-feature-Acool-feature-B 名称设置为记住分别提交 CD,所以你自己的 Git 添加了 你的 origin/ 这两个名字的变体。

如果他们(无论 "they" 是谁——在 origin 上控制存储库的人)进行 fast-forward 合并,他们将滑动 他们的 master up to point to commit C。 (请注意,GitHub 网络界面 没有用于进行 fast-forward 合并的按钮。 我没有使用 GitLab 的网络界面;它可能在很多方面有所不同。)如果他们强制进行真正的合并——这是 GitHub 网页 "merge this now" 默认情况下点击按钮所做的;再一次,我不知道 GitLab 在这里做了什么——他们将进行新的合并提交 E:

...--A------E
      \    /
       B--C
           \
            D

(这里我特意去掉了所有的名字,因为他们的名字和你的不太匹配)。他们可能会删除(或者甚至可能从未真正创建过)他们的 cool-feature-A 名字。无论哪种方式,您都可以拥有自己的 Git fast-forward 您的 master 名称,同时更新您的 origin/* 名称:

...--A------E   <-- master, origin/master
      \    /
       B--C   <-- cool-feature-A [, origin/cool-feature-A if it exists]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

或:

...--A
      \
       B--C   <-- cool-feature-A, master, origin/master [, origin/cool-feature-A]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

现在是否删除你的名字cool-feature-A——为了以后画图方便,假设你删除了——如果你运行:

git checkout cool-feature-B
git rebase master

Git 现在将枚举可从 DDCB 等到达的提交列表从 master 中减去可访问的提交列表:E(如果存在),然后 A(如果 E 存在)和 C(无论是否存在) E 存在),然后是 B,依此类推。减法的结果就是 commit D.

您的 Git 现在复制此列表中的提交,以便新副本出现在 master 的提示之后:即,如果它们进行了真正的合并,则在 E 之后,或者在 C 之后,如果他们进行了 fast-forward 合并。所以 Git 要么将 D 复制到 E 之后的新提交 D':

              D'   <-- cool-feature-B
             /
...--A------E   <-- master, origin/master
      \    /
       B--C
           \
            D   <-- origin/cool-feature-B

或者它单独留下 D 因为它已经在 C 之后(所以没有什么新东西可以绘制)。

棘手的部分发生在他们使用 GitLab 的 GitHub 的 "squash and merge" 或 "rebase and merge" 可点击按钮的等效项时。我将跳过 "rebase and merge" 案例(这通常不会导致问题,因为 Git 的 rebase 也会检查 patch-IDs)并直接进入困难的案例,即 "squash and merge"。正如您正确指出的那样,这使得 新的和不同的 ,单一的,提交。当您将新提交放入您自己的存储库时——例如,在 git fetch 之后——你有:

...--A--X   <-- master, origin/master
      \
       B--C   <-- cool-feature-A [, origin/cool-feature-A]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

其中 X 是进行新提交的结果,其 snapshot 将匹配合并 E(如果他们要进行合并 E),但其(单个)父项是现有提交 A。所以历史——通过向后工作枚举的提交列表——从 X 只是 X,然后是 A,然后是 A.

之前的任何提交

如果你 运行 一个普通的 git rebase master 而在 cool-feature-B, Git:

  1. 枚举可从 D 访问的提交:DCBA、...;
  2. 枚举可从 X 访问的提交:XA、...;
  3. 从第1步的集合中减去第2步的集合,剩下DCB
  4. 复制那些提交(按 un-backwards-ized 顺序)以便它们在 X.
  5. 之后

请注意,第 2 步和第 3 步都使用词 master 来查找提交:要复制的提交,对于第 2 步,是那些 aren' 可从 master 到达。对于步骤 3,放置副本的位置是 after master.

的提示

但是如果你运行:

git rebase --onto master cool-feature-A

您 Git 在第 2 步和第 3 步中使用 不同的 项目:

  • 要复制的提交列表,来自第 2 步,来自 cool-feature-A..cool-feature-B:从 D-C-B-A-... 中减去 C-B-A-...。那只剩下提交 D.
  • 第3步中放置副本的位置来自--onto master:将它们放在X之后。

所以现在 Git 只将 D 复制到 D',然后 git rebase 将名称 cool-feature-B 拉到指向 D' :

          D'  <-- cool-feature-B
         /
...--A--X   <-- master, origin/master
      \
       B--C   <-- cool-feature-A [, origin/cool-feature-A]
           \
            D   <-- origin/cool-feature-B

这就是你想要的。

如果他们——控制 GitLab 存储库的人——使用真正的合并或 fast-forward not-really-a-merge-at-all,这一切仍然有效:你将拥有你的 Git 枚举 D-on-backwards 但从列表中删除 C-on-backwards,只留下 D 进行复制;然后 Git 将复制 D 以便它出现在 E (真正的合并案例)或 C (fast-forward 案例)之后。 fast-forward 案例 "copy D so that it comes where it already is" 巧妙地根本不去复制,只是将所有内容留在原地。