章节 ▾ 第二版

3.6 Git 分支 - 变基

变基

在 Git 中,将一个分支的更改整合到另一个分支有两种主要方式:merge(合并)和 rebase(变基)。本节将介绍什么是变基、如何执行变基、为什么它是一个相当棒的工具,以及在哪些情况下你不希望使用它。

基本变基

回顾基本合并中的一个早期例子,你可以看到你的工作产生了分歧,并在两个不同的分支上进行了提交。

Simple divergent history
图 35. 简单的分叉历史

正如我们已经介绍过的,整合分支最简单的方法是使用 merge 命令。它会在两个最新的分支快照(C3C4)以及两者最近的共同祖先(C2)之间执行三方合并,从而创建一个新的快照(和提交)。

Merging to integrate diverged work history
图 36. 合并以整合分叉的工作历史

然而,还有另一种方式:你可以获取在 C4 中引入的更改补丁,并将其重新应用到 C3 之上。在 Git 中,这被称为 变基。使用 rebase 命令,你可以将一个分支上提交的所有更改在另一个分支上重放。

对于这个例子,你需要检出 experiment 分支,然后将其变基到 master 分支,如下所示:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

此操作的原理是:首先找到两个分支(当前分支和要变基到的分支)的共同祖先,然后获取当前分支的每个提交所引入的差异,将这些差异保存到临时文件中,接着将当前分支重置到与目标变基分支相同的提交,最后依次应用每个更改。

Rebasing the change introduced in `C4` onto `C3`
图 37. 将 C4 中引入的更改变基到 C3 之上

此时,你可以回到 master 分支并进行一次快进合并。

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
图 38. 快进 master 分支

现在,C4' 指向的快照与合并示例C5 指向的快照完全相同。整合的最终结果没有区别,但变基使得历史记录更清晰。如果你检查一个变基后的分支日志,它看起来像一条线性历史:所有工作似乎都是按顺序发生的,即使它们最初是并行进行的。

通常,你这样做是为了确保你的提交能够干净地应用到远程分支上——也许是你在尝试贡献但并非维护的项目。在这种情况下,你会在一个分支中完成工作,然后在准备好将补丁提交给主项目时,将你的工作变基到 origin/master。这样,维护者就不必做任何整合工作——只需进行一次快进或干净的应用。

请注意,无论是变基操作后最后一次变基提交,还是合并操作后最终的合并提交,它们指向的最终快照是相同的——不同的只是历史记录。变基按照更改引入的顺序,将一条工作线的更改在另一条工作线上重放,而合并则将终点合并在一起。

更有趣的变基

你也可以将变基重放到除了目标变基分支以外的其他地方。例如,一个像从另一个主题分支分出的主题分支的历史。你分出一个主题分支 (server) 来为你的项目添加一些服务端功能,并进行了提交。然后,你从这个分支再分出一个分支来做客户端更改 (client),并提交了几次。最后,你回到了你的 server 分支并又提交了几次。

A history with a topic branch off another topic branch
图 39. 从另一个主题分支分出的主题分支的历史

假设你决定将客户端更改合并到主线中进行发布,但希望暂缓服务器端更改,直到它们得到进一步测试。你可以通过使用 git rebase 命令的 --onto 选项,将 client 分支上不在 server 分支上的更改(C8C9)重放到你的 master 分支上:

$ git rebase --onto master server client

这基本上是说:“取出 client 分支,找出它从 server 分支分化以来的补丁,并将这些补丁在 client 分支中重放,就好像它直接基于 master 分支一样。” 这有点复杂,但结果相当棒。

Rebasing a topic branch off another topic branch
图 40. 变基一个从另一个主题分支分出的主题分支

现在你可以快进你的 master 分支了(参见快进你的 master 分支以包含 client 分支的更改):

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
图 41. 快进你的 master 分支以包含 client 分支的更改

假设你决定也将 server 分支拉入。你可以通过运行 git rebase <basebranch> <topicbranch>,将 server 分支变基到 master 分支上,而无需先检出它——这会为你检出主题分支(在本例中是 server),并将其重放到基础分支(master)上:

$ git rebase master server

这会将你的 server 工作重放到你的 master 工作之上,如将你的 server 分支变基到你的 master 分支之上所示。

Rebasing your `server` branch on top of your `master` branch
图 42. 将你的 server 分支变基到你的 master 分支之上

然后,你可以快进基础分支 (master)。

$ git checkout master
$ git merge server

你可以删除 clientserver 分支,因为所有工作都已整合,你不再需要它们,整个过程的历史记录看起来就像最终提交历史

$ git branch -d client
$ git branch -d server
Final commit history
图 43. 最终提交历史

变基的危险

啊,但是变基的幸福并非没有缺点,这可以用一句话概括:

不要对你仓库外部的,或其他人可能基于其进行过工作的提交进行变基。

如果你遵循这个准则,你会没事。如果你不遵循,人们会讨厌你,你也会受到朋友和家人的鄙视。

当你变基时,你是在放弃现有的提交并创建相似但不同的新提交。如果你将提交推送到某个地方,并且其他人拉取这些提交并基于它们进行工作,然后你又用 git rebase 重写了这些提交并再次推送,你的协作者将不得不重新合并他们的工作,当你尝试将他们的工作拉回你的仓库时,事情会变得一团糟。

