
熟练使用版本控制系统是作为一个程序员应该掌握的基本技能,本文主要介绍了在多人协作中git的常用操作rebase和cherry-pick。所有内容均是个人想法,不是标准,可能存在错误或有欠缺的地方。仅做参考。
多人协作模型
Git常见的多人协作模型很多,我熟悉的主要有主干开发模型、特性分支开发模型这两种。
主干开发即仓库一般只有一个master分支(也可能有其他专用来发布的分支),所有开发者的代码都提交到主分支,比较适用于规模较小的项目(或微服务架构的项目)。
但是当一个项目较大或者同时参与维护的开发者数量较多时,主干分支开发就有点无能为力了。
因为这样的项目需求变动频繁,经常会有多个需求需要同时开发,而且每个需求的上线时间往往不一致。这种场景下特性分支模型就是最好的选择。
即一个新需求一个分支,当新需求开发完毕且测试通过之后再合并到master分支部署。例如github的Pull Request模式,gitlab的Merge Request模式。
优雅且实用的rebase、cherry-pick
不过在实际应用中,每个公司都可能有自己独特的协作风格。
比如我们上家公司,使用的也是特性分支开发模型,但是全程几乎没有用到merge,而使用rebase,cherry-pick替代。
什么是 rebase
rebase常常被拿来与merge作比较,他俩都可以用来合并两个分支的代码,只是在处理原理上有差别。
merge 比较简单也比较安全,对初入行的小白友好,它只是将两个分支的代码做一个合并,有冲突则解决冲突,此外不会有任何操作风险。
但是两个分支相互merge一般会生成一个新的提交,如图,很不美观。

相对来说,用rebase合并代码就是个进阶操作了。我认识的大牛中基本都是用rebase合并代码的,鲜有用merge的。因为rebase不会造成新的merge提交,提交历史会比较整洁。但是如果不理解rebase的原理胡乱执行rebase很容易酿成事故。(具体后面会说)
简单介绍下我对rebase的理解。
顾名思义,rebase 变基-变更基线

。rebase执行过程中,会以目标分支为新的基础(比如你在dev分支执行 git rebase master,则以master为基础),你当前分支(dev)与目标分支(master)的
差异(新)提交都会以目标分支(master)为基础重新提交,而历史的新提交记录会被
丢弃,
即相当于将当前分支同步到目标分支一致然后将当前分支的新提交重新提交一遍。(
注意这里的丢弃说的并不是丢弃代码,而是丢弃提交历史生成新的提交历史的过程,即会生成新的提交hash。跟后面要说的
cherry-pick有些类似,只不过cherry-pick不会丢弃原提交历史,而是仅在目标分支上直接生成一个新的内容一样的提交)。
除了两个分支间的代码合并,rebase还能做很多事情。
将同一个分支上的多个提交合并成一个大提交也是我比较喜欢用的一个功能。因为开发中,我比较喜欢小步提交。比如构思出大概框架提交一版,基本实现功能提交一版,最终review代码时发现可优化的点或者潜在的bug又会有多个提交。但是同时我又是个有洁癖的人,我希望推到远程的代码应该是一个完整的提交,即一个需求在主干上只对应一个提交。这样主干看起来就比较整洁,同时也方便领导对你的提交做code-review。
除此之外rebase还可以将一个大的提交拆分成多个,或者丢弃某个提交等等。
什么是 cherry-pick

cherry-pick 更加简单直观,从名字上就能看出来,'摘樱桃',摘之即用的意思。在任意分支上都可以将其他任意分支的任意一个或多个提交摘过来合并到当前分支。在idea中操作更简单。只需选中目标分支的提交记录然后点击小樱桃按钮即可

当然无论是rebase 还是cherry-pick,与merge 都一样,本质上都是合并代码,都可能会产生冲突。遇到冲突只需要按照提示,妥善处理就行了。相信一个合格的开发是不会在处理冲突时把代码搞坏的。
诸葛亮带你使用rebase、cherry-pick进行多人协作
为了方便描述,我建了一个测试仓库
github.com/qjyoung/git…
(演示过程大部分操作使用idea内置支持的Version Control工具,用该工具操作git更加高效)
这个仓库里一共有三个贡献者,分别是管理员、创作者诸葛亮和归有光。
首先管理员创建了两个分支,一个master 一个ci分支。主分支master用来发布,而ci分支用来内部审阅测试。
仓库建好之后,大家纷纷从主分支创建出自己的分支,开始创作。诸葛亮创建了分支 ZGL_出师表 要写出师表,归有光创建了分支 GYG_项脊轩志 要写项脊轩志。他们俩忙的不亦说乎。
归有光同学才思泉涌,一气呵成。兴冲冲的要将自己的作品发布。但是在发布之前必须先通过内部审阅。于是他切换到ci分支。在idea里将鼠标指到了自己分支的那个提交上。点了右上角的小樱桃按钮进行cherry-pick操作。一切顺利,没有冲突。此时自己的作品就成功合并到了ci分支上,只需要将ci分支推送到远程仓库,由运维和测试同学集成部署并审阅就可以了。

