git rebase / squash - 为什么我需要再次解决冲突?

4

我尝试了查找类似的主题,但似乎无法掌握这个概念。

我fork了一个repo,并进行了一些更改。随着时间的推移,我还使用了git fetch upstreamgit merge upstream/<branch>几次。有时会出现冲突,我解决了并重新测试了。

现在当涉及将更改推送到上游时,我想进行单个提交。我收到的指令是使用git fetch upstreamgit rebase -i upstream/<branch>

我不明白的是,我再次被困在处理冲突解决方案中。我不明白为什么在我的fork与其原始版本相同的情况下,我需要解决冲突。我可以备份所有修改过的文件,删除我的fork,再次fork,恢复备份,就没有要解决的冲突,可以准备提交。这个过程似乎很机械化,我不明白为什么我必须走弯路(再次解决冲突)。

有人能帮我理解吗?


2
rerere可能会有帮助。请查看https://git-scm.com/blog/2010/03/08/rerere.html。 - ElpieKay
1个回答

5
"为什么需要重新解决所有问题"的最佳答案是“不需要”。但这意味着放弃你所接到的指示(我不清楚是谁给你这些指示)。
正如ElpieKay的评论,你可以使用git rerere来自动化以前记录的解决方案的重复使用。通过这种方式,你确实在重新解决所有问题,但是让Git来完成,而不是手动完成。
如果你的目的是在当前的上游代码库顶部创建一个新的“压缩合并”提交(即不是合并,只是普通提交),以从中拉取请求,则无需执行任何操作。你只需执行以下命令序列(请注意以下假设):"
git fetch upstream
git checkout -b for-pull-request upstream/branch
git merge --squash branch
git commit
git push origin -u for-pull-request

