git merge
vs. git rebase
本人原来一直都是个人单机使用git,使用的操作基本只限于git add
和git commit
,对git的其他操作都是一知半解。直到现在进入公司接触团队开发,需要了解必要的分支相关的知识,其中最让我费解的就是git merge
和git rebase
了,于是花了半天时间好好了解了一下相关知识,写下本文记录一下。
首先,本文不考虑发生冲突的情况,只是描述了两种操作的原理以及区别。
git merge
操作
git merge
的操作分为以下两种情况:
fast-forward模式
假设你现在的分支情况如图所示:
如今你已经完成了基于master
分支的问题分支hotfix
,此时你需要将hotfix
分支进行合并,操作如下:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
git merge
操作首先会找到两个分支的公共分支C2
,判断公共分支C2
到两分支的路线是否有分叉(即两分支的提交历史是否是线性的),如果没有分叉,则默认使用fast-forward模式,git合并两者时只会简单的将master
指针从C2
移动到hotfix
指针相同的位置C4
。
三方合并
当然,在正常生产环境中出现公共分支有分叉的情况往往更容易见到。接着上文的情形,合并并删除hotfix
分支后,你回到iss53
的工作中完成了后续工作。
此时你需要将iss53
合并到master
分支,这里同样通过git merge
进行合并操作:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
此时两个分支的公共分支C2
到两分支的路线有分叉,所以git会使用两分支所指快照C4
和C5
以及两分支的公共祖先C2
做一个简单的三方合并。
它将创建一个提交记录,其父提交记录有两个,此时的提交记录已经变成拓扑结构了。此时可以删除iss53
分支,拓扑结构仍旧保留。
(注:对于前面的fast-forward的情况,如果要在此情况下执行三方合并,需要输入git merge --no-ff
指令)
git rebase
操作
理解rebase的字面意思对于了解该指令操作起到很好的作用。rebase的意思是“变基”,理解为“基底的改变”,琢磨这个单次的含义,继续下面的情形。
假设此时有两个分支,我们想将master
分支合并到experiment
(不要搞反对象,更改的分支为experiment
),此时我们进行git rebase
操作:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
首先给出git rebase
的结果
下面来说原理,总结为:
- 找两分支的公共祖先
C2
,为当前基底 - 当前分支
C4
对于公共祖先C2
的修改保存Actions(C2<-C4)
- 当前分支的基底从
C2
移动到合并的分支位置,即目标基底C3
- 将
Actions(C2<-C4)
依次应用于此时的基底
中间有一个很明显的基底切换的操作,这是rebase
的重点。
这里拿出git merge
合并experiment
(更改的分支为master
)的操作结果图与上面的git rebase
结果进行一个比较:
根据分析,可以得到一些结论:
git merge
在三方合并的时候会产生一个新的提交,指向两个父提交,版本树呈拓扑结构;git rebase
则会多一部分基底(零个或多个提交),不会出现指向多个父提交的情况,版本树呈线性结构git merge
操作不会修改基底,git rebase
操作会修改基底,修改基底的操作会导致历史信息产生改变(例如上文,rebase
前是C0<-C1<-C2<-C4,rebase
后是C0<-C1<-C2<-C3<-C4’,相当于修改了历史)
使用场景
两种合并方式的差异没有绝对的优劣,在不同的场景下,使用不同的合并方法可能会事半功倍。
在master分支上一定不要使用git rebase
指令
因为rebase
指令会修改历史,当你通过rebase
操作将master
分支的历史修改,那么master
分支与所有其他的开发者的分支的历史信息都不一样了(因为其他开发者的分支的基底都和你的分支基底不相同),要解决这个问题非常麻烦。
推荐在功能分支上使用git rebase
指令
通过git rebase
指令,可以将master分支的最新版本作为基底,功能分支的版本更改都在最后面,这样利于功能分支版本操作(比如版本回退)。
本地拉取远端对应同一条分支,推荐使用git rebase
指令
本地分支和远端分支本质上是不同的两个分支,所以在git pull
操作中,其实包含git fetch
和git merge
两步操作,通过rebase
操作,可以将本地分支的最新修改的版本都保存在本地分支的版本树的最后面,有利于版本回退等操作。