cherry-pick 过程

cherry-pick 执行成功之后,我们可以看到ci分支上也有了一个相似的提交,虽然没有冲突,但是也生成了一个新的提交,内容一样hash不一样。
诸葛亮同学心情沉重,在自己的分支上提交了三次才算把出师表写完了。
恰就在这时候,管理员发布了几条注意事项,并提交到了git仓库里,嘱咐大家要按着注意事项里面的要求来写。
但是这个注意事项的文件呢是在master分支上,诸葛同学本地分支上没有这个文件,要看只能去提交历史里面去看了。这就有点不方便。那该怎么办。像归有光同学那样把注意事项pick到自己的分支?可行。但是管理员提交了两次,我需要cherry-pick两遍。这还是管理员提交的次数较少。显然这不是最好的解决办法。那怎么做才最高效呢?
那就用merge吧 ,于是诸葛亮在命令行执行了
git merge master

没有冲突,master分支的注意事项文件顺利地合并到了自己的分支上。但是令诸葛亮有点头疼的是,即使没有冲突也在自己的分支上留下了合并记录。诸葛亮并不想要这个记录。
诸葛亮忽然想到刚学的rebase命令也可以用来合并代码,于是他将代码回滚到合并之前,并小心翼翼的在命令行敲下
git rebase master
一个错误也没有,诸葛亮同学第一次rebase操作顺利完成。主分支的注意事项文件成功的合并到了自己的当前分支上。并且没有留下像merge那样的’多余‘的合并记录。

但是让诸葛亮同学感到意外的是自己之前的提交历史都变成了新的(如上图)。每个提交的hash都与原先不一样了,而且都被移到到master分支最新提交的后面,但是内容没丢。诸葛亮恍然大悟,原来这就是rebase合并的真谛,变基-重新提交。
看完注意事项中的要求后,诸葛亮对自己的文章进行了修改。又提交了一个版本。此时诸葛亮同学自己的分支上已经有四个提交了,而且做得都是一件事情。

如果直接cherry-pick到ci分支,看起来有点傻,而且需要pick多次。所以在pick之前应该将这四个提交合并成一个提交,像归有光那样只往ci分支上pick一个总提交,看起来既美观又高大上。
于是诸葛亮同学用idea提供的工具操作了rebase合并。如下

最后欢快的将自己的作品pick到了ci分支上。
没多久内部审阅通过归有光和诸葛亮同学的两篇作品,并由管理员cherry-pick到master 分支,发布给所有人查看。
最终效果

rebase 雷区
那么上面可以注意到,将别的分支代码合并到master分支采用的是cherry-pick,那么可以用rebase吗?
绝对不行!
使用git rebase合并代码要遵循一个原则:不要在公共分支上执行rebase合并其他分支的代码。
如果你理解透rebase原理就能知道禁止这么做的原因。
因为master,ci等公共分支上有别人提交的而你分支上没有的提交,也就是说,你的分支相对master这类分支提交”不够健全“,如果你在master分支执行了 git rebase your_branch 那么就会以你的分支为新的基线,所有你分支上没有的提交都会被重新提交,虽然内容没变,但是hash变了!在你的同事想要在将master分支上的修改merge到他自己的分支时就会出现重复提交。
以下我故意用诸葛亮这个作者在master分支上执行了rebase ,

结果可以看到诸葛亮把master上归有光的提交给重新提交了一遍。

此时再推送提示本地代码不是最新的需要合并或者rebase。如下图

如果选择merge或者放弃推送使用push -force强推,在别人拉下来代码时就会出现重复的提交。如下图(直接选择merge的情况,强推也一样)

一般使用idea工具在公共分支操作rebase合并其他分支时也会出现警告。如下图,提示会造成重复提交,非常不建议这么做。


所以,使用rebase需要遵循上面那个原则,就是如果当前分支有多人在维护就不要使用rebase来合并其他分支的代码,可以使用cherry-pick代替。
也就是说,rebase合并代码一般用在两种情况下,一是将主(公共)分支(健全分支)的代码合并到到个人分支(缺胳膊短腿分支)。二是在公共分支下,合并远端公共分支代码到本地公共分支(因为远端公共分支除了你本地的新提交外,已经包含了本地公共分支的所有其他人的历史提交,因此不会造成重复提交)。
祝你一切都好。