然后使用网站上的点击按钮将更改请求提交到名为“origin”的网络服务中。最终,如果他们接受了您的更改请求,此时您将删除分支,在新的单提交中放弃所有工作(有关重命名/重置的信息,请参见末尾)。或者,如果他们不接受它,则可以删除for-pull-request,并可以继续在branch上工作,最终重复这个过程。这假定以下情况:您的fork包括两个存储库:您自己电脑/笔记本电脑/其他设备上的存储库和一个网络服务提供商(如GitHub)上的另一个存储库。他们(upstream)的fork与同一网络服务提供商上的您使用短名称“upstream”在您的存储库中引用上游存储库(他们的fork),并使用短名称“origin”引用作为存储在网络服务提供商上的自己的fork。网络提供者提供了制作拉取请求的按钮。可能最重要的是,这假定您愿意放弃该本地分支,转而采用上游已接受的拉取请求(如果存在)。要理解所有这些内容,有几个关键点与Git的提交图形方式密切相关。其余部分与Git分发存储库的(单个)魔法技巧以及各种命令(git merge,git rebase和git merge --squash)实际上所做的事情以及git fetch和git push的真正含义有关。不想再写一篇巨型文章(...太迟了 :-)),因此让我们总结一下这些要点。它们的顺序不是很好,但我不确定是否有很好的顺序:有很多交叉引用项。
  • Git通常只添加新的提交。添加新的提交会导致当前分支名称 - 存储在HEAD中 - 指向新的提交,而新的提交则指回先前的HEAD提交。
  • git rebase通过将现有提交复制到新的提交中,然后放弃原始提交来工作。它使用“游离HEAD”进行复制,其中HEAD直接指向提交,而不是使用分支名称。 (“添加新提交”仍然像往常一样工作,只是它直接更新HEAD而不是存储在HEAD中的分支。)被复制的提交不包括任何合并提交。从某种意义上说,这意味着您当时做的任何冲突解决都会大量丢失(除非您启用了git rerere)。 (实际上由于不同的原因丢失了,我还没有想清楚如何解释。)
  • 每个副本基本上都是通过git cherry-pick生成的,有时甚至是通过运行git cherry-pick
  • 每个cherry-pick可能会导致合并冲突,因为cherry-pick一个提交会运行合并机制,就像动词合并一样。

    此操作的合并基础是要复制/ cherry-pick的提交的父级,即使那不是非常合理的合并基础。 --ours提交始终是当前或HEAD提交,而另一个提交 - --theirs提交 - 是被复制/ cherry-pick的提交。 在成功cherry-pick结束时生成的新提交是普通的、非合并的提交。

  • 相比之下,运行git merge则有些简单,尽管有各种单独的情况。

    有时Git会发现实际上不需要进行合并,可以执行快进操作:更改当前分支指向的提交,以便该分支指向目标提交。

    有时需要合并或者(使用--no-ff)您告诉Git即使可能也不要进行快进。 在这种情况下,Git使用合并机制来执行合并。 此合并的合并基础是两个最终提交的实际合并基础。 像所有合并一样,这里可能会出现合并冲突。 最后生成的提交是合并提交:形容词或名词合并。

    提交图中存在合并提交意味着将来的合并将找到一个新的、更好的合并基础。 这将避免重新解决冲突。

  • 运行git merge --squash是另一种特殊情况。 像实际合并一样,它使用正常的合并机制来计算涉及两个分支提示的真实合并基础。

    要合并的两个提交当然是像往常一样的HEAD--ours)和您在命令行上指定的提交。 由于合并基础是真实合并基础,它使用现有的名词合并(合并提交)来最小化新的合并冲突,因此您可以得到相对无冲突的合并结果。

    然而,最终的提交与cherry-pick的想法有关:最终

    我们需要补充以下事实:
    • 你的分支最初是某个其他存储库的克隆。因此,它最开始具有与“上游”相同的提交(相同的历史记录)。自那时以来,你和他们已经有了一些不同,但同时你通过从自己的本地存储库中推送提交来更新你的分支。

    • 当你执行git fetch upstream时,你会从他们的分支中获取他们的提交,并将它们放入你自己的本地存储库中。这些提交的跟踪名称为upstream/master等。

      当你执行git fetch origin(如果你需要),你会从你的分支中获取提交,并将它们放入你自己的本地存储库中。这些提交的跟踪名称为origin/master等。

      你的存储库有自己的分支(当然)。

      这意味着你的本地存储库——也就是你自己的电脑上的存储库——具有“他们的分支”、“你的分支”和“你的提交”的完整联合。你可以随时将自己的提交推回自己的分支。

    • 你可以很容易地使用你的分支创建“拉取请求”,因为你的提供者(例如GitHub)会为你记住“你的分支”和“他们的分支”之间的链接。

    因此,让我们画出你在存储库中拥有的提交图表,或其简化版本,如下所示:

    我分叉了一个存储库,做了一些更改。随着时间的推移,我也使用了git fetch upstreamgit merge upstream/<branch>几次。有时候会出现冲突,我会解决并重新测试。

    你拥有:

    ...--o--*--o...o...o--T    <-- upstream/branch
             \      \
              A--B---M---C   <-- branch (HEAD), origin/branch
    

    在这里,“commit *” 是你和“upstream”起点的共同基础提交。你在自己的分支上做了一些提交,例如 A 到 C,还至少做了一个合并提交 M。我假设你在各个时间点上都运行了 “git push origin branch”,这样你自己的仓库中的“origin/branch”记录了你的分支名称“branch”指向你的最新提交“C”。即使没有这样做,因为我们以下不使用它,所以也无关紧要。
    如果你现在运行“git rebase -i upstream/branch”,这将列出提交 A、B 和 C,而不是提交 M,作为要复制(“pick”)的提交。要复制的目标是提交 T,这是你的 Git 从“upstream”的“branch”分支上记住的终点。
    如果你愿意重新处理所有合并冲突,并将这三个提交复制为三个新提交,那么你会得到:
                            A'-B'-C'   <-- branch (HEAD)
                           /
    ...--o--*--o...o...o--T    <-- upstream/branch
             \      \
              A--B---M---C   <-- origin/branch
    

    你可以将这个更改推送(或强制推送)到origin,或者你现在可以(不会有合并冲突),将A'-B'-C'序列压缩成一个单一的"压扁"提交S

                            S   <-- branch (HEAD)
                           /
    ...--o--o--o...o...o--T    <-- upstream/branch
             \      \
              A--B---M---C   <-- origin/branch
    

    再次将此推送或强制推送到您的派生分支origin作为分支名称branch

    (注意,一旦基本提交*不再是有趣的基本提交,我就停止标记它了。当我们考虑运行git rebase -i upstream/branch时,它特别有趣,因为它是branchupstream/branch永久重新加入的点。这确定了必须复制的提交集用于git rebase操作。)

    但如果我们创建一个名为for-pull-request的新分支,并指向提交T,并将其检出:

    ...--o--o--o...*...o--T    <-- for-pull-request (HEAD), upstream/branch
             \      \
              A--B---M---C   <-- branch, origin/branch
    

    现在我们运行git merge --squash branch。这将调用合并机制,使用当前提交T作为HEAD,另一个提交为C。我们会找到合并基础,现在我标记的提交是*:它是第一个(或“最低”)公共祖先,而不是重新定位使用的最后一个不相交的祖先A。也就是说,从提交C和提交T开始向后工作,Git找到了“最接近”两个分支端点的提交,这是您上次合并的提交。
    这意味着您将看到的唯一冲突是您在C中所做的任何与自*以来他们所做的任何事情冲突的内容。
    git merge --squash branch完成使用合并机制合并提交TC并使用此*作为合并基础后,它停止并让您手动运行git commit。当您执行该操作时,您将获得一个新的普通提交,我们可以称之为S代表Squash。
                            S   <-- for-pull-request (HEAD)
                           /
    ...--o--o--o...o...o--T    <-- upstream/branch
             \      \
              A--B---M---C   <-- branch, origin/branch
    

    现在,除了这个命名为for-pull-request之外,我们完成的图表与按照给定的指令使用git rebase -i upstream/branch得到的图表是一样的。此外,如果我们正确解决任何合并冲突,提交S将具有与另一种方式相同的源代码--但是我们将有更少的合并冲突需要解决。
    现在您可以将此名称(和提交S)推送到您提供程序上的分叉中(GitHub?),并执行合并请求。如果他们接受它,您可以删除分支branch,然后创建一个指向S的新分支branch(或者如果他们选择squash-merge,则指向其合并中的S',它基本上与S相同,但具有不同的哈希ID和不同的提交者:无论如何,您都必须再次git fetch upstream以获取他们的提交)。
    而不是删除和重新创建,您可以简单地强制使您的分支名称branch指向最新的提交。假设他们将您的压缩合并到他们的分叉中,以便他们有了一个名为S'的新提交,该提交是S的副本,您可以通过git fetch获取它:
                            S   <-- for-pull-request (HEAD), origin/for-pull-request
                           /
    ...--o--o--o...o...o--T--S'  <-- upstream/branch
             \      \
              A--B---M---C   <-- branch, origin/branch
    

    现在你可以运行git branch -f branch upstream/branchgit checkout branch && git reset --hard upstream/branch来让你的Git具备以下内容:
                            S   <-- for-pull-request, origin/for-pull-request
                           /
    ...--o--o--o...o...o--T--S'  <-- branch, upstream/branch
             \      \
              A--B---M---C   <-- origin/branch
    

    这些分支中哪一个是HEAD取决于你使用的命令序列。

    一旦你使用git push --force origin branch将更新后的branch推送到提供者上的你的fork并删除for-pull-request(同时在你自己的repo和fork中),你就会有效地放弃origin/branch系列的提交,从而得到:

                            S   [abandoned]
                           /
    ...--o--o--o...o...o--T--S'  <-- branch, origin/branch, upstream/branch
             \      \
              A--B---M---C   [abandoned]
    

    如果我们停止绘制所有被弃用的提交,一切看起来都干净整洁,这可能就是所有这些工作的目的。 :-)


