微信扫描登录

聊聊代码提交那些事
作者:庄表伟

一、解题

为何要聊“代码提交”这么小的一件事情?在Gitchat的主题介绍里,我谈到了一个线索:随着团队人数越来越多,提交代码这件小事,变得复杂起来。甚至极端一点说,任何复杂度的软件项目,也无非是要管好两件事情:需求和代码;

顺着这条线索,我们可以观察到一种螺旋上升的现象:

  • 需求越来越多、越来越复杂 ->
  • 代码越来越多、越来越复杂 ->
  • 添加更多的人手开发软件 ->
  • 更多的人手会犯下更多的错误 ->
  • 选择制定某种管理规范 ->
  • 通过工具执行某种规范 ->
  • 围绕一组工具的特性,形成某种工具与管理的共生平衡 ->
  • 需求再次爆炸...

从个体户到小团队、从小团队到大兵团,需求的数量、人员的数量、代码的数量,都会有百倍、千倍甚至更加惊人的增长。在这种变化的过程中,不仅仅是人数需要增加,相应的工具、以及管理流程,也要发生变化。

本文主要就是探讨:如何管好代码这件事情,这篇文章不会介绍基础知识,而仅仅是一种逻辑上的梳理。关于:代码提交、代码管理、团队管理、研发质量管理等等内容。

二、个体户的幸福生活

在单枪匹马干活的日子里,很多事情都相当简单。甚至,在当年我们都不知道什么叫版本管理。我们在自己的机器上写代码,当然也在自己的机器上完成编译,然后自己试用一下,算是“测试”。

如果是服务器端的开发,我们就直接登录到服务器上,打开vim,直接写代码,写完了保存就OK。

当年我还在写PHP程序的时候,最喜欢的编辑器是EditPlus,因为它支持FTP链接到服务器,直接就能修改服务器上的文件,Ctrl+S以后,再刷一下浏览器,结果就出来了。

这样的开发习惯,当我成为Java程序员的时候,也影响到了我的技术选型。我最喜欢的java web server是resin。最大的一个原因是:使用resin的服务,修改java代码,也不必编译、然后再重启服务。

三、为何需要版本管理?

当我们的项目,越来越复杂、代码越来越多时,就开始需要版本管理的工具了。

在事情还不那么复杂的时候,我们可以将修改代码的原因,记录在注释里。但是,如果一个文件被反复修改,那么将修改理由记录在提交说明里,将是一个更好的选择。

一个原本正确的代码,被改坏了,需要回退。这个时候我们不能仅仅依靠自己的记忆力,恢复代码到原来的样子。

当协同开发的人超过一个,就可能会出现:一个文件,曾经被多个人修改过的情况。这时:找到当时那个干了坏事的家伙,就变得非常重要。

所以,我们至少应该能够有地方记录:谁,在什么时候,因为什么理由,修改了一个文件(或者修改了一组文件)。

四、早期的版本管理工具为何选择那么变态的工作模式?

我曾经用过的最早的版本管理工具,叫做Visual SourceSafe,简称VSS。也许是资历太浅,VSS就是我用过的,最难用的版本管理工具了。

为了保证源代码的安全,VSS采用了最为极端的独占工作模式。当我想要修改某个文件的时候,就把这个文件check out出来,然后在我修改完成,并再次check in之前,任何其他人都无法check out这个文件,当然也无法修改这个文件。

因此,当时最常见的办公室对话是:是谁,又签出了文件?那个谁谁谁,你快点改啊,我也要改这个文件!

事隔多年以后,我在维基百科上看到:“VSS虽然是微软公司的产品,但微软内部却很少使用它。” 真是欲哭无泪。

本质上,VSS是一个将代码安全的需求,置于团队研发效率之上的工具。幸好,这个产品已经没人用了。

五、为何需要分支和版本号?

在人数很少的时候,VSS实际上也能工作得很好。但是,当软件越来越复杂,需求越来越多的时候,我们只能招聘更多的工程师,并且催促他们尽快上手开始写代码。

