好用但被误解的rebase

9/14/2022

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效果

在merge的图中,可以看到git新创建了一个提交,commitId 是 952de2 ,而我和同事的提交记录都还在,可以看到明显的分叉。

rebase效果

在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 操作后,我们可以很清晰和看出每次提交都是做了什么改动

但是有几个约定:

  1. 只适用于开发分支,禁止在master和release分支操作
  2. 只能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:

  1. 容易对其他分支造成冲突
  2. 会无法保留feature分支的完整起始

# 3.例外情况

其次如果这个分支参与者都可控,哪怕push到远程也可以随便执行rebase,毕竟可控就说明可以push -f。