感谢您详细的回复,这正是我所需要的。但是在我完成这个操作后,当我尝试使用新更改再次向我的分支请求拉取时,它仍然会显示所有提交记录,包括我的上一个拉取请求(只有提交记录(A、B、M、C),而不是更改)。我应该做些什么来使我的分支与上游保持同步? - some user
@someuser:我感觉有些地方不太明白,因为你在谈论“你的分支”(我猜想这意味着你在本地机器上有一个存储库,并且在GitHub上有一个你称之为“你的分支”的存储库),以及一个拉取请求(我猜想这意味着有另一个 GitHub 存储库不是你的,你从中创建了你的 GitHub 存储库)... - torek
我通过将社区repo(上游)fork到我的账户(我的repo)中来处理一个repo。我首先提交了更改到我的repo,并发起了一个拉取请求以将其合并到社区repo中。我按照您的建议,将我的更改合并到我的repo的本地分支中,并使用它来发起拉取请求。但是,如果我再次尝试发起拉取请求,它会列出我之前所做的所有提交,包括以前的拉取请求。 - some user
如果您使用单个Squash-Merge替换原始工作,然后(如有必要)强制推送Squash-Merge到您的分支,最后使用附加到您的分支的Web界面执行拉取请求,则在拉取请求中仅获取一个S'提交。看到A--B--M--C表示您的分支仍具有旧提交(无论本地存储库中有什么)。 - torek

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