前言

在使用 Git 进行多人协作的过程中,导师曾提起过要学会使用 git rebase 进行代码的推送,因此我也是查找了一些信息,并在此进行简单整理,将 git rebase 的优点及使用场景等信息进行介绍。

merge 和 rebase 的区别

通常我们开发时,会从某个分支拉取一个新分支,如图中所示,在 C2 时期,从 master 分支中拉取一个新分支 experiment,后续 master 分支和 experiment 分支各自也都有了新的提交:

新建分支

git merge

当我们觉得 experiment 分支的开发结束了,需要合入到 master 分支进行发布,通常会选择切到 master 分支,执行 git merge experiment,将 experiment 分支 merge 到 master 分支之中,合并后的结果如下:

使用 merge

合并时会生成 C5 节点,C5 节点是 C3 和 C4 的内容合并,并且一般来讲它的 commit 信息都是 Merge branch 'experiment' into 'master' 这种,甚至 master 和 experiment 各自分支上都有很多次提交节点,那么最终合并出来的节点里,会糅合很多很多不同开发者的提交。

对于最终的 master 来讲,前面每个提交节点 commit 信息、提交人很明确、提交内容都很明确,但是做了一次 merge 操作,生成的 C5 节点又把前面一堆节点信息包含了一遍,后续如果对 master 分支做记录回溯或者回滚时,麻烦就大了。

git rebase

其实,还有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做变基。 你可以使用 git rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。具体什么是 rebase,可以通过下图来理解,在 experiment 分支执行 git rebase master 后,C4 的修改将会变基到 C3 上:

使用 rebase

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。

rebase 后,现在 experiment 和 master 的所有提交节点又成了一个简单的链式结构,在 master 分支里执行 git merge experiment,此时就是简单的快进合并了:

1
2
$ git checkout master
$ git merge experiment

rebase 后 merge

还有一种情况,从 experiment 分支又拉取了一个 other 分支,master、experiment、other 三个分支分别进行不同的提交,othe 分支想早于 experiment 分支合入 master,可以使用 git rebase --onto master experiment other 命令,直接将 other 分支变基到 master,之后将 other 合并到 master 里时,也是快进合并,单链表结构。

孰优孰劣

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用_谎言_掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

变基的风险

使用 rebase 命令必须遵守一条准则:不要对在你的仓库外有副本的分支执行变基

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

常用场景

拉取远端更新

  • git pull = git fetch + git merge,拉取到远端分支的更新,merge 到本地分支里,如果本地和远端分别有不同的提交节点,那么 merge 时会生成一个新的合并节点:

拉取远端更新 merge

  • git pull --rebase = git fetch + git rebase,拉取到远端分支的更新,采用 rebase 模式,将自己本地分支的提交节点变基到远端分支最新的节点上,保持链式结构。

拉取远端更新 rebase

可以将 git pull 配置成默认 rebase 模式,就不需要每次加 --rebase 了。

日常开发

模式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 当前是 develop 分支
$ git pull --rebase # 将本地 develop 分支更新成最新
$ git checkout -b develop_feature # 基于 develop 分支拉取一个 develop_feature
# 当前是 develop_feature 分支

# -----此处经过一系列 commit,注意:是 commit,只有 commit 才会生成 git 的提交节点,没 commit 的东西,git 不认识------
$ git checkout develop
$ git pull --rebase # 更新 develop,拉过来一堆别人在 develop 分支上的提交
$ git checkout develop_feature
$ git rebase develop # 将当前 develop_feature 分支自己的那堆提交变基到 develop 分支最新节点上(通俗地理解就是时不时地从母分支更新东西过来)
# git rebase 时跟 git merge 一样,都有可能会产生冲突,将冲突解决以后,执行 git rebase --continue 即可完成 rebase,git rebase --abort 表示放弃本次 rebase

# -----上述步骤可以重复执行很多次,时不时执行一下就行-----
# 需要合并到母分支里了,最终再执行一次上述rebase操作
$ git checkout develop
$ git merge develop_feature
# 在 develop 分支上是一条完美的链式结构, 最早的是很古老的那些节点,其次是别人在 develop 上提交的那些节点,最后是你自己分支里的这些提交。

模式二:不拉取新分支,就在 develop 分支开发,往 develop 分支提交。这个很简单,就是时不时以 rebase 模式更新一下代码,经常保持自己的提交代码指向该分支远端的最新节点:

1
2
3
4
5
6
7
8
9
10
11
$ git checkout develop
$ git pull --rebase
$ git commit1
$ git commit2
...
$ git commitn
$ git pull --rebase
$ git commitx
$ git commity
$ git pull --rebase
$ git push

如果基于 develop 切出来一个新分支 develop_feature,并推送到了远端生成一个 origin/develop_feature 分支,想用merge request 进行合入,也是时不时在本地执行一下模式一里那个重复的更新步骤,在本地变基到 develop 分支的最新提交节点上,并 push 到远端 origin/develop_feature 分支上,那么远端的 origin/develop_feature 分支一直是跟 origin/develop 在一条链上,审核人点击同意合入时,也是快进合并,不会生成新的 merge 节点

其他

  1. git rebase 还可以进行多次提交的合一,比如多次提交其实就是对于一个问题的修复,不想之后留下多次修改记录,那么可以 git rebase -i 进行 commit 的合并操作。

  2. git stash 可以将本地的修改进行暂存,在不想 commit 的情况下进行 merge、rebase、checkout 等操作时,可以先暂存一下,之后再 pop 出来就行。

  3. git commit --amend:当一个 commit 还在本地没 push 上去时,这个命令可以把刚修改的东西追加到最后一次 commit 里,不会生成新的 commit 节点。

参考文章

Git - Pro Git 中文版(第二版)