为了帮助一群人,能够顺畅地协作,我们需要创造诸多的概念、流程、工具与协作方法。

  1. 版本号:实质上对为一个阶段的工作成果命名,按照某种惯例,特别不成熟的成果,我们会命名为0.1,甚至0.01;第一个可以正式发布的版本,我们会命名为1.0。预发布的版本,我们会称之为1.01-alpha;然后是1.01-beta;最终我们会发布一个1.01-final;

    这些做法实际上意味着:版本号具有隐含的质量属性。这样的质量属性,即方便对外公告,也方便内部管理;更进一步,从搜集bug的角度来说,我们也可以较为准确地将某一个bug,记录在特定的版本之下,直到他们在每一个版本之后,被解决掉。

  2. 分支:实质上是为了更多的人并行工作,而出现的概念。当然,这一概念,需要版本管理工具的支持。

    在某个版本发布之后,并不意味着这一版本已经完美无缺,所以,我们需要维护一个分支,在其中只添加bugfix类的改进,而不会增加新的功能特性。

    而另一方面,我们会有一个master分支,开发人员可以尽情先把功能特性提交上去,而不必太过顾虑产品的稳定性。

    在人数更多的时候,我们会创建更多的分支,比如:一个大项目组,可以分为3个小组,每个小组有一个自己的分支,他们先在自己的分支上工作一段时间。然后再将这一个小组的工作,批量汇入主干分支。

六、开源社区的一大创造

在工具改进的过程中,工作流程也在改进,起因还是因为人容易犯错误,在开源社区,这样的问题尤其突出。如果是在公司里,大家都是抬头不见低头见的同事,当初也是经过足够的面试,具备基本的能力,才能进来的。但是在开源社区,一个从来没有见过的ID号,想要向我的项目提交代码,我怎么可能放心?

因此,在开源社区最初的形态:mailist中,就已经形成了一套行之有效的代码提交规范。

想要向某一个项目提交自己的代码,首先需要订阅那个项目的邮件列表,跟里面的committer混个脸熟。提交代码,其实就是发一封邮件。在邮件里,要清楚地介绍自己打算干些啥,简明扼要,而且最好不要一下子就“搞一个大动作”。一开始,大家都不熟,你上来就想要贡献一个“几千行代码的大特性”,谁都没法信任你。

最好是从bugfix开始,所以那些代码贡献,都被称之为补丁(patch)。一个补丁,最好不要太大,几行(最多几十行),这样那些大牛们才会有心情review这些代码。

因为,实际上只有他们才有资格向代码库提交代码,所以:他们才被称之为committer。所以,我查看提交日志的时候,会发现两个属性:author(实际写这段代码的人)以及committer(将代码提交到代码库里的人)。

而这样的工作模式,就被称之为code review。随着开源社区的日益成熟,开源社区的这种工作模式,也开始进入企业,在企业内部贯彻code review的工作流,也变成了一种常态。

七、另一种保障质量的手段:自动化检查

随着团队数量的进一步上升,仅仅依靠人类肉眼审查代码,想要杜绝各种错误,其实是不可能。这时候,工具的作用再一次体现出来了。

  • 如果一个工作,实际上是一种简单重复劳动,那么:就可以通过编程,将他自动化完成。例如,自动化编译、自动化测试。
  • 如果一个工作可以自动化完成,那么:我们完全可以将这个工作分解得更加细致。例如:从自动化的验收测试,到自动化的单元测试。
  • 如果我们可以在一个工作的各个环节都执行检查,那么:我们完全可以自动化地检查所有可能检查的部分。例如:我们不仅仅可以检查功能,还可以检查语法,可以检查编程规范,检查是否存在安全隐患等等。
  • 如果我们可以自行一次自动化检查,那么:我们完全可以更加频繁地执行这样的检查。例如:开发者的每一次提交,都能够触发一次自动化的检查。

最为理想的开发流程是:所有的开发工作,都能够各自独立进展,互不干扰。最好是每一个特性、每一个bugfix,都有一个独立的分支。然后,在自己的分支上,完成一套自动化检查。再将这个分支合入主干,再跑一遍全套的自动化检查。这样,我们就能知道:这一项工作确实已经完成了,而且没有破坏任何其他的部分。

当然,事情并没有那么简单,如果是小型项目,那么每次CI的成本,都会非常低。速度也会快到忽略不计。但是,一旦项目变得复杂,代码库变得庞大,编译与测试的时间以小时来计算时,当开发者数量增长,并发提交的人数,超过并发CI的服务器数量时,问题就会复杂到专门写一本书了:)

八、为何从选择SVN到选择Git?

如果Linux的开发者,不是成千上万那么多,也许Linus当初也不会选用BitKeeper。当然,也不会因此出现各种风波,最后让大神在一怒之下,10天时间,自己撸了一个Git出来。

