Git 的核心功能之一就是可以编辑历史。与其它将历史视为铁律的版本控制系统不同,在 git 中我们可以根据需要更改历史记录。它为我们提供了许多强大的工具,允许我们像使用重构来维护良好的软件设计一样策划良好的提交历史。这些工具对新手甚至中级用户来说可能有点难度,但指南将帮助解开 git-rebase 的神秘面纱。
一句警告:通常不建议改变公共、共享或稳定分支的历史。编辑功能分支和个人复刻的分支是一个不错的选择,编辑你尚未推送的分支肯定是没问题的。在编辑提交后,使用 git push -f 强制提交,会将更改推送到个人复刻或功能分支。
尽管有这样严肃的警告,但值得一提的是,本指南中提到的都是非破坏性操作。在 git 中永久丢失数据实际上非常困难。指南末尾就介绍了在出错时如何修复问题。
本指南由 sourcehut (骇客联盟)提供。100% 开源且由 Mercurial 托管,持续集成,有邮件列表,且没有 JavaScript!现在就试试吧!
我们不想搞砸你原有的仓库,因此本指南中我们将全程使用沙箱仓库。从运行下面的命令开始:
如果遇到麻烦,只需要运行 rm -rf /tmp/rebase-sandbox 并在此运行上面的步骤即可重新开始。本指南的每个步骤都可以在新的沙箱中运行,因此没有必要重新执行每项任务。
让我们从简单的修改开始:修复你最近的一次提交。我们在沙箱中添加一个文件(并且做了一个错误操作):
修复这个错误很容易。我们可以编辑文件并使用 --amend 提交,如下所示:
使用 -a 指令(即 git add)会自动暂存所有 git 已知的文件,而 --amend 会将更改压缩到最近的提交中。保存并退出编辑器(如果你愿意,也可以现在修改提交信息)。你可以通过运行 git show 查看这次修复的提交:
上面的修改仅适用最近的提交。如果你需要更正旧的提交又会怎样么?我们首先对沙箱做出相应的设置:
看起来 greeting.txt 缺少了 "world" 单词。我们来写一个正常的提交来修复这点:
所以现在文件看起来是正确的,但我们的历史还可以优化一下(使用一个新的提交来 "fixup" 最后一次提交)。为此,我们需要引入一个新的工具:交互式 rebase。我们将使用这种方式修改最后三个提交,运行 git rebase -i HEAD~3(-i 用与交互)。这会打开文本编辑器,如下所示:
这个是 rebase 执行计划,通过编辑此文件,你可以指示 git 如何编辑历史记录。我已经将摘要信息修改为只与 rebase 指南中这一部分相关的摘要信息,但是你可以随意阅览文本编辑器中的完整摘要。
当我们保存并关闭编辑器时,git 将从历史记录中删除这里全部的提交记录,然后依次执行每一行。默认情况下,它会找到每一次提交,从堆中将其取出并添加到分支中。如果我们根本没有编辑这个文件,最终会回到开始的地方,按原有选择每个提交。现在来使用一个我最喜欢的功能:fixup。编辑第三行,将 "pick" 修改为 "fixup",并立即将其移动到我们想要 "fix up" 为位置:
提示:我们同样可以将其简写为 "f",来加快速度。
保存并退出编辑器(git 会运行这些命令)。我们可以检查日志来验证结果:
你在工作时,可能会发现在你达成一个小的功能节点或修复先前提交中的错误时,编写了大量有用的提交。但是,将这些提交压缩("squash")在一起会更好,可以使你的工作在合并到 master 之前历史更清晰。为此,我们将使用 "squash" 操作。我们先写一堆提交(如果你想快一点,直接复制粘贴即可):
这里会创建一个文件,说 "Hello world",做了很多次提交。我们开启另一个交互式的 rebase 将它们合并到一起。注意,我们首先要检出一个分支然后再尝试此操作。正是这样,由于我们检出了一个新分支,所以允许使用 git rebase -i master来快速修改所有自分支检出之后的提交。结果:
提示:你的本地 master 分支独立于远程 master 分支之外,且 git 将远程分支存储为 origin/master。结合这个技巧,使用 git rebase -i origin/master 来修改尚未合并到上游的所有提交是一种非常方便的方法。
我们要将这些更改全部压缩到第一次提交中。要做到这一点,需要将除第一行之外的每个 "pick" 操作修改为 "squash",如下所示:
当你保存并关闭编辑器时,git 会处理一小会儿,然后再次打开编辑器来修改最终的提交消息。你会看到这样:
默认这就是所有被压缩后的提交消息的组合,但是保留这样的消息并不是你想要的。不过,旧的提交消息在编写新提交时可能有参考意义。
提示:你在上一节中了解到的 "fixup" 命令也可以用在这里(但是它会丢弃压缩提交的消息)。
现在我们删除所有内容并用更好的提交消息替换,如下所示:
保存并退出编辑器,然后检查你的 git 日志,成功了!
在继续之前,我们将作出的更改拉入 master 分支并清除这个新分支的痕迹。我们可以像使用 git merge 一样使用 git rebase,但是这避免了合并提交操作:
除非是我们合并两个不相关的历史记录,否则通常还是希望避免使用 git merge。如果你有两个不同的分支,git merge 对于记录它们于何时被合并是很有用的。在正常工作过程中,使用 rebase 通常更合适。
有时会遇到相反的问题(一次提交太多了)。我们来试着把它分开。这一次,编写一点实际的代码。从一个简单的 C 程序开始(你仍然可以复制粘贴到 shell 来快速完成):
我们先做第一次提交。
之后,对程序做一些扩展:
提交之后,我们就为学习如何拆分提交做好准备了:
第一步是启动一个交互式 rebase。我们使用 git rebase -i HEAD~2 来 rebase 两次提交,下面给出这个 rebase 计划:
将第二个提交命令由 "pick" 改为 "edit",然后保存并关闭编辑器。Git 会花费一会时间处理,然后显示这个:
我们可以按照这些说明向提交中添加新的更改,不过这里让我们使用 git reset HEAD^ 来做一个 "soft reset"。如果你在这之后运行了 git status,你会看到git将最后一次提交的内容退还到编辑后还没有stage的状态,并将其加到工作树中:
要拆分这个提交,我们需要做一个交互式 commit。这样我们就可以有选择地只修改工作树中的特定更改。运行 git commit -p 来启动此过程,你将看到一下提示:
Git 只给了一个 "大块头"(即单个的变更)来展示提交。不过这太多了,所以我们来使用 "s" 命令把这个大块头 "split" 成小的部分。
提示:如果你对其他选项比较好奇,按 "?" 来了解它们的描述。
这个大块头看起来也不错,单一且变化自成一体。输入 "y" 回复这个问题(并暂存这个 "大块头"),然后 "q" 退出交互式会话并继续提交。你的编辑器突然提示并要求你输出一个合适的提交信息。
保存并关闭你的编辑器,然后我们将进行第二次提交。我们可以打开另一个交互式 commit,但由于我们只想在这个提交中包含其余的更改,只需要这么做:
最后一个命令告诉 git,我们已经完成了对此提交的编辑,并继续下一个 rebase 命令。就这样!运行 git log 命令来看看你的劳动成果:
这个很容易了!我们从设置沙箱开始:
现在 git 日志应该是这样的:
显然,这里排序错乱了。我们要针对过去的 3 个提交进行交互式 rebase,来解决这个问题。运行 git rebase -i HEAD~3 会出现这个 rebase 计划:
这个修复过程很简单:只需要按照你希望的提交顺序重新排序这些行。看起来应该是这样:
保存并关闭你的编辑器,git 会完成剩下的工作。注意,当你在实际工作中执行这个操作时,最终可能会发生冲突,点击这里查看冲突的帮助。
如果你一直在已经更新的上游分支创建提交,通常 git pull 会创建一个和并提交。这方便,git pull 的行为默认相当于:
还有另一个选择,往往更有用且使历史保持简洁:git pull —rebase。与合并方式不同,这相当于:
合并的方式更简单且更容易理解,但是如果你会使用 git rebase 的话,这种 rebase 方式则更容易达成你的目的。如果你愿意,可以将将其设置为某些默认行为:
当你执行此操作时,技术上来说,你正在使用我们在下一节将讨论的内容。所以我们来解释一下这么做的意义是什么。
讽刺的是,我使用最少的 git rebase 功能就是其中一个名为分支变基功能。比如你有一下分支:
事实证明,feature-2 并不依赖于 feature-1 中的任何变更,而是依赖于 E 提交,因此你可以将其 rebase 到 master。解决方式为:
非交互式 rebase 对所有相关提交("pick")都会执行默认操作,它会在 feature-2 重复这些在 master 顶部而非 feature-1 中的提交。你的提交历史现在看起来像这样:
解决合并冲突的细节超出了本指南的范围(留意之后针对此功能的指南)。假设你熟悉解决一般冲突,下面是使用 rebase 的细节。
有时,在执行 rebase 时会遇到合并冲突,你可以像处理其他冲突一样处理 rebase。Git 会在受影响的文件中设置冲突标记,git status 可以显示这些待解决的问题,并且你可以将解决冲突后的文件使用 git add 或 git rm 标记。然而,在 git rebase 的背景下,有两个选择你要注意。
第一个就是如何彻底解决冲突。不同于 git commit,你将要解决的是由 git merge 造成的冲突,重新 rebase 的适用命令是 git rebase --continue。然而,这里还有其他的可用的选择:git rebase --skip。它会跳过你正在处理的提交,并将其从 rebase 中移除。在交互式 rebase 中,当 git 没有意识到提交是从其他分支来出来的,且我们当前分支已有一个更新的版本,这是很常见的。
毫无疑问,rebase 操作有时可能会很困难。如果你犯一个错误并因此丢失了一个你需要的提交,而这里有一个git reflog 命令可以保存当天的历史。运行这个命令你会看到你所有变更操作的 ref,或者是分支、标签的引用。每一行都显示了引用的指向,并且一旦你认为提交丢失了,就可以使用 git cherry-pick,git checkout,git show或其他操作来处理。
英文原文:https://git-rebase.io/
译者:敦伟