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

git rebase / squash - why do I need to resolve conflicts again?

我确实尝试过浏览类似的主题,但我似乎无法理解这个概念。

我分叉了一个仓库,做了一些修改。加班,我也用了几次git fetch upstreamgit merge upstream/<branch>。有时,我解决并重新测试了一些冲突。

现在谈到向上游推送更改时,我想进行一次提交。我得到的指示是使用 git fetch upstreamgit rebase -i upstream/<branch>

我不明白的是,我再次被困在处理冲突解决方案中。我不明白为什么当我的叉子是最新的时我需要解决冲突。我本可以备份我所有修改过的文件,核对我的叉子,再次叉子,恢复备份,这样我就没有冲突需要解决并准备好提交。这个过程看起来很机械,我不明白为什么我必须走艰难的路(再次解决冲突)。

谁能帮我理解一下?

"why do you need to re-resolve everything" 的最佳答案是 "you don't"。但这意味着放弃给你的指示(我不清楚是谁给你这些指示)。

一样,您可以使用 git rerere 来自动化 re 使用以前 recorded 重新解决方案。通过这种方式,您 重新解决所有问题,但让 Git 来解决,而不是必须手动解决。

不过,如果您的意图是在当前 upstream 提示从中发出拉取请求,无需执行任何操作。您可以只执行以下命令序列(注意下面的假设):

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.

的 Web 服务上的可点击 Web 按钮发出拉取请求

最终,他们接受了您的拉取请求,此时您将 删除 branch,放弃所有这些工作,转而支持他们提出的新的单一提交公认。 (请参阅结尾以了解如何 rename/reset。)或者,他们不接受它,此时您可以删除 for-pull-request,并可以继续处理 branch 并最终重复过程。

假设:

  • 您的复刻包含两个存储库:您的存储库——您自己的计算机/笔记本电脑/其他任何东西上的存储库——以及网络服务提供商(如 GitHub 上的另一个存储库。
  • 他们(upstream 的)分支在同一个网络服务提供商上。
  • 在您的计算机上,在您的存储库中,您使用短名称 upstream 来指代上游存储库(他们的分支),并使用短名称 origin 来指代您自己的分支作为存储在 Web 服务提供商上。 Web 提供商提供了可点击的按钮,可以发出拉取请求。
  • 也许最重要的是,这假定您愿意放弃本地 b运行ch 以支持上游接受的拉取请求,如果发生这种情况的话。

理解所有这些有几个关键,其中大部分与 Git 的 提交图 的工作方式有关。其余部分与 Git 分发存储库的(单一)魔术有关,以及各种命令 - git mergegit rebasegit merge --squash - 实际上 ,以及git fetchgit push真正做的。

我真的不想再写一篇关于这个的巨篇文章(...太迟了:-)),所以让我们通过注意这些要点来总结一下。它们的顺序不是很好,但我不确定 是否是一个很好的顺序:有很多交叉引用项。

  • Git 通常只是添加新的提交。添加新提交会导致当前 b运行ch 名称(存储在 HEAD 中)指向新提交,而新提交指向 HEAD 提交。
  • 但是 git rebase 通过 复制 现有提交到新提交,然后 放弃 原始提交来工作。它使用 "detached HEAD" 进行复制,其中 HEAD 直接指向提交,而不是使用 b运行ch 名称。 ("Add new commit" 仍然像往常一样工作,除了它只是直接更新 HEAD,而不是名称不再存储在 HEAD 中的 b运行ch。)复制的提交不包括任何合并提交。在某种程度上,这意味着您当时所做的任何冲突解决都将大部分丢失(除非您启用了 git rerere)。 (实际上因为各种原因丢失了,我还没想好怎么解释。)
  • 每个副本都好像是由 git cherry-pick 制作的,有时是 运行ning git cherry-pick.
  • 每个 cherry-pick 都会导致合并冲突,因为 cherry-picking 提交 运行s 合并机制,合并为动词我喜欢这样称呼它。

    此操作的 合并基础 是被精心挑选的提交的父级,即使这不是一个非常明智的合并基础。 --ours 提交是当前或 HEAD 提交,一如既往,另一个提交 - --theirs 提交 - 是被复制/挑选的提交。新提交,在成功的 cherry-pick 结束时,作为普通的非合并提交。

  • 运行 git merge,相比之下,有点不那么复杂,除了有各种不同的情况。

    有时Git发现根本不需要合并,可以进行快进操作:将当前b[=的提交更改为345=]ch 点,使 b运行ch 指向目标提交。

    有时需要合并,或者(使用 --no-ff)您告诉 Git 即使可以也不要快进。在这种情况下,Git 使用合并机制进行合并。此合并的合并基础是两个提示提交的实际合并基础。与所有合并一样,此处可能存在合并冲突。最后的最终提交是合并提交:merge as an adjective or noun.

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

  • 运行 git merge --squash 是另一个特例。与实际合并一样,它使用正常的合并机制来计算涉及的两个 b运行ch 提示的 true 合并基础。

    要合并的两个提交当然是一如既往的HEAD--ours),以及您在命令行中命名的提交。由于合并基础是真正的合并基础,它使用现有的合并作为名词合并(合并提交)来最小化新合并冲突,您可以获得相对无冲突的合并结果.

    最后的 提交 然而,又回到了樱桃挑选的想法:最终的提交 不是 合并提交。这只是一个普通的提交。 tree 是由 merge-as-a-a-verb 计算的,但提交是常规提交,而不是 merge-as-a-a-noun。 (没有特别好的理由,--squash 标志也会打开 --no-commit 标志,迫使你自己 运行 git commit。可能曾经有一个原因——可能是对于编写初始 --squash 代码的人来说,提前退出是最方便的——但今天没有理由这样做。)

