-
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
该操作的原理是:首先找到两个分支(即当前分支和目标变基分支)的公共祖先,然后获取当前分支上每次提交引入的差异(diff),将这些差异保存到临时文件中,接着将当前分支重置为目标变基分支的最新提交,最后依次应用之前保存的每个变更。
C4 中引入的变更变基到 C3 上此时,你可以回到 master 分支并执行一次快进(fast-forward)合并。
$ git checkout master
$ git merge experiment
master 分支的快进现在,C4' 指向的快照与 合并示例 中 C5 指向的完全一样。最终整合的结果并无区别,但变基可以使历史记录更加整洁。如果你查看变基后的分支日志,你会发现历史呈现出线性的:即使工作最初是并行发生的,看起来也像是串行完成的。
通常,这样做是为了确保你的提交能干净地应用在远程分支上——比如在一个你试图贡献代码但不是维护者的项目中。在这种情况下,你会先在分支中进行工作,然后在准备提交补丁时将工作变基到 origin/master 之上。这样,维护者就不需要进行任何整合工作——只需执行快进或干净地应用即可。
请注意,无论你是通过变基得到最后一次提交,还是通过合并得到最终的合并提交,它们最终指向的快照都是一样的——只有历史记录不同。变基是将一个工作流的变更按引入顺序重新应用到另一个工作流上,而合并则是将两个分支的末端合并在一起。
更有趣的变基
你还可以将变基的目标设为除目标分支之外的其他分支。以 基于另一个特性分支的特性分支历史 为例。你为了添加服务端功能而创建了一个特性分支(server)并进行了一次提交。然后,你基于该分支创建了客户端变更分支(client)并进行了几次提交。最后,你回到 server 分支又做了一些提交。
假设你决定将客户端的变更合并到主线中发布,但希望暂时保留服务端变更直到进一步测试。你可以提取 client 分支中不在 server 分支上的变更(C8 和 C9),并利用 git rebase 的 --onto 选项将它们重新应用到 master 分支上:
$ git rebase --onto master server client
这基本上是在说:“取出 client 分支,找出它脱离 server 分支后的补丁,然后在 master 分支上重放这些补丁,就好像它直接基于 master 分支一样。”虽然稍微复杂一点,但结果非常棒。
现在你可以快进你的 master 分支(参见 快进 master 分支以包含 client 分支的变更)
$ git checkout master
$ git merge client
master 分支以包含 client 分支的变更假设你决定也要拉入 server 分支。你可以通过运行 git rebase <基分支> <特性分支> 将 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 重写这些提交并再次推送,那么你的协作者将不得不重新合并他们的工作,当你尝试再次将他们的工作拉取回你的仓库时,事情会变得非常混乱。
让我们看一个将公开的工作进行变基导致问题的示例。假设你从中心服务器克隆了一个仓库,然后基于此进行了一些工作。你的提交历史如下:
现在,其他人做了更多工作,包含了一次合并,并推送到中心服务器。你获取(fetch)了这些内容并合并了新的远程分支,你的历史记录看起来是这样的:
接下来,那个推送了合并工作的人决定回头去变基他们的工作;他们执行了 git push --force 来覆盖服务器上的历史。然后你从服务器获取数据,下载了这些新的提交。
现在你们都陷入了困境。如果你执行 git pull,你将创建一个包含两条历史路径的合并提交,你的仓库看起来会是这样:
如果此时你运行 git log,会看到两个作者、日期和信息都相同的提交,这会让人感到困惑。此外,如果你将此历史记录推回服务器,你将把所有那些变基后的提交重新引入到中心服务器,这会进一步困惑他人。可以肯定的是,另一位开发者并不希望 C4 和 C6 出现在历史中;这就是他们当初进行变基的原因。
变基时的变基
如果你确实发现自己处于这种情况,Git 有一些神奇的方法可以帮助你。如果团队中有人强制推送(force push)了覆盖你所基于工作的变更,你的挑战在于找出哪些是你的工作,哪些是他们重写的内容。
事实证明,除了提交的 SHA-1 校验和外,Git 还计算了一个仅基于提交引入补丁的校验和。这被称为 “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,以便在问题发生后能稍稍减轻一点痛苦。
变基与合并的比较
现在你已经见识了变基和合并的实际操作,你可能在想哪一个更好。在我们回答这个问题之前,让我们退后一步,谈谈历史的意义。
一种观点认为,仓库的提交历史是实际发生过的事情的记录。 它是一份历史文档,本身就有价值,不应被篡改。从这个角度看,更改提交历史几乎是亵渎神明的;你在关于发生过的事情上撒谎。乱糟糟的合并提交序列又怎样?那正是事情发生的过程,仓库应该为后人保留这一点。
另一种对立的观点是,提交历史是项目是如何制作的故事。 你不会出版一本书的初稿,所以为什么要展示你凌乱的工作过程呢?当你进行项目开发时,你可能需要记录所有的失误和死胡同,但当向世界展示你的工作时,你可能希望讲述一个从 A 到 B 的更连贯的故事。持这种观点的人使用 rebase 和 filter-branch 等工具在合并到主分支之前重写提交。他们利用这些工具,以最适合未来读者的方式来讲述故事。
现在,关于合并还是变基哪个更好的问题:希望你已经明白,这并不简单。Git 是一个强大的工具,允许你对历史进行各种操作,但每个团队和每个项目都是不同的。既然你已经了解了两者是如何工作的,如何选择最适合你特定情况的方法,就取决于你自己了。
你可以两全其美:在推送之前通过变基清理你的本地变更,但永远不要变基任何已经推送到某处的提交。