git rebase / squash - 为什么我需要再次解决冲突?
git rebase / squash - why do I need to resolve conflicts again?
我确实尝试过浏览类似的主题,但我似乎无法理解这个概念。
我分叉了一个仓库,做了一些修改。加班,我也用了几次git fetch upstream
和git merge upstream/<branch>
。有时,我解决并重新测试了一些冲突。
现在谈到向上游推送更改时,我想进行一次提交。我得到的指示是使用 git fetch upstream
和 git 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 merge
、git rebase
和 git merge --squash
- 实际上 做,以及git fetch
和git 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 上进行了一些提交,例如 A
到 C
。您还至少进行了一次合并提交 M
。我假设你 运行 git push origin branch
在不同的点,所以 origin/branch
在你自己的存储库中记录你的分支的 b运行ch 名称 branch
指向你的提示提交 C
。 (用不用并不重要,因为我们下面不用它。)
如果现在 运行 git rebase -i upstream/branch
,这将列出提交 A
、B
和 C
,但是 不提交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
时,它特别有趣,因为这是 branch
和 upstream/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
使用合并机制完成合并提交 T
和 C
使用此 *
作为合并基础时,它会停止并让你 运行 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/branch
或 git 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]
如果我们停止绘制所有废弃的提交,一切看起来都干净整洁,这可能就是所有这一切的意义所在。 :-)
我确实尝试过浏览类似的主题,但我似乎无法理解这个概念。
我分叉了一个仓库,做了一些修改。加班,我也用了几次git fetch upstream
和git merge upstream/<branch>
。有时,我解决并重新测试了一些冲突。
现在谈到向上游推送更改时,我想进行一次提交。我得到的指示是使用 git fetch upstream
和 git 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
.
最终,他们接受了您的拉取请求,此时您将 删除 branch
,放弃所有这些工作,转而支持他们提出的新的单一提交公认。 (请参阅结尾以了解如何 rename/reset。)或者,他们不接受它,此时您可以删除 for-pull-request
,并可以继续处理 branch
并最终重复过程。
假设:
- 您的复刻包含两个存储库:您的存储库——您自己的计算机/笔记本电脑/其他任何东西上的存储库——以及网络服务提供商(如 GitHub 上的另一个存储库。
- 他们(
upstream
的)分支在同一个网络服务提供商上。 - 在您的计算机上,在您的存储库中,您使用短名称
upstream
来指代上游存储库(他们的分支),并使用短名称origin
来指代您自己的分支作为存储在 Web 服务提供商上。 Web 提供商提供了可点击的按钮,可以发出拉取请求。 - 也许最重要的是,这假定您愿意放弃本地 b运行ch 以支持上游接受的拉取请求,如果发生这种情况的话。
理解所有这些有几个关键,其中大部分与 Git 的 提交图 的工作方式有关。其余部分与 Git 分发存储库的(单一)魔术有关,以及各种命令 - git merge
、git rebase
和 git merge --squash
- 实际上 做,以及git fetch
和git 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
制作的,有时是 运行ninggit 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
andgit 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 上进行了一些提交,例如 A
到 C
。您还至少进行了一次合并提交 M
。我假设你 运行 git push origin branch
在不同的点,所以 origin/branch
在你自己的存储库中记录你的分支的 b运行ch 名称 branch
指向你的提示提交 C
。 (用不用并不重要,因为我们下面不用它。)
如果现在 运行 git rebase -i upstream/branch
,这将列出提交 A
、B
和 C
,但是 不提交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
时,它特别有趣,因为这是 branch
和 upstream/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
使用合并机制完成合并提交 T
和 C
使用此 *
作为合并基础时,它会停止并让你 运行 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/branch
或 git 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]
如果我们停止绘制所有废弃的提交,一切看起来都干净整洁,这可能就是所有这一切的意义所在。 :-)