好用但被误解的rebase
rebase 真的太好用,它能提供一个清晰的代码提交记录。
rebase有两种使用方式:
- 搭配pull(fetch)的使用
- 独立使用
我们先介绍第一种。
# 一、merge和rebase的区别
以场景为例:
- 我和同事共同开发一个功能 login
- 所以当前的分支叫 feature-login,注意不是master
- 我们从master创建新分支的时候,起点commitId 是 A
- 我的同事也从A开始做了1次提交 B,然后push了
- 我做了1次提交 C 然后,我push之前需要先pull一下
这时候我有两个可选操作:
git pull
git pull --rebase
第一种上一篇已经讲过,实际上执行的是 fetch + merge:
git fetch origin login && git merge origin/login
而第二种——基于rebase的pull,执行的就是 fetch+rebase
git fetch origin login && git rebase origin/login
# 1.看效果
- 初始提交是 1301325
- 同事的提交是 1b28714
- 我的提交是 c46a2f9
在merge的图中,可以看到git新创建了一个提交,commitId 是 952de2 ,而我和同事的提交记录都还在,可以看到明显的分叉。
在rebase的图中,可看到所有的提交合并成了一条线,就像svn的提交历史一样。但我的commitId 变了,从 c46a2f9 变成了 ffec35c。
rebase的效果相当于:我在同事的基础上做了提交。
# 2.理解rebase
rebase 可以理解为 reset base,就是重置起始提交的意思。
那上面的场景来说:
- 同事从A提交到B 并 push 了
- 我从A 提交到C
此时
- 远程仓库提交记录:A -> B
- 我本地仓库的提交记录:A -> C
如果merge,那么会新生成一个D,从A到D有两条线,ABD和ACD:
# git pull 后
A -> B -> D
\ /
C
如果用 rebase,那么只会有一条线 A -> B - C1
。
# git pull --rebase 后
A -> B -> C1
这里我用了 C1
,因为它不是原本那个C。 原本的C是基于A做的改动,而C2是基于B做的改动。是git帮我们修改了改动。
这就是变基,是rebase,是reset base
谁执行的rebase,就改变谁的base:我执行的rebase,所以我的base就改变了,我的base变成了远程分支的提交。
# 3.多个提交下的rebase
刚才两个人都只有一个提交。假如两个人都有多个提交呢?
- 同事从A做了B C D E……N。等多个提交
- 我从A做了 P Q……Z 等多个提交
我做了这个时候rebase,我的提交会变成 A B C……N P1 Q1……Z1。
我所有的历史提交的commitId都会改变,但是每个提交变动的内容一般情况是不变的(除非有冲突)。
假如多个人操作呢?
总会有个先后,每次都是拿上一个rebase的结果当新的base。
上面强调了下不是master分支,因为只有在两个人同时开发一个分支才推荐用rebase。原因下面讲。
# 二、rebase的冲突
# 1.直接放弃和中途放弃
跟 merge一样,rebase 也提供了 abort git rebase --abort
中途放弃同样也是先提交一版本,再reset
# 2.解决冲突
前两步和merge的冲突一样:看明白冲突然后编辑。
但是第3步不一样,在进行 git add
操作之后,rebase 的结束不是 git commit
,而是 git rebase --continue
.
# 3.遇到解决不完的冲突
有时候明明已经解决冲突了,但是git还是提示有冲突,但是这个时候我们很自信,就是想强制提交!那么git提供了一个让步的方案:git rebase --skip
# 三、约定
git pull --rebase
操作后,我们可以很清晰和看出每次提交都是做了什么改动。
但是有几个约定:
- 只适用于开发分支,禁止在master和release分支操作
- 只能rebase当前分支的远程分支,禁止rebase其他分支
第1个是为了有一个清晰的合并。
第2个是为了规避如下场景:
- 假如我们的开发分支已经push
- 然后我们为了和master及时同步,rebase了master
这时候我们之前在开发分支提交的记录就都变了,这时候就push不上去了,这时候用pull问题也很大,用 pull --rebase会有冲突。
把master换成其他分支,也可能有同样的问题。
# 1.为什么应该用rebase
当merge的分支过多的时候,我们可以明显看到各种分支交织错乱的场景,不仅惨不忍睹,追踪起来也费时费力。哪怕是git的神器 bisect,在纷繁复杂的提交树前依然捉襟见肘。
这个时候rebase的优势就体现出来了:多个人在同一分支开发时,用它能保证一条清晰整洁的提交记录。哪怕最后merge到master或release也会整洁不少。
# 2.被误解的rebase
rebase很好用,但网上还有些声音不推荐用。
我看大多数原因是会造成一些问题,或者会改变/丢失一些提交。
其实从我们生产环境中用了这么多年,只要按约定做,从未造成过问题,反而使得提交记录变得简单清晰。
如果说真的要慎用rebase,那应该是慎用另一种使用方式:单独使用rebase。
# 四、单独使用 rebase
rebase 有两种使用方式,一种是配合 pull 的。另一种独立使用,是用来修正历史提交的。
我们使用的是 git pull --rebase
相当于 (git fetch origin login) + (git rebase origin/login)
假如我们把rebase的操作对象从 origin/login 改变成我们的历史提交呢?
这就是第二种应用场景:修正提交历史。
这个操作一般是不会做的,因为它是专门用来修改中间某些提交的:比如误提交大文件,比如修改历史提交的备注/作者。
# 1.误提交大文件
- 我们从A开始陆续做了 B C D E的提交
- 我们发现中间 B 提交了一个大文件
- 哪怕后续我们删除这个大文件在做一次提交,它依旧会存在仓库里永久保存
这个时候就可以选择使用rebase:从A开始rebase,并且只能进行交互式rebase.
git rebase -i 1301325
然后就进入到了一个vim界面,供我们编辑后续rebase的脚本。可以选择:
- pick 保留提交
- edit 保留提交,但是要做修改,然后在这个提交上做补充提交
- reword 保留提交,但是修改备注
- squash 保留提交,但是合并到前一个提交
- drop 丢弃提交
- ……还有很多
给个示例界面:
git的强大从这里就可见一斑!
这时候要格外小心。如果删除了一行,这个提交就会被丢掉了。当然只要不保存(执行:w)就没事
# 2.如何删除
如果要删除某个提交,我们应该选drop(看下面第一行)
drop c46a2f9 add-new-ling
pick ffec35c add-second-line
pick 558d292 2to3
pick c0a3154 add-4
pick 792d066 xxxxx
但是,但是,但是。
如果我们删除了这个提交,而后续基于这个提交的改动有冲突了,那么后面所有关于这块的改动,都要一个个解决冲突。哪怕中间引起冲突的内容,被我们恢复到了一个不足以引起冲突的状态,对于git来说依然是冲突,需要一个个解决。
这个时候已经没办法继续了,只好先战术性撤退:执行 git rebase --abort
# 3.改用edit
所以我们需要改用 edit(看下面第一行)
edit c46a2f9 add-new-ling
pick ffec35c add-second-line
pick 558d292 2to3
pick c0a3154 add-4
pick 792d066 xxxxx
然后把我们要删除的文件删掉,继续rebase
rm big-file.log
git add .
git rebase --continue
如果后面没有冲突,那就这样解决了。
# 五、什么场景不建议用rebase
按git的原则是只要是push到远程了,就不该再修改了。
也就是说仅在本地的时候是可以随意修改的。
# 1.已经push到远程的
当前的分支如果已经push到了远程,那我们在合并其他分支的时候,尽量不要用rebase。
场景如下:
- 假如有两个分支都是从 A开始创建的
- 第一个分支dev1,做了两次提交B,C后push了。现在提交记录是:
A -> B -> C
- 第二个分支dev2,也做了两次提交 X,Y后push了。现在提交记录是:
A -> X ->Y
假如dev1 要 rebase dev2,那么dev1的分支会变成:A -> X -> Y -> B1 -> C1
,这个时候再push就会有冲突。这时候用merge就不会有这个问题。
# 2.往master分支合并
master分支应该保留一个 feature 分支的完整起始,如果使用rebase:
- 容易对其他分支造成冲突
- 会无法保留feature分支的完整起始
# 3.例外情况
其次如果这个分支参与者都可控,哪怕push到远程也可以随便执行rebase,毕竟可控就说明可以push -f。