如何避免Git交互式变基破坏拉取请求?

3
我在GitHub上为一个开源项目做贡献。我创建了一个pull request,由维护者进行了审核并要求我重新修改一些东西。与此同时,其他人也做出了很好的贡献。因此,我首先使用git rebase master命令将我的pull request转移到最新的master分支上,然后使用交互式变基功能git rebase -i HEAD~5修复部分提交内容,最后使用git push --force命令推送到我的远程分支。
然而,这之后,GitHub认为我的分支不能再合并到master分支中了,因为存在冲突。实际上,它认为我的分支添加了一些其他提交记录(不是我的),这些提交记录显然和master分支中的相同提交记录发生了冲突。
我做错了什么,应该如何才能做正确呢?
更多细节:
假设master分支有以下历史记录:
(master) A -> B -> C -> D

然后我提交了XYZ,因此我的分支历史如下:

(my-branch) A -> B -> C -> D -> X -> Y -> Z

假设在此期间有人提交并推送了代码库中的代码'E'到'master'分支,请做出相应处理:
(master) A -> B -> C -> D -> E

首先,在my-branch上执行git rebase master。执行完这个命令后,我的分支历史如下:

(my-branch) A -> B -> C -> D -> E -> X -> Y -> Z

然后我执行以下命令:git rebase -i HEAD~5
pick D
pick E
pick X
sqash Y
edit Z

请注意,我不会更改 DE。 之后我的历史记录如下:
(my-branch) A -> B -> C -> D -> E -> X -> Z

然后我执行git push myremote my-branch --force

此后,GitHub声称我的拉取请求不能再合并了,因为我的分支上的提交CDEmaster上的CDE发生冲突。请注意,我没有编辑这些提交,只编辑了自己的提交(而且C甚至不是交互式变基的一部分)。

为什么git会这样呢?
如何避免git交互式变基破坏拉取请求?

2个回答

5

您需要小心交互式变基(或任何变基,但实际上它更容易显示为交互式变基,因为它非常灵活)。

详细考虑提交图可能有所帮助。具体来说,这种类型的绘图本质上有些错误:

(master) A -> B -> C -> D

更好的绘制方法是:

A  <-B  <-C  <-D   <--master

关键是将分支名称放在右侧,并带有箭头。 其余的箭头可以转换为直接链接:
A--B--C--D   <-- master

因为所有提交都是只读的。很难画出内部箭头,所以我们知道它们实际上不能更改,因此可以将它们绘制为连接线。至少最初绘制它们的原因是箭头与子代相关联,而不是父代:子代知道它们的父提交是谁——例如,D 知道要返回到 C,但父代不知道它们的子代是谁。无法从AB;我们只能从B 返回到A,然后意识到嘿,我们从B到达了这里,因此必须有一条从AB的路径。
这是使用Git时的关键认识:它所有的操作都是反向的
所有这些事情之所以重要,是因为在并行开发过程中会发生什么。正如您所说,您创建了XYZ
A--B--C--D   <-- master
          \
           X--Y--Z   <-- my-branch

您的X知道D,但是与您共享的多个Git存储库一样,D却不知道您的X
同时,其他人创建了E,它也指向D。如果我们想象一种某种神奇的超级存储库可以同时知道EX-Y-Z,那么我们可以将其绘制如下:
           E   <-- third-person
          /
A--B--C--D   <-- master
          \
           X--Y--Z   <-- my-branch

然后他们——这里的“他们”指的是GitHub上的开源组织——将第三人称的E收入了他们自己的master中:

           E   <-- master, third-person
          /
A--B--C--D

(他们可能还不知道你的 X-Y-Z,所以我们从图纸上将它们去掉)。现在你可以运行 git fetch upstream 命令将 GitHub 仓库版本更新到你的本地计算机,并且在你自己的仓库中,仍然有 X-Y-Z

           E   <-- upstream/master
          /
A--B--C--D   <-- master, origin/master
          \
           X--Y--Z   <-- my-branch (HEAD), origin/my-branch

