官方参考指南:
Pro Git Book v2, § rebasing. English
Pro Git Book v2, § rebase:衍合. 中文版
(建议还是看一下英文原版,就当熟练英语。)
一、回顾merger
常用的整合多个分支的命令就是:git merger <branch>。
假设现如下:
当在branch:experiment执行>>>> git merge master后,会把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照C5(并提交)。
log记录如下:
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master) (这是分支master的log,未merger前)
$ git log
commit 0b9a92f26977d89edcb6293ed65cf2ae38da89de
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:57:49
C3
commit 501f4477210614af18b75cd9a1a24ce0c23e72d1
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:56:55
C2
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (experiment)(这是分支experiment的log,未merger前)
$ git log
commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:00:10
C4
commit 501f4477210614af18b75cd9a1a24ce0c23e72d1
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:56:55
C2
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (experiment)
$ git merge master
Merge made by the 'recursive' strategy.
c3.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 c3.txt
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (experiment) (执行merger后)
$ git log
commit efabd4644acbe2a5993f30d6965416f4334d15d4
Merge: 0df249c 0b9a92f
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:01:12
Merge branch 'master' into experiment
commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:00:10
C4
commit 0b9a92f26977d89edcb6293ed65cf2ae38da89de
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:57:49
C3
commit 501f4477210614af18b75cd9a1a24ce0c23e72d1
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:56:55
C2
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
明显可以看到有次merge的commit。
二、rebase实现上面例子
(上面是在experiment中执行git merge master。而下面相当于是在master执行git merge experiment)
rebase的执行过程是:
提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。类似将某一分支上的所有修改都移至另一分支上,就好像在C3中“重新执行”一次在C4中的所有操作。
最后生成的快照C4'结果与merge的C5是一样的。
回到 master 分支,进行一次快进合并。
$ git checkout master
$ git merge experiment
从图例可以看出,rebase是呈线型的。而merge则不是。所以,rebase在某些时候能让commit log更加清晰明了。
摘自官方指南原文: (merge与rebase/变基 的区别)
此时,
C4'
指向的快照就和上面使用merge
命令的例子中C5
指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到
origin/master
上,然后再向主项目提交修改。这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
log记录如下,会发现在分支experiment的C4会在master的log,正如上面说的"把C4的操作在当前分支中重新执行一遍":
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git rebase experiment
First, rewinding head to replay your work on top of it...
Applying: C3
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log
commit c6bebe97d0570e16339c53c3bc8958930b3cb587
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:57:49
C3
commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:00:10
C4
commit 501f4477210614af18b75cd9a1a24ce0c23e72d1
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 22:56:55
C2
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
三、 利用rebase合并多余的commit log
核心命令:git rebase -i <other>
(更好的可以参考官方指南的: 更有趣的变基例子/More Interesting Rebases)
官方指南给出的例子用的是:
$ git rebase --onto master server client
以上命令的意思是:“取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重放一遍”。
或者
$ git rebase master server
将 server 分支中的修改整合到master中。
使用 git rebase [basebranch] [topicbranch] 命令可以直接将特性分支(server)rebase到目标分支( master)上。
以下是用 git rebase -i 举例:
假设有以上commit log。当master执行push后,那么origin中的log也是如图。
但是,在图中的C1、C2、C3其实可以只有一次log存在。
ex:
C1是只把功能做了50%,中午吃饭前commit到本地记录。下午接着做了25%,发现临时有事,又commit C2到了本地。后面回来完成了剩余的25%,commit C3并想push到remote。
针对这3次commit,即使对自己而言,最终也没必要有3次commit log。因为,如果以后要比对/还原代码,也是以整个功能来做操作。此时,C1C2C3就在log中显得多余。
(对别人而言更是多余,个人认为的理想情况是,一个commit最好只包含一个完整的功能。这样有助于后面做代码还原。)
所以针对以上情景,就会用到git的rebase命令(rebase:衍合,上面指南中翻译的是"变基"。但个人喜欢叫"衍合")。
远程仓库的commit log(或者说是push log):
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log origin/master
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
commit 4002b861bff4a0553267e77539a9e21659adf417
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:33:50
2017-02-28
本地的commit log::
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log
commit d4d1049144097a10939cad4d3126725a69c574e3
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:51:31
C3
commit 16cfad2a5da48fafa1b9b22e1c8f1cc6cfa90e07
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:51:16
C2
commit 90adaa5af3e22393ce3cc0fac03a252f2e306f33
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:50:59
C1
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
C1C2C3是本地的多次commit,且都没有push到remote。
如果此时git push,那么远程仓库中的log:会有本地3次commit的log信息。
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log origin/master
commit d4d1049144097a10939cad4d3126725a69c574e3
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:51:31
C3
commit 16cfad2a5da48fafa1b9b22e1c8f1cc6cfa90e07
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:51:16
C2
commit 90adaa5af3e22393ce3cc0fac03a252f2e306f33
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:50:59
C1
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
在push前,利用rebase合并3次提交记录(特别提醒:不能乱用rebase来整合commit log,后面在细讲原因。)
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git rebase -i HEAD~3
[detached HEAD 8b092c3] rebase C1C2C3.
Date: Sat Apr 15 23:59:46 2017 +0800
1 file changed, 4 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/master.
(rebase -i 提示修改,个人是把git的编辑改成了notepad++了。注意看P/R/E/S的意思)
pick 74c9765 C1
s 428ca30 C2
s 39d90f5 C3
# Rebase eaef124..39d90f5 onto eaef124 (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
(合并后指定本次的commit log信息)
# This is a combination of 3 commits.
# The first commit's message is:
rebase C1C2C3.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sat Apr 15 23:59:46 2017 +0800
#
# interactive rebase in progress; onto eaef124
# Last commands done (3 commands done):
# s 428ca30 C2
# s 39d90f5 C3
# No commands remaining.
# You are currently rebasing branch 'master' on 'eaef124'.
#
# Changes to be committed:
# modified: test.txt
#
# Untracked files:
# .idea/
# .project
# GitDemo.iml
#
(rebase -i 后可以发现已经不存在C1C2C3的记录了,随之变成了rebase合并的一条记录"rebase C1C2C2.")
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log
commit 8b092c3236c3d2dc760e301da9e144fd324b00f5
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:59:46
rebase C1C2C3.
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
(如果发现rebase有问题,也不用担心。可以通过reflog找到所有的操作,然后再用对应hash做想做的操作。
前提是,这些reflog没有被git gc回收。一般会保存30天。
)
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git reflog
8b092c3 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
8b092c3 HEAD@{1}: rebase -i (squash): rebase C1C2C3.
53778fd HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
74c9765 HEAD@{3}: rebase -i (start): checkout HEAD~3
39d90f5 HEAD@{4}: commit: C3
428ca30 HEAD@{5}: commit: C2
74c9765 HEAD@{6}: commit: C1
(push后remote的log)
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 302 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/vergilyn/GitDemo.git
eaef124..8b092c3 master -> master
Administrator@VergiLyn /d/Adobe/WorkSpace Git/gitdemo (master)
$ git log origin/master
commit 8b092c3236c3d2dc760e301da9e144fd324b00f5
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-04-15 23:59:46
rebase C1C2C3.
commit eaef12481e848225ba3aca0b0b2e55bcd06c8725
Author: VergiLyn <vergilyn@vip.qq.com>
Date: 2017-02-28 17:45:34
add problem
这样在remote中的log要更加整洁,有用。当要还原或对比时,不用对比到一个错误的版本C2。
四、rebase整合commit带来的风险
怎么避免rebase带来的风险?
简单来说,只能rebase合并自己本地的commit。如果这些commit之前已被push到remote,那么就不允许rebase。
(虽然可能最后不影响代码完整性与正确性)
如果把push后的commit进行rebase后并push。那么,你的伙伴不得不重新pull进行整合。
又可能你的伙伴A不知道为什么要rebase哪些commit,所以伙伴A又用git push --force把你rebase的commit重新push到remote。
伙伴A做了多余的整合工作,并又错误的把rebase的commit push到remote中。
于是你有抱怨伙伴A为什么要这么做,并自己可能又要重新rebase这些commit后push。伙伴A又要重新整合,只是已知你rebase的目的。
摘自官方指南原文:
Ahh, but the bliss of rebasing isn’t without its drawbacks, which can be summed up in a single line:
Do not rebase commits that exist outside your repository.
不要对在你的仓库外有副本的分支执行变基。
如果你遵循这条金科玉律,就不会出差错。否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用
git rebase
命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
让我们来看一个在公开的仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史如图所示:
Figure 44. 克隆一个仓库,然后在它的基础上进行了一些开发
然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:
Figure 45. 抓取别人的提交,合并到自己的开发分支
接下来,这个人又决定把合并操作回滚,改用变基;继而又用
git push --force
命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现多出来一些新的提交。
Figure 46. 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交
结果就是你们两人的处境都十分尴尬。如果你执行
git pull
命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:
Figure 47. 你将相同的内容又合并了一次,生成了一个新的提交
此时如果你执行
git log
命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。很明显对方并不想在提交历史中看到C4
和C6
,因为之前就是他把这两个提交通过变基丢弃的。
五、rebase vs. merge (全部摘自官方指南)
变基 vs. 合并
至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。
有一种观点认为,仓库的提交历史即是 记录实际发生过什么。它是针对历史的文档,本身就有价值,不能乱改。从这个角度看来,改变提交历史是一种亵渎,你使用_谎言_掩盖了实际发生过的事情。如果由合并产生的提交历史是一团糟怎么办?既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。
现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。
总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。