运行git merge --squash
是另一种特殊情况。 像实际合并一样,它使用正常的合并机制来计算涉及两个分支提示的真实合并基础。
要合并的两个提交当然是像往常一样的HEAD
(--ours
)和您在命令行上指定的提交。 由于合并基础是真实合并基础,它使用现有的名词合并(合并提交)来最小化新的合并冲突,因此您可以得到相对无冲突的合并结果。
然而,最终的提交与cherry-pick的想法有关:最终
我们需要补充以下事实:
你的分支最初是某个其他存储库的克隆。因此,它最开始具有与“上游”相同的提交(相同的历史记录)。自那时以来,你和他们已经有了一些不同,但同时你通过从自己的本地存储库中推送提交来更新你的分支。
当你执行git fetch upstream
时,你会从他们的分支中获取他们的提交,并将它们放入你自己的本地存储库中。这些提交的跟踪名称为upstream/master
等。
当你执行git fetch origin
(如果你需要),你会从你的分支中获取提交,并将它们放入你自己的本地存储库中。这些提交的跟踪名称为origin/master
等。
你的存储库有自己的分支(当然)。
这意味着你的本地存储库——也就是你自己的电脑上的存储库——具有“他们的分支”、“你的分支”和“你的提交”的完整联合。你可以随时将自己的提交推回自己的分支。
你可以很容易地使用你的分支创建“拉取请求”,因为你的提供者(例如GitHub)会为你记住“你的分支”和“他们的分支”之间的链接。
因此,让我们画出你在存储库中拥有的提交图表,或其简化版本,如下所示:
我分叉了一个存储库,做了一些更改。随着时间的推移,我也使用了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 到 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
时,它特别有趣,因为它是branch
和upstream/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
完成使用合并机制合并提交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
现在,除了这个命名为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/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]
如果我们停止绘制所有被弃用的提交,一切看起来都干净整洁,这可能就是所有这些工作的目的。 :-)
rerere
可能会有帮助。请查看https://git-scm.com/blog/2010/03/08/rerere.html。 - ElpieKay