相对于SVN,Git有一些特别明显的好处:

  • 分布式配置库,支持离线工作,保存各种提交历史。而且,在不存在中心仓库的情况下,任何两个Git仓库之间,可以交换代码。
  • 更好的分支模型,使得创建一个分支,合并一个分支,快捷高效。当一个团队的人数越来越多,代码越来越复杂时,使用SVN分支所带来的烦恼,就会迫使他们最终选择Git。
  • Git有一个复杂的由commits组成的DAG(有向无环图),这样一种数据结构,能够更加准确的记录实际开发过程中的各种状况,尤其是在用到rebase、cherry-pick、three-way merge的特性时,会感觉非常自然。

当然,有很多不必面对这些复杂性的软件项目,会感到Git过于复杂,难以理解,而且没有必要掌握。至于那些的确需要创建诸多分支,的确存在多种代码合并的情况,的确需要更加频繁的运行自动化测试的团队,选择Git,就会非常自然。

九、从Git到Github、Gerrit

当出现了Git这样的工具之后,我们会发现世界并没有变得更加美好,因为一个常见现象:过于灵活的工具,会让人更加容易犯错。在项目组完全使用Git的情况下,依然可以设计出多种多样的工作流程。

  • 最简单的一种:每个人有自己的Git仓库,然后各自多加几个remote,然后随意地传来传去
  • 类似于SVN的集中工作流程,大家都向同一个中心仓库提交代码
  • 集成管理者工作流,贡献者将代码推送到各自的公开仓库,然后给维护者发送邮件,请求拉取自己的更新,最后由维护者手动完成合入
  • 司令官与副官工作流,在Git官方网站的介绍中,描述到:“一般拥有数百位协作开发者的超大型项目才会用到这样的工作方式,例如著名的 Linux 内核项目”

在Git出现(2005年)之后的几年里,陆陆续续出现了一些新的创造,较为突出的有两个:Github(2008年)、Gerrit(2008年)。这两个工具,本质上都是对于过于灵活的Git工作流程进行限制、简化以及优化。只不过两种方案的出发点,大不相同,导致其产品设计与功能特性,也有了诸多区别。

Github的出发点是大量中小型开源项目的托管服务,而且,为了形成一个完整的大社区,Github在社交化方面,投入了大量的精力,也创造了诸多世界第一的奇迹。

至于Gerrit的出发点,这是大型复杂项目的Code Review管理。从诞生之出,Gerrit就是为了像Android这样的大型项目服务的。通常一个Gerrit实例,只服务于一个大项目。对于构造一个社区,形成某种跨项目的开源生态圈,Gerrit并无兴趣。

十、没有终点的旅程

很多时候,我们会发现这样的现象:研发工具的演进与开发模式的演进,往往呈现一种交互影响的关系。复杂的开发模式,会促使新工具的诞生。而新工具的引入,也会促使传统的开发模式,发生变革。

在本文的前半部分,我们主要探讨的是各种需求复杂化之后,如何催生了一代又一代的新工具。而另一方面,当团队引入一种新工具之后,常常会发生的,并不是happy ending。

一些问题的确被解决了,但是新的问题又产生了。乐观的说,我们会发现新工具的各种能力,经过巧妙的组合,产生新的用法,甚至远远超过工具设计者的设想。悲观的说,我们也会发现新工具的各种强大能力,在尚未熟练掌握的人手里,会产生各种意想不到的灾难。

在那些较为积极进取的团队中,新工具往往会成为某种催化剂,他们会迅速上手,然后越玩越好。于是我们最终会发现,一切问题,究其根本,还是人的问题。

人、工具与流程,是项目管理中的三大要素,而其中人是决定因素。对于管理者而言,是否能够正确的判断问题出在哪里,以及通过何种手段去解决,是最为重要的。我们经常会遇到这样的管理者,在遇到麻烦时,习惯性的选择某一种手段:有些领导特别喜欢“制定管理规范”、有些领导特别喜欢“采用先进的工具”、还有些领导特别喜欢“给员工打鸡血”。我们并不能简单地说他们做错了。但是:只会单一手段的领导,通常更容易犯错

总而言之,在软件开发的过程中,为目前的团队选择合适的工具,并且既不保守、又不冒进的选择某种新的工具,而且还能够带领整个团队,用好这些工具。的确是极大的挑战,也可以说是没有终点的旅程......

Chat实录:《庄表伟谈代码提交的最佳实践》


enter image description here

enter image description here