让我们看一个例子,说明对已公开的工作进行变基会如何导致问题。假设你从中心服务器克隆,然后在此基础上进行了一些工作。你的提交历史看起来像这样:

Clone a repository, and base some work on it
图 44. 克隆一个仓库,并在此基础上做一些工作

现在,其他人做了更多工作,其中包括一次合并,并将这些工作推送到中心服务器。你获取它并将新的远程分支合并到你的工作中,使你的历史看起来像这样:

Fetch more commits, and merge them into your work
图 45. 获取更多提交,并将它们合并到你的工作中

接下来,推送了合并工作的人决定回去变基他们的工作;他们执行 git push --force 来覆盖服务器上的历史。然后你从该服务器获取,拉下新的提交。

Someone pushes rebased commits, abandoning commits you’ve based your work on
图 46. 有人推送了变基后的提交,放弃了你基于其进行工作的提交

现在你们都陷入了困境。如果你执行 git pull,你将创建一个包含两条历史线的合并提交,你的仓库将看起来像这样:

You merge in the same work again into a new merge commit
图 47. 你再次将相同的工作合并到一个新的合并提交中

如果你的历史记录看起来像这样时,你运行 git log,你会看到两个具有相同作者、日期和消息的提交,这会让人感到困惑。此外,如果你将这段历史推送回服务器,你将把所有那些变基过的提交重新引入到中心服务器,这会进一步混淆人们。可以很安全地假设,另一个开发者不希望 C4C6 出现在历史中;这就是他们最初进行变基的原因。

遇到被变基的提交,如何变基

如果你**确实**发现自己处于这种情况,Git 还有一些更高级的技巧可以帮助你。如果你的团队成员强制推送了覆盖你已基于其进行工作的更改,你的挑战是弄清楚哪些是你的工作,哪些是他们重写的。

事实证明,除了提交的 SHA-1 校验和之外,Git 还会计算一个仅基于提交引入的补丁的校验和。这被称为“补丁 ID”(patch-id)。

如果你拉取了被重写的工作,并将其变基到你的伙伴的新提交之上,Git 通常可以成功地找出哪些是属于你独有的,并将其重新应用到新分支的顶部。

例如,在前面的场景中,如果我们在有人推送了变基后的提交,放弃了你基于其进行工作的提交时,不进行合并,而是运行 git rebase teamone/master,Git 将会:

  • 确定我们分支独有的工作(C2C3C4C6C7

  • 确定哪些不是合并提交(C2C3C4

  • 确定哪些尚未被重写到目标分支(只有 C2C3,因为 C4C4' 是相同的补丁)

  • 将这些提交应用到 teamone/master 的顶部

Rebase on top of force-pushed rebase work
图 48. 在强制推送的变基工作之上进行变基

这仅在你的伙伴创建的 C4C4' 几乎完全是相同的补丁时才有效。否则,变基将无法判断它是重复的,并将添加另一个类似于 C4 的补丁(这很可能无法干净地应用,因为更改至少已经部分存在)。

你也可以通过运行 git pull --rebase 而不是普通的 git pull 来简化此操作。或者,在这种情况下,你可以手动执行 git fetch,然后执行 git rebase teamone/master

如果你正在使用 git pull 并希望将 --rebase 设置为默认行为,你可以通过 git config --global pull.rebase true 等命令设置 pull.rebase 配置值。

如果你只对从未离开过你本地电脑的提交进行变基,那就没问题。如果你变基了已经推送但其他人尚未基于其进行提交的提交,你也会没事。如果你变基了已经公开推送的提交,并且其他人可能已经基于这些提交进行了工作,那么你可能会遇到一些令人沮丧的麻烦,并受到队友的鄙视。

如果某个时候你或你的伙伴确实觉得有必要这样做,请确保每个人都知道运行 git pull --rebase,以尽量减轻事后带来的痛苦。

变基 vs. 合并

现在你已经看到了变基和合并的实际操作,你可能想知道哪一个更好。在我们回答这个问题之前,让我们退一步,谈谈历史的意义。

其中一种观点是,你的仓库的提交历史是**实际发生过的记录**。它是一份有其自身价值的历史文献,不应该被篡改。从这个角度来看,改变提交历史几乎是亵渎神明的行为;你是在撒谎关于实际发生的事情。所以,即使有一系列混乱的合并提交又如何?事情就是这样发生的,仓库应该为后代保留它。

相反的观点是,提交历史是**你的项目如何创建的故事**。你不会出版一本书的初稿,那为什么还要展示你的凌乱工作呢?当你在一个项目上工作时,你可能需要记录你所有的失误和死胡同,但当你需要向世界展示你的工作时,你可能希望讲述一个更连贯的从A到B的故事。持这种观点的人会使用像 rebasefilter-branch 这样的工具在提交合并到主线分支之前重写它们。他们使用像 rebasefilter-branch 这样的工具,以最适合未来读者的方式讲述故事。

现在,回到合并和变基哪个更好的问题:希望你现在能明白,这并非那么简单。Git 是一个强大的工具,允许你对历史记录进行许多操作,但每个团队和每个项目都不同。既然你已经了解了这两种方式的工作原理,就由你来决定哪种最适合你的具体情况。

你可以两全其美:在推送到远程之前对本地更改进行变基以清理你的工作,但永远不要变基任何你已经推送到远端的提交。

scroll-to-top