章节 ▾ 第二版

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

该操作的原理是:首先找到两个分支(即当前分支和目标变基分支)的公共祖先,然后获取当前分支上每次提交引入的差异(diff),将这些差异保存到临时文件中,接着将当前分支重置为目标变基分支的最新提交,最后依次应用之前保存的每个变更。

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

此时,你可以回到 master 分支并执行一次快进(fast-forward)合并。

$ 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. 基于另一个特性分支的特性分支历史

假设你决定将客户端的变更合并到主线中发布,但希望暂时保留服务端变更直到进一步测试。你可以提取 client 分支中不在 server 分支上的变更(C8C9),并利用 git rebase--onto 选项将它们重新应用到 master 分支上:

$ git rebase --onto master server client

这基本上是在说:“取出 client 分支,找出它脱离 server 分支后的补丁,然后在 master 分支上重放这些补丁,就好像它直接基于 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 <基分支> <特性分支>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)了这些内容并合并了新的远程分支,你的历史记录看起来是这样的:

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 有一些神奇的方法可以帮助你。如果团队中有人强制推送(force push)了覆盖你所基于工作的变更,你的挑战在于找出哪些是你的工作,哪些是他们重写的内容。

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

如果你拉取了被重写的工作,并将其变基到合作伙伴的新提交之上,Git 往往能够成功识别出哪些是你独有的提交,并将它们应用到新分支之上。

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

  • 确定哪些是本分支独有的工作(C2, C3, C4, C6, C7

  • 确定哪些不是合并提交(C2, C3, C4

  • 确定哪些尚未被重写进目标分支(只有 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,以便在问题发生后能稍稍减轻一点痛苦。

变基与合并的比较

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

一种观点认为,仓库的提交历史是实际发生过的事情的记录。 它是一份历史文档,本身就有价值,不应被篡改。从这个角度看,更改提交历史几乎是亵渎神明的;你在关于发生过的事情上撒谎。乱糟糟的合并提交序列又怎样?那正是事情发生的过程,仓库应该为后人保留这一点。

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

现在,关于合并还是变基哪个更好的问题:希望你已经明白,这并不简单。Git 是一个强大的工具,允许你对历史进行各种操作,但每个团队和每个项目都是不同的。既然你已经了解了两者是如何工作的,如何选择最适合你特定情况的方法,就取决于你自己了。

你可以两全其美:在推送之前通过变基清理你的本地变更,但永远不要变基任何已经推送到某处的提交。