“为什么你需要重新解决所有问题”的最佳答案是“你不需要”。但这意味着放弃给你的指示(我不清楚是谁给你这些指示的)。
正如ElpieKay 评论的那样,您可以使用git rerere
自动重用以前记录的re解决方案。通过这种方式,您正在重新解决所有问题,但让 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
然后使用 web 服务上的 clicky web 按钮发出拉取请求origin
。
最终,他们接受了您的拉取请求,此时您将删除 branch
,放弃所有这些工作,转而支持他们接受的新单一提交。(有关如何重命名/重置,请参阅末尾。)或者,他们不接受它,此时您可以 delete for-pull-request
,并且可以继续工作branch
并最终重复该过程。
这假设:
- 你的 fork 由两个存储库组成:你的存储库——一个在你自己的计算机/笔记本电脑/任何东西上——另一个存储库在 GitHub 等 Web 服务提供商上。
- 他们的 (
upstream
's) 分支在同一个 Web 服务提供商上。
- 在您的计算机上,在您的存储库中,您使用短名称
upstream
来引用上游存储库(他们的分支),并使用短名称origin
来引用存储在 Web 服务提供商上的您自己的分支。Web 提供商提供了发出拉取请求的可点击按钮。
- 也许最重要的是,这假设您愿意放弃该本地分支,转而支持上游接受的拉取请求(如果发生这种情况)。
理解所有这些有几个关键,其中大部分都与 Git 的提交图的工作方式有关。其余的与 Git 分发存储库的(单一)魔术有关,以及各种命令(<code>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-picking一次提交运行合并机制,合并为我喜欢的动词。
此操作的合并基础是被精心挑选的提交的父级,即使这不是一个非常明智的合并基础。与往常一样,--ours
提交是当前或HEAD
提交,而另一个提交——--theirs
提交——是被复制/精心挑选的提交。新的提交,在成功的樱桃挑选结束时,作为一个普通的、非合并的提交。
相比之下, Runninggit merge
的复杂程度要低一些,只是有各种不同的情况。
有时 Git 看到毕竟不需要合并,可以进行快进操作:更改当前分支指向的提交,使分支指向目标提交。
有时需要合并,或者(使用--no-ff
)告诉 Git 即使可以快进也不行。在这种情况下,Git 使用合并机制进行合并。此合并的合并基础是两个提示提交的实际合并基础。与所有合并一样,此处可能存在合并冲突。最后进行的最终提交是合并提交:作为形容词或名词合并。
提交图中存在合并提交意味着未来的合并将找到一个新的、更好的合并基础。这将避免必须重新解决冲突。
跑步git merge --squash
是另一种特殊情况。像实际的合并一样,它使用正常的合并机制来计算所涉及的两个分支提示的真实合并基础。
当然,要合并的两个提交是HEAD
( --ours
) 和您在命令行上命名的提交。由于合并基础是真正的合并基础,它使用现有的merge-as-a-noun合并(合并提交)来最小化新的合并冲突,您可以获得相对无冲突的合并结果。
然而,最终提交又回到了樱桃挑选的想法:最终提交不是合并提交。这只是一个普通的提交。这棵树是由merge-as-a-verb 计算的,但提交是常规提交,而不是merge-as-a-noun。(没有什么特别好的理由,--squash
flag 也打开了--no-commit
flag,迫使你git commit
自己跑。可能曾经有一个原因——可能对于编写初始--squash
代码的人来说,提早退出是最方便的——但今天没有理由为了这。)
我们添加了这些事实:
您的 fork 最初是其他存储库的克隆。所以它开始于相同的提交(相同的历史)upstream
。从那时起,你和他们就有点分歧了,但与此同时,你通过git push
-ing 从你自己的本地存储库提交来更新你的 fork。
当 you 时git fetch upstream
,您从他们的fork 中获取他们的提交,将它们放入您自己的本地存储库中。这些的跟踪名称是等等。upstream/master
当你git fetch origin
(如果你需要)时,你从你的fork 中获取提交,将它们放入你自己的本地存储库中。这些的跟踪名称是origin/master
等等。
您的存储库有自己的分支(当然)。
这意味着您的本地存储库(您自己计算机上的存储库)具有“their fork”、“your fork”和“your commits”的完整联合。您可以随时将自己的提交推送回您自己的分叉。
您可以使用您的 fork 轻松地发出“拉取请求”,因为您的提供商(例如 GitHub)会为您记住“your fork”和“their fork”之间的链接。
因此,让我们绘制存储库中的提交图或简化版本的提交图,结果如下:
我分叉了一个回购,做了一些改变。加班,我也用过git fetch upstream
几次git merge upstream/<branch>
。有时,我解决并重新测试了一些冲突。
你有:
...--o--*--o...o...o--T <-- upstream/branch
\ \
A--B---M---C <-- branch (HEAD), origin/branch
在这里,commit*
是您从其upstream
开始的公共基本提交。您在自己的分支上进行了一些提交,例如A
through 。C
您还至少进行了一次合并提交M
。我假设您git push origin branch
在不同的时间点运行,以便origin/branch
在您自己的存储库中将您的 fork 的分支名称记录branch
为指向您的提示 commit C
。(是否这样做并不重要,因为我们在下面没有使用它。)
如果您现在要运行git rebase -i upstream/branch
,这将列出提交A
、B
和C
,但不列出commit M
,作为要复制的提交(“pick”)。副本的目标是 commit T
,它是 的提示upstream/branch
,你的 Git 从分支branch
上记住它upstream
。
如果您忍受重做所有合并冲突,并将三个提交复制到三个新提交,您将得到:
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-name推送或强制推送到您的 fork branch
。
(请注意,*
一旦它不再是一个有趣的基本提交,我就停止标记基本提交。当我们考虑运行时特别有趣,git rebase -i upstream/branch
因为它是永久重新加入的点。这决定了必须是的提交集复制以进行操作。)branch
upstream/branch
git rebase
但是,如果相反,我们创建一个名为 的新分支for-pull-request
,指向 commitT
并检查它:
...--o--o--o...*...o--T <-- for-pull-request (HEAD), upstream/branch
\ \
A--B---M---C <-- branch, origin/branch
现在我们运行git merge --squash branch
. 这将调用合并机制,使用当前提交T
asHEAD
和另一个提交是C
. 我们将找到合并基础,它现在是我标记的提交*
:它是第一个(或“最低”)共同祖先,而不是A
rebase 使用的最后一个不相交的祖先。也就是说,从提交C
和提交T
开始并向后工作,Git 会找到“最接近”两个分支提示的提交,也就是您上次合并的提交。
这意味着您将看到的唯一冲突是您在C
(最后一次合并之后M
)所做的任何与他们从*
.
当git merge --squash branch
完成使用合并机制合并提交T
并将C
其*
用作合并基础时,它会停止并让您git commit
手动运行。当你这样做时,你会得到一个新的普通提交,我们可以S
为 Squash 调用它:
S <-- for-pull-request (HEAD)
/
...--o--o--o...o...o--T <-- upstream/branch
\ \
A--B---M---C <-- branch, origin/branch
现在,除了this被命名为 的事实之外,for-pull-request
如果我们按照给定的说明执行操作,我们将得到相同的图表。git rebase -i upstream/branch
此外,如果我们正确地解决了任何合并冲突,commitS
具有与我们以另一种方式得到的相同的来源——但我们首先要解决的合并冲突要少得多(如果有的话)。
您现在可以将此名称(和 commit S
)推送到您的提供商(GitHub?)上的 fork,并执行合并请求。如果他们接受它,您可以删除您的分支branch
,然后创建一个branch
指向S
(或其合并的新分支upstream
,或者如果他们合并,则它们S'
基本上相同,S
但具有不同的哈希 ID 和不同的提交者:无论如何您将不得不git fetch upstream
再次获得他们的提交)。
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
(这些分支中的哪一个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]
如果我们停止绘制所有被放弃的提交,一切看起来都干净整洁,这可能就是所有这一切的重点。:-)