Skip to content

Git Rebase 两个常用功能

个人觉得对于Git这个工具,能说自己会用必须要基本掌握git rebase这条命令。这个命令也是非常强大的,下面就讲述一下两个开发中非常常用的场景:

  • 多人开发中避免不必要的merge操作
  • 修改自己的提交历史

由于各种Git GUI工具五花八门,所讲述的功能不一定所有的GUI都支持,所以这里就以CLI(命令行接口)为例子来讲述,理解了命令行图形界面就简单很多。

避免不必要的merge

这是开发中最常遇到的问题,大概的GitFlow图形如下

图1

最后合并阶段就是很头疼的问题,dev分支上充满了无意义(个人认为)的merge提交,而且大多数人不是很在意出现冲突后的合并。有些时候你会发现你明明删除掉的代码在merge之后它又回来了(这是真实发生过的事情)

其实这个问题可以使用git rebase解决。

使用rebase拯救臃肿的dev分支

可以看出,dev分支臃肿的原因是充满了merge提交,要如何消除这些merge呢?我们分几种情况讨论:

本地当前分支已经mergedev中,且已经推送到了远端

抱歉,这种情况除了改写远端别无他法。而多人开发中这意味着如果你强制覆盖了远端,即使用了--force选项,那么其他人就无法顺利地将自己的修改提交到远端,会引发更大的问题,所以这种情况下请等待下次吧。

本地当前分支已经mergedev中,但尚未推送到远端

大概图例如下:

图2

这是最后的拯救机会,首先要移除本地的这个merge提交,但注意要使用混合模式,即git reset的默认模式,这种会将你的修改保留,但移除掉对分支树的修改,一般只需执行

shell
git reset HEAD~1

这个命令的意思是,撤销HEAD指向的提交(即本地的最新的提交),执行成功后大概图例如下:

图3

这就到了最简单的情况——

本地当前分支尚未mergedev

这里我们要做的事情就很简单了,即把上面一张图中的分支树变成下图这样

图4

这里就完美说明了rebase这个词的含义,即将3这个提交的上一级提交(即base),例子中是1,更换为2这个提交。它的命令也比较简单:

shell
git rebase dev

它代表了会把当前分支(即git status显示的分支)的内容rebasedev分支上,这一步分为两种情况,一种无需解决冲突,即如果你的提交和dev分支上没有冲突的话,那就可以很轻松的完成。另一种需要解决冲突,点击 跳转至解决冲突 一节。

无需解决冲突

shell
# rebase前
$ git log --all --oneline --graph
* 6355958 (dev) add other commit
| * 4717a58 (HEAD -> feature/new) add new feature, modify 1.txt
|/
* ab0643c first commit, create 1.txt

$ git rebase dev
Successfully rebased and updated refs/heads/feature/new.

# rebase后
$ git log --all --oneline --graph
* aa98dad (HEAD -> feature/new) add new feature, modify 1.txt
* 6355958 (dev) add other commit
* ab0643c first commit, create 1.txt

它的好处我们在上图也可以看出来,完全没有merge提交,提交历史清晰,向远端提交也没有任何压力,直接git push origin HEAD:dev即可,因为前面的内容完全一致,远端只需执行Fast-Forward合并即可。

同时也可以看出,这个rebase命令是会删除老的在feature\new分支上的SHA4717a58的提交,再在dev分支上SHA6355958这次提交后追加一个新的aa98dad的新提交的,虽然内容完全一样,但它已经是一个新提交了。

需要解决冲突

如果Git不能决定如何快进,就需要自己手动解决冲突。这和merge时遇到的冲突需要合并是一样的。

假设rebase之前代码是这样的,分为feature/new分支和dev分支

shell
# rebase前
$ git log --all --oneline --graph
* 6e03fd3 (HEAD -> feature/new, origin/feature/new) change to 2
| * fa4959a (origin/dev, origin/HEAD, dev) modify code.txt
|/
* 76e43f7 add code.txt

但使用git rebase时并不能直接完成,而是提示冲突。

shell
$ git rebase dev
Auto-merging code.txt
CONFLICT (content): Merge conflict in code.txt
error: could not apply 6e03fd3... change to 2
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config advice.mergeConflict false"
Could not apply 6e03fd3... change to 2

这时候打开code.txt,就会发现他的内容变成了

first commit

<<<<<<< HEAD
this content will be 1

// no comment
=======
this content will be 2
>>>>>>> 6e03fd3 (change to 2)

我们想调整的是把1变为2,然后保留// no comment这一行,则直接编辑变成这样,保存。所有有冲突的文件都会被处理成这样的格式,我们需要一一修改,改漏了也不用担心,git status随时可以查看哪些文件尚未解决冲突。

first commit

this content will be 2

// no comment

然后在命令行git add修改的文件。可以使用git add code.txt添加完成了手动合并操作的文件,或者使用git add .添加所有修改后的冲突文件。

然后执行git rebase --continue,会添加一个解决冲突的提交,写上你的解决提交内容,我个人是强烈建议写上rebase字样,以便以后查找修改,撰写完毕保存即可。再看一下提交日志:

shell
$ git log --oneline --all --graph
* 781e3cc (HEAD -> feature/new) change to 2
* fa4959a (origin/dev, origin/HEAD, dev) modify code.txt
| * 6e03fd3 (origin/feature/new) change to 2
|/
* 76e43f7 add code.txt

可以看出新增了781e3cc这个提交,成功地将6e03fd3重新变换了 base 提交。

接下来同样地,将当前的分支(feature/new或者说HEAD)提交到远端dev即可。

shell
$ git push origin HEAD:dev

# 再次查看提交历史
$ git log --oneline --all --graph
* 781e3cc (HEAD -> feature/new, origin/dev, origin/HEAD) change to 2
* fa4959a (dev) modify code.txt
| * 6e03fd3 (origin/feature/new) change to 2
|/
* 76e43f7 add code.txt

至于这个多余的origin/feature/new在确认合并没有问题之后可以删除。

shell
$ git push origin --delete feature/new

至此,通过rebase重排提交,且出现冲突的例子解释完毕。但有些特殊情况。

但有些开发环境的合并分支并不能直接使用merge,而是使用Pull Request,这种情况就不适用这个方案,因为这种情况下dev分支极有可能是不允许直接merge的,所以最后的git push origin HEAD:dev并不能执行成功

Git的改变

你可能会好奇我根本没有做merge,它这个merge提交是怎么生成的?

答案就在这个git pull上面,一般我们在准备要合并自己的分支到dev时,会习惯性的执行git pull,这个命令相当于两个命令的结合git fetchgit merge(仅限老版本的Git,默认pull是基于merge,新版本的Git安装式会询问pull是基于rebase还是merge),它会拉取远端分支上的内容,将其merge到本地分支中,所以这次merge提交是由pull请求生成的。

但新版的Git已经意识到这个问题,开始将pull的合并策略改为可调整的,如果用rebase的话就可以减少很多麻烦的工作。