我们添加这些事实:

  • 您的分支最初是某个其他存储库的克隆。所以它从 upstream 的相同提交(相同的历史记录)开始。从那时起,你和他们有点分歧,但同时你通过 git push-ing 从你自己的本地存储库提交来更新你的 fork。

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

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

    您的存储库有您自己的 b运行ches(当然)。

    这意味着您的本地存储库(您自己计算机上的存储库)具有 完整联合 "their fork"、"your fork" 和 "your commits".您可以随时将自己的提交推回自己的分叉。

  • 你可以很容易地使用你的叉子制作 "pull requests",因为你的提供商(例如,GitHub)会为你记住 link 之间的 "your fork" ] 和 "their fork".

因此,让我们画出您存储库中的提交图或简化版的提交图,结果是:

I forked a repo, made some changes. Overtime, I also used git fetch upstream and git merge upstream/<branch> a few times. Some time, there are conflicts which I resolved and retested.

您有:

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

在这里,提交 * 是您和 upstream 开始的公共基础提交。您在自己的 b运行ch 上进行了一些提交,例如 AC。您还至少进行了一次合并提交 M。我假设你 运行 git push origin branch 在不同的点,所以 origin/branch 在你自己的存储库中记录你的分支的 b运行ch 名称 branch 指向你的提示提交 C。 (用不用并不重要,因为我们下面不用它。)

如果现在 运行 git rebase -i upstream/branch,这将列出提交 ABC,但是 提交M,因为提交复制("pick")。副本的目标是提交 T,这是 upstream/branch 的提示,您的 Git 从 运行ch branch 中记住了 [= =24=].

如果您容忍重做所有合并冲突,并将三个提交复制到三个新提交,您将得到:

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

然后您可以将其推送(或强制推送)到 origin,或者您现在可以(这次没有合并冲突)将 A'-B'-C' 序列折叠为单个 "squashed" 提交 S:

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

然后再次将其推送或强制推送到您的叉子 origin as b运行ch-name branch.

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

但是,如果相反,我们创建一个名为 for-pull-request 的新 b运行ch,指向提交 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。我们将找到合并基础,它现在是我标记为 * 的提交:它是 first(或 "lowest")common 祖先,而不是 rebase 使用的最后一个不相交的祖先 A。也就是从commitC和commitT开始往回推,Git找到commit"nearest to"两个b运行ch提示,也就是commit你最后合并了。

这意味着您将看到的唯一冲突是您在 C 中所做的任何事情(在上次合并 M 之后)与他们在 *.[=132 之后所做的任何事情发生冲突=]

git merge --squash branch 使用合并机制完成合并提交 TC 使用此 * 作为合并基础时,它会停止并让你 运行 git commit 手动。当你这样做时,你会得到一个新的普通提交,我们可以将其称为 S for 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 具有相同的 source 我们会得到另一种方式——但我们将有更少的,如果有的话,合并首先要解决的冲突。

您现在可以将此名称(并提交 S)推送到提供商(GitHub?)上的分支,并执行合并请求。如果他们接受了,你可以删除你的b运行chbranch,然后创建一个新的b运行chbranch指向S(或者它在 upstream 中的合并,或者如果 它们 挤压合并,它们的 S'S 基本相同但是具有不同的哈希 ID 和不同的提交者:在任何情况下,您都必须再次 git fetch upstream 才能获得他们的提交)。

您可以简单地强制您的 b运行ch 名称 branch 指向最新的提交,而不是删除并重新创建。假设他们将你的 squash 合并到他们的 fork 中,这样他们就有了新的提交 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

(这些 b运行 中哪些是 HEAD 取决于您使用的命令序列)。

一旦您 git push --force origin branch 将更新的 branch 发送到您的供应商分叉并删除 for-pull-request (在您的回购和分叉中),您将有效地放弃 origin/branch 系列提交,给出:

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

如果我们停止绘制所有废弃的提交,一切看起来都干净整洁,这可能就是所有这一切的意义所在。 :-)