snowdrop's blog

git操作中的merge和rebase

git操作经常使用,如拉取、推送、合并代码等操作都是日常开发中必不可少的,尤其是合并代码的时候,rebasemerge自然是都会用到的,但从来没有深究过这两种合并代码的方式到底有何差别,使用场景是什么,今天决定一探究竟并记录下来。

用途

首先,两者的诞生都是为了解决同样的问题:将一个分支的改变合并到另一个分支上。但两者的实现方式还是有很多差异。

具体差异借助一个开发中常用的场景来加以说明:当你在一个开发分支上开发一个新功能的时候,团队其他人在mastercommit了若干个新的提交,于是分支的历史变成了如下图这样。由于master上的commit对你正在使用的开发分支feature是有意义的,为了将master上的commit合入你的开发feature分支上,你有两个选择:merge或者rebase
提交历史

选择merge

1
2
git checkout feature 
git merge master

合并后,feature分支的提交历史变成了如下图
merge后feature分支的提交log

从图中我们可以看到,最终两个分支的历史以分叉线的方式汇合到一起,且在汇合出是一个多出来的新的commit Merge branch 'master' into feature.
以上是没有出现冲突的情况,但出现冲突是非常常见的事情,同样以例子来说明
featuemaster分支同时修改了a.jsb.jsgit merge master出现冲突提示,解决后feature最终的提交历史变成如下图:
feature git log

由此我们可以得出使用merge操作的特征

  • merge是一个非破坏性的操作,原有的已经存在的提交(时间先后顺序、log线)等都得到了保留
  • 产生了一个外来的代表合并的commit。如果master分支非常活跃,会对feature造成大量的历史污染,造成其他开发者难以看懂或理解你的项目的历史记录。
  • 出现冲突时,只需解决一次冲突,解决完冲突后会产生一个commit,该commit即代表了合并也包含了最终冲突解决结果

选择 rebase

1
2
git checkout feature 
git rebase master

合并后,feature分支的提交历史变成了如下图
rebase后feature分支的提交log

从上图我们可以看出,整个master分支移动到了feature分支上,且没有使用一个代表 merging 的 commit,整个项目提交历史很清晰。

同样,当出现冲突时,rebase是如何解决冲突的?以同样的例子操作为例,操作如下图
rebase conflict
从图中我们可以看出,执行git rebase master之后提示b.js有冲突,手动解决后,执行git rebase --continue,又提示一次让解决b.js的冲突,重复上述操作后,最终feature分支的提交历史如下图
rebase conflict git log
从日志中可以看到,最终并没有产生一个解决冲突的提交
由此我们可以得出使用rebase操作的特征

  • 没有引入不必要的commit
  • 整个提交历史是线性无分叉的,便于阅读及清晰操作整个历史记录。但清晰的commit的代价就是失去了可追溯性,因为从现有的提交历史中,已无法得知是在哪个时间点及提交点将上游的master分支合并进入feature分支的。
  • 出现冲突时,可能需要多次解决冲突(具体以feature分支的提交点中和master分支中有几次出现了冲突为准),解决完冲突后需要以命令git rebase --continue继续变基,且不会产生额外的commit。

    交互式rebase

    rebase还有一个更高级的用法:交互式变基,除了完成最终的分支合并之外,交互式变基可以帮助我们在变基过程中,增加自己的操作(合并提交、拆分提交、删除提交),然后再变基。虽然平时使用得不多,但借助它也可以产生一些神奇的效果,在此也一并简单介绍下。

交互式rebase对应命令

1
git reabse -i <branch>

合并提交

假设我要将下图中标红框的部分合并成一个提交
交互式rebase combine
首先要找到起始commit的前一个提交,即e3017604,执行git rebase -i e3017604,
此时会唤起编辑器,并带出e3017604前的所有提交,按时间顺序展示,首行是最早的提交,末行是最新的提交,如下图
交互式rebase combine
每行格式如下

1
(action) (partial-sha) (short commit message)

action 有很多种,默认是pick
action
现在我们想把后三个提交合并到第一个中去,这里需要用到 squash,该 action 表示 使用该提交,但是把它与前一提交合并,所以只需把后四个的 action 改为 squash 即可。
squash

保存之后,会唤出编辑器提示创建一个新的提交信息,编辑好提交信息保存即可。
合并完之后历史记录变成如下图:

combine

拆分提交

如果我想把某个commit拆分成多个commit,可以使用edit action,该action告诉git说我要使用该提交,但是先在这里停一下,等我把该提交拆分好后再继续执行下一步

初始提交历史如下图:
split commit
红框标出的master commit 5这个提交中包含了两个文件修改,新增b.js和修改a.js,我现在想把它拆分成两个提交,一个提交修改一个文件,那我可以这样做
执行git rebase -i 9d257fe8,然后修改 27c84ed 这个 commit 的 action 为 edit,如下图
edit
保存并退出后,提示执行git commit --amend对commit信息进行重新编辑
我们这里是要拆分 commit,所以要先对 commit 内容 reset,然后重新提交,按照如下图的操作一步步提交:
commit history
执行完成后git rebase --contine,即达到了我们想要的效果,即把一个提交拆分成了两个提交,最终结果如下图
result

删除提交

如果想删除某个提交,使用 git rebase -i *** 后直接在编辑器中使用drop action删除那一行 commit 即可,如果出现冲突,手动解决冲突后再执行git rebase --continue即可
下图为我执行操作删除’master commit 4’这个提交点及最终的执行结果

总结

最后,对这两个命令做个总结。
总结

这两个命令各有优劣,很难概括哪一个最好,那到底该如何选择呢?
个人理解:

  • 团队成员对两种命令的熟悉程度
  • 个人喜好:比如有人倾向清晰的阅读性,有人不喜欢多次处理冲突

一般建议:

  • 开发过程中,合并上游分支(如master)要频繁(切忌两耳不闻窗外事,一心闷头写代码)时使用rebase,有冲突提前就fix掉
  • 测试通过后合并到上游分支时,可以使用merge,保证所有提交点的可追溯性

不管选择哪个,作为一个开发人员,两种合并方式的异同都建议大家掌握。

参考资料

Merging vs. Rebasing
merge和rebase的区别

浏览量

扫描二维码,分享此文章