你说:
在变基之前,我想压缩我的提交(11 次提交)
(强调我的)。但是很快,你运行了一个将在变基期间或之后git rebase
进行挤压的,这将是一个问题。我们稍后再谈。
所以我做了这样的事情......
说“我做了类似<填空>之类的事情”总是很可疑,因为那时我们不知道你实际做了什么,这使得很难猜测到底发生了什么。:-) 幸运的是,这些都很有意义:
# On master
git pull
这在技术上还不是必需的,但让我们介绍一下它的作用。该git pull
命令只是一个方便的快捷方式,git fetch
后跟第二个 Git 命令。第二个 Git 命令默认为git merge
,但您可以将其配置为git rebase
. 现在让我们假设第二个命令是正确的git merge
和/或它对我们没有特别重要的作用,这可能是正确的。
(这git fetch
部分总是安全的,没有伤害任何东西。)
git checkout my_branch
# on my_branch
git fetch
git rebase -i origin/master
除非上游变化很快,否则这第二个git fetch
——第一个在git pull
——是不必要的,但与往常一样,额外的获取是无害的,可能是一个好习惯。问题在于git rebase -i origin/master
,它开始变基。然后,您编辑了要执行的指令,squash
但所有挤压都将在变基期间发生,即,在变基到新的提示提交之前,您不会被挤压origin/master
。
鉴于即将发生冲突,它将发生在您正在复制的 11 次提交期间的某个地方。它似乎发生在第一个:
当我保存并退出时出现错误 -error: could not apply e7ce468... 'commit1 message'
这个早期合并冲突的好处是它比后来的合并冲突更容易解释,尽管此时您可以对它做的事情与其他任何地方一样。
该错误告诉您 Git 无法完成第一次提交的副本。
您可以停止正在进行的变基,将所有内容恢复到开始之前的状态,使用:
git rebase --abort
请继续阅读以决定是否要这样做并尝试不同的方法。
Rebase 通过复制提交来工作
使用 Git 时,您需要牢记“提交图”,即使只是作为一种背景、阴暗、不祥的隐现事物 :-) 大多数时候。实际上,提交图虽然可能令人生畏,但它是有帮助的。它通常非常大并且“隐约可见”。(也许可以把它想象成一只非常大但友好的狗。)
提交图是我们可以绘制的,如果我们这样做,它会看起来像这样:
...--o--o--*--o--o--o--o--L <-- origin/master
|
|
\
A--B--C--D--E--F--G--H--I--J--K <-- my_branch
这些单字母名称中的每一个都是实际提交 ID 的缩写(那些丑陋的 40 字符散列之一,如7c56b20857837de401f79db236651a1bd886fbbb
)。圆形o
节点代表更多的提交,我不需要说什么,所以它们很无聊。这*
是一个我不知道其 ID 的提交,也没有名称,但它非常有趣,所以我给了它一颗星。
(较早的提交在左侧,分支名称指向每个分支的提示提交。最后一点是 Git 的实际工作方式:分支名称指向提示提交,每个提交向后指向其较早的父提交。内部的向后箭头有点让人分心和烦人,而且很难用 ASCII 画出来,所以我只是把它们画成线。)
请注意,A
通过K
是 11 次提交,并且L
是提示提交到哪些origin/master
点(在您的git fetch
es 使您的 Git 更新到 Git 存储库后origin
)。我只是随机猜测要吸引多少普通无聊o
的提交,但这并不重要。
当你运行时git rebase origin/master
(有或没有-i
),Git 会尝试复制你的所有 11 个提交,副本在“提交之后” L
。也就是说,它希望将图形更改为如下所示:
...--o--o--*--o--o--o--o--L <-- origin/master
| \
| A'-B'-C'-...-J'-K' <-- my_branch
\
A--B--C--D--E--F--G--H--I--J--K [abandoned]
(当您将其设为交互式变基并使用squash
时,Git 会简单地修改复制过程,使其首先A'
照常制作,然后制作B'
出A' + B
并将其链接到L
,然后制作C'
出B' + C
,依此类推。这里的关键是 Git仍在一步一步地做所有事情。)
这里的问题是每一步都可能导致合并冲突。目前尚不清楚每一步是否会——我们知道第一个会,但我们对其余的还不太了解。
如果只有一种方法可以压缩所有 11 个提交,同时将它们附加到我标记的提交上*
。也就是说,不是一次复制每个提交,而是将它们添加到 中L
,如果我们可以让 Git 只创建一个AK
代表 总和的新提交A+B+C+...+J+K
呢?我们可能会画出这样的漂亮结果:
...--o--o--*--o--o--o--o--L <-- origin/master
|\
| AK <-- my_branch
\
A--B--C--D--E--F--G--H--I--J--K [abandoned]
现在我们可以做一个简单的变基AK
(它仍然会与 冲突L
,但我们只需要解决一次问题)。
变基:选择你的目标
好吧,事实证明这真的很容易。我们需要git rebase -i
像以前一样运行,但告诉它:不要变基到L
,变基到*
。也就是说,将链从另一条链上的位置复制到它现在所在的位置。
我们已经基于*
,但是如果我们“重新定位”到现在的位置,我们可以轻松地压缩所有内容:不会有冲突,并且压缩会正常工作。我们只需要以*
某种方式命名 commit。
你怎么能找到 commit *
?一种方法是使用git log
,它会显示所有提交 ID。剪切并粘贴您关心的提交的 ID:
$ git rebase -i <id-of-commit-*>
和 squash 像以前一样,Git 会做你想做的事:在变基之前 squash。(嗯,不完全是“之前”,而是在大变基之前作为一个更小的,squash-y rebase。)
另一种查找提交 ID 的方法*
是使用git merge-base
:
$ git merge-base my-branch origin/master
这会打印出 ID,准备好进行剪切和粘贴(或者您可以使用 shell 语法将 ID 直接插入命令中,或设置变量,或其他)。
也有捷径
随意忽略这个快捷方式,但你可以:
$ git checkout my_branch
$ git reset --soft <id-of-commit-*>
$ git commit
这样做是放弃A--B--...--J--K
链(my_branch
指向 commit *
),但将文件保留在工作树和索引中,就像它们在 commit 中一样K
,然后使用当前索引进行新的提交。所以这会产生一个提交,其树与 的 匹配K
,但其父级是 commit *
。(你必须编写一个全新的提交消息,而压缩让你可以编辑来自原始 11 个提交的消息。)
或者,只是解决冲突
无论如何,您最终都希望将新AK
提交或原始A--B--...--J--K
链重新设置为 commit L
。这将需要解决合并冲突。
这样做的好处AK
是您只需解决一次,而不是每次冲突提交一次。缺点是每次解决一个较小的冲突时可能会更清楚该怎么做:可能A
有一个很明显的冲突,J
有一个不同但也很明显的冲突,K
还有一个相似A
但仍然很明显的冲突......但是当您一次重新组合所有组合,和AK
之间的冲突,加上 的分心,很难看出如何解决它。A
K
J
因此,首先压缩的好处是更少的可能发生冲突的提交。首先压缩的缺点是可能发生冲突的提交更少。由你决定尝试哪一个。
最后的一些想法
无论你做什么,请记住 Git 总是通过添加新提交来工作。旧的,即使它们被“遗弃”,仍然在您的存储库中。默认情况下,它们至少存在 30 天,并且它们的 ID 保存在 Git 的“reflogs”中。如果你想取回旧的提交,你可以简单地使用保存的 ID 创建一个新的分支或标签名称。
每个分支都有一个 reflogHEAD
和一个。reflogmy_branch
更容易用于从变基中恢复。