根据我们的需要改变历史记录是Git的一项核心功能,这项功能为我们很多强大的工具,同时允许我们能够使用重构来实践软件设计原则。这些工具对新手甚至中级git用户来说可能有点吓人,但本文将为你揭开git-rebase
的神秘面纱。
需要注意的是:通常不建议修改公共的、共享的或者稳定的分支。建议编辑个人分支与特性分支以及尚未推送的提交。git push -f在编辑提交后强制将更改推送到个人分支或功能分支到远程分支。
构建沙箱
为了不影响到你任何现有存储库,本文将使用沙箱仓库。从运行这些命令以开始:
如果遇到任何问题,运行rm -rf /tmp/rebase-sandbox
删除现有的沙箱仓库并按照上述步骤重新构建沙箱仓库即可,后续每个章节的内容都是独立的,因此在构建沙箱仓库后,按照指定章节内容继续操作即可,不需要重新执行所有章节的操作。
修改上次提交
让我们从以下步骤开始:修复你最近一次的提交。
首先,添加一个文件到仓库中,并犯下一个错误(world单词拼错):
修复这个错误很简单,只需要重新编辑文件,并在提交时带上--amend
参数:
指定-a
参数自动重新追踪所有git已经提交的文件(同:git add's
),--amend
参数将修改压缩到最近的提交中。保存并退出编辑器(如果需要,可以修改对应的提交信息)。之后可以通过运行git show
查看修复的提交:
|
|
修复旧的提交
上面的操作只适用于修改最近一次的提交,如果需要修改更久远的提交历史该怎么办呢?首先按照如下步骤设置仓库:
|
|
看起来greeting.txt中缺少了单词world,让我们通过正常提交修复:
现在文件看起来是对的了,但我们的历史提交显得有些啰嗦,现在让我们通过新提交来修复上一个提交,为了实现这个功能,需要引入新的工具:交互式rebase。我们将以这种方式编辑最后三个提交,因此我们将运行git rebase -i HEAD~3(-i用于交互式)。这会打开你的文本编辑器,如下所示:
这是rebase计划,通过编辑此文件,可以指示git如何编辑历史记录。我已将摘要修改为与rebase指南的这一部分相关的详细信息,但您可以随意浏览文本编辑器中的完整摘要。
当我们保存并关闭我们的编辑器时,git将从其历史记录中删除所有这些提交,然后一次执行每一行。默认情况下,它将选择每个提交,从堆中召唤它并将其添加到分支。如果我们根本不编辑这个文件,我们最终会回到我们开始的地方——按原样选择每个提交。我们现在将使用我最喜欢的功能之一:fixup。编辑第三行以将操作从“pick”更改为“fixup”,并将该行内容移动到我们想要“修复”提交的下一行:
提示:我们也可以将其缩写为“f”,以便下次加快速度。
保存并退出编辑器 - git将运行这些命令。我们可以检查日志以验证结果:
将几个提交压缩成一个
在你工作时,你可能会发现在你达到小型里程碑或修复先前提交中的错误时编写大量提交很有用。但是在到达里程碑、修复完成后,将这些提交“压缩”在一起可能会更有用,它可以在你的工作合并到主分支之前为你提供更清晰的历史记录。为此,我们将使用“squash”操作。让我们从写一堆提交开始:
这会创建很多提交,接下来我们将使用rebase将这些提交合并。请注意,我们首先检出一个分支以尝试此操作。因为通过检出新分支我们可以使用git rebase -i master
编辑master分支的所有历史提交,而不再需要使用git rebase -i $commit
:
|
|
提示:你的本地主分支独立于远程主分支,git将远程分支存储为 origin/master。结合这个技巧,git rebase -i origin/master通常是一种非常方便的方法来修改尚未合并到上游的所有提交!
我们要将所有提交压缩到第一次提交中。要做到这一点,将每个“pick”操作更改为“squash”,第一行除外,如下所示:
当你保存并关闭编辑器时,git会根据上述修改重新计算提交信息,然后再次打开编辑器以供你修改最终的提交消息。你会看到:
git默认将所有的提交信息组合,但是保留这样的消息不太可能是你想要的。但是,旧的提交消息能够为编写新提交信息提供参考。
提示:您在上一节中了解到的“fixup”命令也可以用于此目的 - 但它会丢弃压缩提交的消息。
让我们删除所有内容并用更好的提交消息替换它,如下所示:
保存并退出编辑器,然后检查你的git日志 - 成功!
在继续下一步操作之前,让我们将我们的更改并入主分支并删除该分支。我们可以像使用git merge一样使用git rebase,但相比于git merge,git rebase避免了合并提交:
除非我们实际上合并不相关的历史,否则我们通常会避免使用git merge。如果你有两个不同的分支,git merge对于记录它们何时被合并是很有用的。在正常工作过程中,rebase通常更合适。
将一个提交拆分为多个
有时会发生相反的问题 - 一次提交太大了。让我们看看将它拆分开。这一次,让我们写一些实际的代码。从简单的C程序开始:
接着提交:
之后,再尝试扩展程序:
再次提交:
接下来我们将尝试拆分git提交:
第一步是启动交互式rebase。让我们通过执行git rebase -i HEAD~2
rebase两次提交:
将第二个提交的命令从“pick”更改为“edit”,保存并关闭编辑器。Git会计算一秒,然后展示:
我们可以按照上述说明为提交添加新的更改,但这里我们通过运行git reset HEAD^
来执行“软重置” 。如果在此之后运行git status
,您将看到它取消最新提交并将其更改添加到工作树:
为了解决这个问题,我们将进行交互式提交。这允许我们有选择地仅提交工作树中的特定更改。运行git commit -p以启动此过程,您将看到以下提示:
|
|
Git为您提供了一个“hunk”(即单个更改)来考虑提交。但是这个太大了 - 让我们使用“s”命令将大块“拆分”成更小的提交。
提示:如果你对其他选项感到好奇,请按“?” 查看相关说明。
Tip: If you’re curious about the other options, press “?” to summarize them.
这个hunk作为一个单独的,自足的变化,看起来更好。让我们点击“y”来回答问题(以及“hunk”的阶段),然后点击“q”以“退出”交互式会话并继续提交。您的编辑器会弹出,要求您输入合适的提交消息。
保存并关闭编辑器,然后我们将进行第二次提交。我们可以做另一个交互式提交,但由于我们只想在此提交中包含其余的更改,我们将执行此操作:
最后一个命令告诉git我们已完成编辑此提交,并继续执行下一个rebase命令。运行git log
看看你的劳动成果:
对提交重新排序
这部分比较简单。让我们从以下操作开始:
|
|
git日志现在看起来是这样的:
显然,顺序是不对的。让我们对过去3次提交进行交互式变更以解决此问题。运行git rebase -i HEAD~3
:
修复现在很简单:只需按照希望提交的顺序重新排列这些行:
保存并关闭您的编辑器,git将为您完成剩下的工作。
git pull –rebase
如果您在已经上游更新的分支上编写了一些提交,通常git pull会创建一个合并提交。在这方面,git pull默认情况下的行为相当于:
还有另一种选择,且它通常更有用,能够带来更清晰的历史:git pull –rebase。与合并方法不同,这相当于以下内容:
merge方法更简单,更容易理解,但如果您了解如何使用git rebase,则rebase方法几乎总是您想要做的。如果您愿意,可以将其设置为默认行为,如下所示:
当你这样做时,从技术上讲,你正在应用我们在下一节讨论的程序……所以让我们解释一下故意这样做的意义。
使用git rebase来… rebase
假设您有以下分支:
可以看出:feature-2不依赖于feature-1中的任何更改,即在提交E上,因此您可以将其基于master。因此修复:
非交互式rebase对所有涉及的提交(“pick”)执行默认操作,它只是重放feature-2中不在master基础上的feature-1中的提交。您的历史现在看起来像这样:
|
|
解决冲突
有关解决合并冲突的详细信息超出了本文的范围 - 请留意未来的其他指南。假设您熟悉一般解决冲突,以下是适用于变基的细节。
有时你在做一个rebase时会遇到合并冲突,你可以像任何其他合并冲突一样处理它。Git将在受影响的文件中设置冲突标记,git status
向您显示需要解决的内容,并且可以使用git add
或git rm
标记已解析的文件。但是,在git rebase的上下文中
,您应该注意两个选项。
第一个是如何完成冲突解决,使用git rebase --continue
。但是,还有另一种选择:git rebase --skip
。这将跳过您正在处理的提交,并且它不会包含在rebase中。这在执行非交互式rebase时最常见,当git没有意识到它从“其他”分支中提取的提交是它在“我的”分支上与之冲突的提交的更新版本。
救命!我打破了它!
毫无疑问 - 有时候变基很难。如果你犯了一个错误并且这样做了你需要的遗失,那么git reflog
可以挽救你。运行此命令将显示更改ref或引用的每个操作- 即分支和标记。每行显示旧参考的指向,你可以使用git cherry-pick
,git checkout
,git show
,或使用任何其他操作。
原文链接:git rebase in depth