章节 ▾ 第二版

3.6 Git 分支 - 变基

变基

在 Git 中,有两种主要的方法能将一个分支的更改集成到另一个分支中:mergerebase。在本节中,你将学习什么是变基、如何执行变基、为什么它是一个非常棒的工具以及在哪些情况下你不应该使用它。

基本的变基

如果你回到基本合并中的一个早期示例,你可以看到你将工作分叉并创建了两个不同分支上的提交。

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 分支。你可以将 server 分支变基到 master 分支上,而无需先检出它,通过运行 git rebase — 这会为你检出主题分支(在本例中为 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”。

如果你拉取了被重写的工作,并将其变基到你伙伴的新提交之上,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 是一个强大的工具,允许你对历史进行许多操作,但每个团队和每个项目都不同。现在你已经了解了这两种方法的工作原理,由你来决定哪种最适合你的特定情况。

你可以两全其美:在推送到清理工作之前变基本地更改,但绝不应对已推送的任何内容进行变基。