(I'm assuming here that your origin represents your GitHub fork, and your upstream represents the original GitHub repository from which you made your fork).

不使用交互式变基

当你运行命令git rebase upstream/master或让你的master指向提交E并运行git rebase master时,Git会复制提交XYZ
             X'-Y'-Z'  <-- my-branch (HEAD)
            /
           E   <-- upstream/master
          /
A--B--C--D   <-- master, origin/master
          \
           X--Y--Z   <-- origin/my-branch

(这个示意图假设您尚未更新您的master; 如果您已经更新了,我们可以稍微调整一下示意图)。

这些副本——标有勾号,X'-Y'-Z'——已准备好供您强制推送到您的分叉中。 假设您此时这样做,您的分叉将拥有它们,原始的X-Y-Z链将被放弃:

             X'-Y'-Z'  <-- my-branch (HEAD), origin/my-branch
            /
           E   <-- upstream/master
          /
A--B--C--D   <-- master, origin/master
          \
           X--Y--Z   [abandoned]

如果我们同时更新您自己的masterorigin/master,我们就可以将所有内容简化为以下形式:
A--B--C--D--E   <-- master, origin/master, upstream/master
             \
              X'-Y'-Z'   <-- my-branch (HEAD), origin/my-branch

这些内容都在你的电脑上的存储库中,如果是在GitHub上,你的分支只需命名为mastermy-branch,没有origin/部分,也没有upstream/部分。

你的拉取请求将自动更新为使用新的X'-Y'-Z'提交,这些提交仅在提交E的基础上添加,因此如果他们的存储库仍以提交E结束,则不应该存在吸收你的提交的问题。

使用交互式变基

如果你想将Y'压缩到X'中,并对Z'的消息进行处理,现在可以使用交互式变基来完成。或者,你可以选择在初始变基的基础上执行所有操作:

$ git checkout my-branch
$ git rebase -i upstream/master

请注意,这里的 upstream/master 实际上指的是我们图表中的“提交 E”,它是其中一个将 E 作为 upstream/master,而将 ZZ' 作为你自己的 my-branch 提交顶部的提交之一。
Git 现在将列出那些位于 my-branch 上但不在 upstream/master 上的三个提交,即从 ZZ' 开始并向后工作到 A,列出这些提交,然后从该列表中删除所有从 E 开始向后工作的提交。这三个提交的哈希 ID 将进入三个 pick 命令。
现在,您可以像以前一样将一个更改为 squash,将另一个更改为编辑:由于您仅处理自己的提交,而不处理其他人的提交,因此您不会将任何其他人的提交复制到新的和不同的提交中。编辑完成后,您将再次拥有新的提交——如果您已经将 X 复制到 X',将 Y 复制到 Y',并将 Z 复制到 Z',现在您将拥有 X",它是 X' + Y'(squashed) 和 Z"Z' 的编辑版本,但连接到 X"
              X"-Z"   <-- my-branch (HEAD)
             /
A--B--C--D--E   <-- master, origin/master, upstream/master
             \
              X'-Y'-Z'   <-- origin/my-branch

现在,您可以强制推送这些到您的分支,使得它们的my-branch(在这里称为origin/my-branch)指向提交Z",这样您的分支就被更新了,您的pull请求也会自动更新,并且您准备好让他们查看X"Z"了。


1
感谢详细的解释。然而,我仍不清楚为什么我的原始方法失败了。由于我已经在上游主分支上进行了变基,然后才运行交互式变基,所以我不知道出了什么问题。 - Venemo
@Venemo:交互式合并通常会尝试“快进”现有的提交而不是复制它们,但鉴于您的结果,它必须已将我们标记为E的提交复制到了一个新的E'。然后,您的拉取请求具有您的E'D之上,这与他们的ED之上发生冲突(或者在C之上拥有两个副本)。很难没有看到实际的提交图,而GitHub坚持隐藏实际的图(他们的“网络图”完全是另一回事,并且似乎永远都不太正确)。 - torek
抱歉,我不明白。为什么交互式变基会“复制”E或其他内容? - Venemo
我理解为什么它会复制我实际修改的提交,但不理解为什么它会复制其他任何提交。 - Venemo
就像我之前所说的,通常情况下它是不会的,但在某些情况下它是被强制执行的:例如,如果你有意或无意地进行任何更改,包括更改它们的父链接;或者如果你使用了 --no-ff - torek
显示剩余2条评论

0
一个想法是在命令行上指定合并策略,始终通过从上游存储库获取更改来解决冲突。
git rebase -s recursive -X ours master

虽然不太直观,但在变基过程中,“ours”和“theirs”会被颠倒。因此,这将使用合并的上游更改来解决任何冲突。要始终采用您的更改,请在变基过程中传递“theirs”(更多详细信息:https://git-scm.com/docs/git-rebase#git-rebase---strategy-optionltstrategy-optiongt

进行交互式变基以清理任何历史记录,强制推送到您的分支... 在 PR 过程中,从 GitHub 的角度来看,就不应该有任何冲突了。


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