当父分支被压缩并提交后,如何将分支与主分支进行变基?

13
我在Gitlab中使用一种乐观的工作流程,它假设我的大部分合并请求将被接受而无需更改。该流程如下:
  1. 提交一个名为cool-feature-A的分支的合并请求
  2. 基于cool-feature-A创建一个新分支,名为cool-feature-B,并开始在此分支上进行开发。
  3. 同事批准了我对cool-feature-A的合并请求。
  4. 我将cool-feature-Bmaster进行变基(这很容易),然后继续开发。
问题出现在同事在第3步执行一个Squash Merge时。这将重写存在于cool-feature-B中的大量历史记录。当我到达第4步时,我将遇到许多合并痛点。
如何避免这种情况的发生?

1
据我所知,真正的合并和压缩合并对代码没有任何影响。这两种方法预计会产生相同的代码,并使用相同的标准来解决任何冲突。 - ElpieKay
@ElpieKay 我已经为问题添加了更多细节。如果进行了压缩,那么在变基期间肯定会有所不同。 - Duncan Jones
这听起来像是一个人的问题,而不是技术问题。但你尝试过 git checkout cool-feature-B && git rebase --fork-point master 吗?当我在rebase多个依赖分支(例如cool-feature-Acool-feature-Bcool-feature-C)时,使用--fork-point效果非常好,尽管我承认我没有尝试过压缩合并。 - Chris
1
@Chris:--fork-point 使用分支上游的 reflog,因此为了使其工作,您必须在上游拥有提交 BC(并且已经将它们看到进入您自己的存储库),但这在一般情况下都不会成立。 - torek
1个回答

31

本质上,需要告诉 Git:我想要将cool-feature-Bmaster的基础上进行变基,但我想要复制一组不同于通常计算的提交。最简单的方法是使用--onto。也就是说,通常你会运行,就像你说的那样:

git checkout cool-feature-B
git rebase master

但你需要做:

git rebase --onto master cool-feature-A

你删除你自己的分支名称/标签 cool-feature-A 之前。

即使他们使用了普通合并,你仍然可以这样做。 这不会有什么影响,除了你需要输入更多内容并记住(无论你喜欢什么方式)以后需要这个正确的名称或哈希 ID 的--onto

(如果您可以将此刻指向 cool-feature-A 的提交的哈希 ID 放入 cool-feature-B 的上游的 reflog 中,您可以使用 --fork-point 功能让 Git 自动为您计算,前提是所涉及的 reflog 条目没有过期。但总的来说,这可能比手动执行要难一些。另外还有那个“提供”的部分。)

为什么会这样

像往常一样,让我们从图形绘制开始。最初,在您自己的存储库中,您有以下设置:

...--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/名称(即两个远程跟踪名称):在他们的存储库中,在origin处,他们获取了提交BCD,并将其cool-feature-Acool-feature-B名称设置为分别记住提交CD,因此你自己的Git添加了这两个名称的origin/变体。

如果他们(任何“他们”的人-控制origin上存储库的人)执行快进合并,他们将把他们的主干向上移动以指向提交C。(请注意,GitHub web界面没有按钮可以进行快进合并。我没有使用过GitLab的Web界面;它可能在各种方面有所不同。)如果他们强制执行实际合并-这就是GitHub网页上的“现在合并”按钮默认执行的操作;再次说明,我不知道GitLab在这里做什么-他们将创建一个新的合并提交E

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

(这里我故意去掉了所有名称,因为它们与您的名称不完全匹配)。他们可能会删除(或甚至从未实际创建)他们的cool-feature-A名称。无论哪种方式,您都可以快速转发您自己的Git 名称,同时更新您的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将枚举从D-D,然后是C,再然后是B等可达提交的列表,并减去从master可达提交的列表:E(如果存在) ,然后A(如果存在E)和C(无论E是否存在),然后是B等。减法的结果就是提交D

现在,你的Git会复制此列表中的提交,使得新副本出现在master提示后面:即在实际合并之后的E后面,或者在快进合并之后的C后面。因此,Git将D复制到一个新的提交D',并将其放置在E之后。

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

或者它保留了C,并且不做任何操作,因为D已经在C之后(所以没有新的内容需要绘制)。

棘手的部分出现在他们使用GitLab相当于GitHub的"squash and merge"或"rebase and merge"按钮时。我将跳过"rebase and merge"的情况(通常不会有问题,因为Git的rebase也会检查patch-ID),直接进入困难情况——"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是一个新提交的结果,其快照将与合并E匹配(如果他们进行合并),但其父提交是现有的提交A。因此,历史记录——从X向后工作列举的提交列表只有X,然后是A,再然后是A之前的任何提交。

如果在cool-feature-B上运行常规的git rebase master,Git将:

  1. 枚举从D可达的提交: DCBA,...;
  2. 枚举从X可达的提交: XA,...;
  3. 从步骤1的集合中减去步骤2的集合,得到DCB
  4. 复制这些提交(以非倒序方式),使它们出现在X之后。

请注意,步骤2和3都使用单词master来查找提交:要复制的提交(步骤2)是那些从master无法到达的;将复制品放置的位置(步骤3)是在master末端之后。

但如果您运行:

git rebase --onto master cool-feature-A

你在步骤2和3中使用了不同的Git项目:

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

因此,现在Git仅复制DD',然后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 repo的人使用了真正的合并或快进却不完全是合并,这一切仍将起作用:您的Git将在向后枚举D时从列表中删除C-on-backwards,只留下D进行复制;然后Git会复制D以使其出现在E之后(真正的合并情况)或C(快速前进情况)之后。快速前进情况,“将D复制到它已经存在的位置”,聪明地根本不需要复制,而只需保持一切不变。

4
哇,感谢你花时间写下这些。 - Duncan Jones
你接下来更新 origin/cool-feature-B 指向新的本地分支的步骤是什么?在这里使用 git push -f 安全吗? - zeroliu
1
@zeroliu:请记住,origin/cool-feature-B 是你的 Git 记录了另一个 Git 的 cool-feature-B 名称。因此,你必须说服那个 Git 改变它自己的名称——更准确地说,改变存储在其分支名称中的哈希 ID——为此,通常需要某种形式的强制推送。--force-with-lease 选项是在你担心其他人也可能尝试更新其他 Git 的分支名称时执行此操作的好方法;如果你确定现在没有其他人在处理它,git push -f 更简单。 - torek

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