-
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 分支 - Rebase
Rebase
在 Git 中,将一个分支的更改集成到另一个分支主要有两种方法:merge(合并)和 rebase(变基)。在本节中,您将了解 rebase 是什么,如何进行 rebase,为什么它是一个非常强大的工具,以及在什么情况下不应使用它。
基本的 Rebase
如果您回顾一下 基本合并 中的一个早期示例,您会看到您分叉了工作并在两个不同的分支上进行了提交。
正如我们之前讨论过的,集成分支的最简单方法是使用 merge 命令。它在两个最新的分支快照(C3 和 C4)以及它们最近的共同祖先(C2)之间执行三方合并,创建一个新的快照(以及提交)。
然而,还有另一种方法:您可以获取 C4 中引入的更改的补丁,然后将其重新应用到 C3 之上。在 Git 中,这称为 rebasing(变基)。使用 rebase 命令,您可以获取在一个分支上提交的所有更改,并将它们重新应用到另一个分支上。
对于这个例子,您将检出 experiment 分支,然后像这样将其 rebase 到 master 分支上:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
此操作的工作方式是:找到两个分支(您当前所在的分支和您正在 rebase 到的分支)的共同祖先,获取您所在分支的每个提交所引入的差异,将这些差异保存到临时文件中,将当前分支重置到与您正在 rebase 到的分支相同的提交,最后依次应用每个更改。
C4 中引入的更改 rebase 到 C3 之上此时,您可以返回到 master 分支并执行快进合并。
$ git checkout master
$ git merge experiment
master 分支现在,C4' 指向的快照与 合并示例 中 C5 指向的快照完全相同。集成的最终产品没有区别,但 rebase 使得历史更加整洁。如果您检查 rebase 后的分支的日志,它看起来像一个线性的历史:似乎所有工作都是按顺序发生的,即使它们最初是并行发生的。
通常,您会这样做以确保您的提交能够干净地应用到远程分支上——也许是您试图贡献但并未维护的项目。在这种情况下,您会在一个分支中完成工作,然后在准备提交补丁到主项目时,将您的工作 rebase 到 origin/master 之上。这样,维护者就不必进行任何集成工作——只需要一个快进或干净的应用。
请注意,您最终得到的最后一个提交所指向的快照,无论是 rebase 后的最后一个 rebase 提交,还是合并后的最终合并提交,都是相同的快照——仅仅是历史不同。Rebase 将工作线路上的更改按引入的顺序重新应用到另一条工作线路上,而合并则将两个端点合并在一起。
更有趣的 Rebase
您也可以让 rebase 应用于非 rebase 目标分支。例如,考虑一个 从另一个主题分支分出的主题分支的历史。您分出了一个主题分支(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 分支 rebase 到 master 分支上,而无需先检出它——这会为您检出主题分支(在此例中为 server),并将其 rebase 到基础分支(master)之上。
$ git rebase master server
这会将您的 server 工作重新应用到您的 master 工作之上,如 将您的 server 分支 rebase 到您的 master 分支之上 所示。
server 分支 rebase 到您的 master 分支之上然后,您可以快进基础分支(master)。
$ git checkout master
$ git merge server
您可以删除 client 和 server 分支,因为所有工作都已集成,您不再需要它们了,这样整个过程的历史看起来就像 最终提交历史。
$ git branch -d client
$ git branch -d server
Rebase 的危险
啊,但是 rebase 的美好并非没有缺点,这些缺点可以归结为一句话:
不要 rebase 那些已经存在于您的仓库之外,并且可能有人在此基础上进行了工作的提交。
如果您遵循这条准则,您将平安无事。如果您不遵循,人们会讨厌您,您会被朋友和家人唾弃。
当您 rebase 东西时,您是在放弃现有的提交并创建新的、相似但不同的提交。如果您将提交推送到某个地方,其他人将其拉下来并在此基础上进行了工作,然后您使用 git rebase 重写了这些提交并再次推送,那么您的协作者将不得不重新合并他们的工作,当您尝试将他们的工作拉回到您的项目中时,情况会变得一团糟。
让我们来看一个关于您公开的 rebase 工作可能导致问题的示例。假设您从一个中心服务器克隆,然后在上面进行了一些工作。您的提交历史看起来是这样的:
现在,其他人进行了更多工作,其中包括一次合并,并将这些工作推送到中心服务器。您将其获取下来,并将新的远程分支合并到您的工作中,使您的历史看起来像这样:
接下来,推送了合并工作的人决定回过头来 rebase 他们的工作;他们执行 git push --force 来覆盖服务器上的历史。然后您从该服务器获取,下载新的提交。
现在您们都陷入了困境。如果您执行 git pull,您将创建一个合并提交,其中包含两条历史记录,您的仓库将看起来像这样:
如果您在历史看起来是这样的情况下运行 git log,您会看到两个具有相同作者、日期和消息的提交,这将令人困惑。此外,如果您将此历史推送到服务器,您将把所有这些 rebase 后的提交重新引入中心服务器,这可能会进一步混淆人们。可以安全地假设其他开发人员不希望 C4 和 C6 出现在历史记录中;这就是他们最初进行 rebase 的原因。
Rebase 时 Rebase
如果您确实遇到了类似这样的情况,Git 有一些更神奇的功能可以帮助您。如果您的团队中的某个人强制推送了覆盖了您在其基础上进行工作的更改,您的挑战是弄清楚哪些是您的,哪些是他们重写的。
原来,除了提交的 SHA-1 校验和之外,Git 还计算了一个仅基于提交引入的补丁的校验和。这被称为“patch-id”。
如果您下载了被重写的工作,并将其 rebase 到您合作伙伴的新提交之上,Git 通常能够成功地找出哪些是您独有的,并将它们重新应用到新分支之上。
例如,在前面的场景中,如果我们不再执行合并,而是处于 某人推送了 rebase 后的提交,放弃了您在其基础上进行工作的提交 时,而是运行 git rebase teamone/master,Git 将会:
-
确定哪些工作是我们的分支独有的(
C2、C3、C4、C6、C7)。 -
确定哪些不是合并提交(
C2、C3、C4)。 -
确定哪些尚未被重写到目标分支(只有
C2和C3,因为C4与C4'是相同的补丁)。 -
将这些提交应用到
teamone/master的顶部。
因此,我们得到的不是 您将同一份工作再次合并到一个新的合并提交中 中看到的结果,而是类似 在强制推送的 rebase 工作之上进行 rebase 的情况。
这仅在您的合作伙伴所做的 C4 和 C4' 补丁几乎完全相同时才有效。否则,rebase 将无法识别它们是重复的,并会添加另一个 C4 类的补丁(这很可能无法干净地应用,因为更改至少部分已经存在)。
您也可以通过运行 git pull --rebase 而不是普通的 git pull 来简化这一点。或者,在这种情况下,您可以手动执行 git fetch,然后是 git rebase teamone/master。
如果您使用 git pull 并希望将 --rebase 设置为默认值,您可以使用类似 git config --global pull.rebase true 的命令设置 pull.rebase 配置值。
如果您只 rebase 那些从未离开过您计算机的提交,您将安然无恙。如果您 rebase 那些已经推送出去但没有人在此基础上进行过工作的提交,您也将安然无恙。如果您 rebase 那些已经公开推送的提交,并且人们可能在此基础上进行了工作,那么您可能会遇到一些令人沮丧的麻烦,并招致团队成员的鄙视。
如果您或您的合作伙伴在某个时候发现有必要这样做,请确保每个人都知道运行 git pull --rebase 以便在发生问题后尽量简化处理。
Rebase 与 Merge
现在您已经看到了 rebase 和 merge 的实际应用,您可能想知道哪个更好。在回答这个问题之前,让我们退后一步,谈谈历史的意义。
关于这一点的一种观点是,您的仓库的提交历史是实际发生的事情的记录。它是一份历史文件,本身就很有价值,不应该被篡改。从这个角度来看,改变提交历史几乎是亵渎神明的;您在撒谎关于实际发生的事情。那么,为什么会有混乱的合并提交系列呢?事情就是那样发生的,仓库应该为后代保存下来。
相反的观点是,提交历史是您的项目是如何制作的故事。您不会出版一本书的第一稿,那又何必展示您混乱的工作呢?当您从事一个项目时,您可能需要一份记录您所有失误和死胡同路径的记录,但当需要向世界展示您的工作时,您可能希望讲述一个更连贯的故事,说明如何从 A 到 B。持此观点的人使用 rebase 和 filter-branch 等工具在将提交合并到主线分支之前重写他们的提交。他们使用 rebase 和 filter-branch 等工具,以最适合未来读者的方式讲述故事。
现在,关于 merge 还是 rebase 更好这个问题:希望您能看到这并非易事。Git 是一个强大的工具,允许您对历史进行和进行许多操作,但每个团队和每个项目都是不同的。现在您知道这两者是如何工作的了,由您来决定哪一个最适合您的特定情况。
您可以获得两全其美:在推送之前 rebase 本地更改以清理您的工作,但永远不要 rebase 任何您已经推送到某个地方的内容。