git 作为代码管理工具,我们通常使用 merge(普通合并)和 rebase(衍合或变基)来合并代码,它们的区别在哪呢?
快进合并
什么是快进合并?如下,合并 dev 分支到 master 分支时,master 分支在 dev 分支的起点 B 后并没有做任何更改,dev 分支包含所有 master 分支上的历史,因此只要把 master 分支的位置移动到 dev 的最新提交上,分支就合并了,这叫做快进(fast-forward)合并。
dev
|
C1━━━>D1━━━>E1
/
A━━━>B
|
master
//快进合并
dev
|
A━━━>B━━━C1━━━>D1━━━>E1
|
master
基本的合并操作
如下,在 commit 为 B 时开出一个分支 dev,并开发到 E1,master 分支也到了 D2。
dev
|
C1━━━>D1━━━>E1
/
A━━━>B━━━>C2━━━>D2
|
master
将分支 dev merge 到 master:
//在master分支上
$ git merge dev
可以看到,merge 会产生一个多余的提交历史 F:
dev
|
C1━━━>D1━━━>E1
/ \
A━━━>B━━━>C2━━━>D2━━F
|
master
通常,我们在开发分支,想要与 master 的更新保持一致,就可以在开发分支 rebase master。
//在dev分支上
$ git rebase master
dev
|
C2━━━>D2━━━C1'━━━>D1'━━━>E1'
/
A━━━>B━━━>C2━━━>D2
|
master
可以看到,dev 分支上原有的 C1、D1、E1 被删除后重新提交为 C1'、D1'、E1'了,它的原理是找到两个分支最近的祖先提交(B),然后根据当前分支(dev,要衍合的分支)后续的提交生成一些列补丁文件(C1'、D1'、E1'),然后以基底分支(master 分支)最后一个提交为基准点(D2),逐个应用这些补丁文件,生成新的提交,从而改变了提交历史,使其成为 master 分支的直接下游。 现在就可以回到 master 分支进行快进合并了,最终的提交历史如下所示,比较干净整洁。
dev
|
A━━━B━━━C2━━━>D2━━━C1'━━━>D1'━━━>E1'
|
master
rebase 存在的问题
考虑如下的情景,我的项目里有两个分支,一个主分支 master,一个开发分支 dev。
dev
|
A━━━B
|
master
现在小明在 dev 分支上开发,并把新的提交 C1、C2 推送到了远程服务器(origin/dev)。
dev
|
C1━━━>C2
/
A━━━B
|
master
小红在 dev 分支小明的提交基础上高高兴兴地开发,并新提交了 C3、C4。
dev
|
C1━━━>C2━━━>C3━━━>C4
/
A━━━B
|
master
现在小明想同步 master 上的最新内容(小东提交到 master 上的 C),在 dev 上进行了 rebase,之前的提交 C1、C2 被删除掉变成了 C1'和 C2'了,小明继续开发到了 D1。
dev
|
C1’━━━>C2‘━━━>D1
/
A━━━B━━━C
|
master
小明接着把当前的修改推送到远程服务器,结果报错:
! [rejected] dev -> dev (non-fast-forward)
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
表示不能进行快进合并,小明推送前服务器端的 dev 分支如下:
dev
|
A━━━>B━━━>C1━━━>C2
显然小明本地的分支因为 rebase,原有的提交 C1、C2 没有了,也就说小明本地的 dev 分支并没有包含服务器端 dev 分支的全部提交历史,也就不能进行提交。只能先 pull 服务器端的代码并解冲突。
remotes/origin/dev
|
C1━━━━━━━━━>C2
/ \
A━━━B 🤣🤣🤣🤣 \
\ \
C━>C1’━>C2‘━>D1━>F
|
dev
提交线图就变成了下面这样:
关于 rebase,有一条金科玉律,就是不要在公共分支进行 rebase。 如果在已经被推送到公共仓库的提交上执行变基命令,并因此丢弃了一些别人的开发所基于的提交,那你就有麻烦了,面对同事的疯狂吐槽吧!!!