-
1. 起步
-
2. Git 基础
-
3. Git 分支
-
4. 服务器上的 Git
- 4.1 协议
- 4.2 在服务器上部署 Git
- 4.3 生成 SSH 公钥
- 4.4 架设服务器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管服务
- 4.10 小结
-
5. 分布式 Git
-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: Git 命令
3.6 Git 分支 - 变基
变基
在 Git 中,将一个分支的更改整合到另一个分支有两种主要方式:merge
(合并)和 rebase
(变基)。本节将介绍什么是变基、如何执行变基、为什么它是一个相当棒的工具,以及在哪些情况下你不希望使用它。
基本变基
回顾基本合并中的一个早期例子,你可以看到你的工作产生了分歧,并在两个不同的分支上进行了提交。

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

然而,还有另一种方式:你可以获取在 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
此操作的原理是:首先找到两个分支(当前分支和要变基到的分支)的共同祖先,然后获取当前分支的每个提交所引入的差异,将这些差异保存到临时文件中,接着将当前分支重置到与目标变基分支相同的提交,最后依次应用每个更改。

C4
中引入的更改变基到 C3
之上此时,你可以回到 master
分支并进行一次快进合并。
$ git checkout master
$ git merge experiment

master
分支现在,C4'
指向的快照与合并示例中 C5
指向的快照完全相同。整合的最终结果没有区别,但变基使得历史记录更清晰。如果你检查一个变基后的分支日志,它看起来像一条线性历史:所有工作似乎都是按顺序发生的,即使它们最初是并行进行的。
通常,你这样做是为了确保你的提交能够干净地应用到远程分支上——也许是你在尝试贡献但并非维护的项目。在这种情况下,你会在一个分支中完成工作,然后在准备好将补丁提交给主项目时,将你的工作变基到 origin/master
。这样,维护者就不必做任何整合工作——只需进行一次快进或干净的应用。
请注意,无论是变基操作后最后一次变基提交,还是合并操作后最终的合并提交,它们指向的最终快照是相同的——不同的只是历史记录。变基按照更改引入的顺序,将一条工作线的更改在另一条工作线上重放,而合并则将终点合并在一起。
更有趣的变基
你也可以将变基重放到除了目标变基分支以外的其他地方。例如,一个像从另一个主题分支分出的主题分支的历史。你分出一个主题分支 (server
) 来为你的项目添加一些服务端功能,并进行了提交。然后,你从这个分支再分出一个分支来做客户端更改 (client
),并提交了几次。最后,你回到了你的 server
分支并又提交了几次。

假设你决定将客户端更改合并到主线中进行发布,但希望暂缓服务器端更改,直到它们得到进一步测试。你可以通过使用 git rebase
命令的 --onto
选项,将 client
分支上不在 server
分支上的更改(C8
和 C9
)重放到你的 master
分支上:
$ git rebase --onto master server client
这基本上是说:“取出 client
分支,找出它从 server
分支分化以来的补丁,并将这些补丁在 client
分支中重放,就好像它直接基于 master
分支一样。” 这有点复杂,但结果相当棒。

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

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

server
分支变基到你的 master
分支之上然后,你可以快进基础分支 (master
)。
$ git checkout master
$ git merge server
你可以删除 client
和 server
分支,因为所有工作都已整合,你不再需要它们,整个过程的历史记录看起来就像最终提交历史。
$ git branch -d client
$ git branch -d server

变基的危险
啊,但是变基的幸福并非没有缺点,这可以用一句话概括:
不要对你仓库外部的,或其他人可能基于其进行过工作的提交进行变基。
如果你遵循这个准则,你会没事。如果你不遵循,人们会讨厌你,你也会受到朋友和家人的鄙视。
当你变基时,你是在放弃现有的提交并创建相似但不同的新提交。如果你将提交推送到某个地方,并且其他人拉取这些提交并基于它们进行工作,然后你又用 git rebase
重写了这些提交并再次推送,你的协作者将不得不重新合并他们的工作,当你尝试将他们的工作拉回你的仓库时,事情会变得一团糟。
让我们看一个例子,说明对已公开的工作进行变基会如何导致问题。假设你从中心服务器克隆,然后在此基础上进行了一些工作。你的提交历史看起来像这样:

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

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

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

如果你的历史记录看起来像这样时,你运行 git log
,你会看到两个具有相同作者、日期和消息的提交,这会让人感到困惑。此外,如果你将这段历史推送回服务器,你将把所有那些变基过的提交重新引入到中心服务器,这会进一步混淆人们。可以很安全地假设,另一个开发者不希望 C4
和 C6
出现在历史中;这就是他们最初进行变基的原因。
遇到被变基的提交,如何变基
如果你**确实**发现自己处于这种情况,Git 还有一些更高级的技巧可以帮助你。如果你的团队成员强制推送了覆盖你已基于其进行工作的更改,你的挑战是弄清楚哪些是你的工作,哪些是他们重写的。
事实证明,除了提交的 SHA-1 校验和之外,Git 还会计算一个仅基于提交引入的补丁的校验和。这被称为“补丁 ID”(patch-id)。
如果你拉取了被重写的工作,并将其变基到你的伙伴的新提交之上,Git 通常可以成功地找出哪些是属于你独有的,并将其重新应用到新分支的顶部。
例如,在前面的场景中,如果我们在有人推送了变基后的提交,放弃了你基于其进行工作的提交时,不进行合并,而是运行 git rebase teamone/master
,Git 将会:
-
确定我们分支独有的工作(
C2
、C3
、C4
、C6
、C7
) -
确定哪些不是合并提交(
C2
、C3
、C4
) -
确定哪些尚未被重写到目标分支(只有
C2
和C3
,因为C4
与C4'
是相同的补丁) -
将这些提交应用到
teamone/master
的顶部
因此,我们最终会得到更像在强制推送的变基工作之上进行变基的结果,而不是你再次将相同的工作合并到一个新的合并提交中的结果。

这仅在你的伙伴创建的 C4
和 C4'
几乎完全是相同的补丁时才有效。否则,变基将无法判断它是重复的,并将添加另一个类似于 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的故事。持这种观点的人会使用像 rebase
和 filter-branch
这样的工具在提交合并到主线分支之前重写它们。他们使用像 rebase
和 filter-branch
这样的工具,以最适合未来读者的方式讲述故事。
现在,回到合并和变基哪个更好的问题:希望你现在能明白,这并非那么简单。Git 是一个强大的工具,允许你对历史记录进行许多操作,但每个团队和每个项目都不同。既然你已经了解了这两种方式的工作原理,就由你来决定哪种最适合你的具体情况。
你可以两全其美:在推送到远程之前对本地更改进行变基以清理你的工作,但永远不要变基任何你已经推送到远端的提交。