为什么不能直接push到master分支?

许多大型Git项目中不允许直接push到master上。但是当需要给新人介绍为什么不能直接push到master上时,一个Git老手也不一定能简明扼要地解释清楚。想要让新人放弃直截了当地git push origin master,采取建立分支-commit-push-PR-merge的弯弯绕方式,需要有让人信服的理由,特别是在新人对Pull Request(PR)还没有任何概念的时候。本文试图以比较好理解的方式,帮助初学Git的人了解不能直接push到master分支的原因。

不能push到master上的根本原因是:

master应该是一个稳定的分支

所谓“稳定”,是指这个分支在任意时刻的代码都没有大的问题,可以编译或者可以实现它设计时的目的。这个大前提有时不成立,比如这个仓库里放的是一个短期的个人的项目,我只给我自己近期用,我闲着没事就push一下存个档,那这时直接push到master上就是天经地义的。假如项目仍然是个人项目,但是时间尺度从短期变成长期,比如你知道未来的你要用的时候,这时你就会希望master是稳定的。否则半年之后你再运行仓库中的代码(可能是想作为一个小工具使用),可能会面对自己也看不懂的错误信息。另一方面,当项目不再是个人的,你肯定不希望其它人来到你的仓库时发现这个代码根本就跑不了。其实,对于纯粹的短期、个人项目,Git并不是最合适的版本管理工具,这不是它的设计初衷。所以绝大多数情况下,master应该是一个稳定的分支。

而不能push到master上的直接原因是:

大型多人项目中保持master稳定比在小型单人项目中困难得多

对于小型的只有一个贡献者的项目,直接push到master上是有很大概率可以保证master的稳定性的,这时直接push到master上也无可厚非。
实际上,你正在看的这个blog,从头到尾都是直接push到master上的。
小型单人之一不被满足时,直接push到master上就很可能影响master分支的稳定性。

当项目不再是小型的

比如说只有你一个贡献者的项目。你在这个项目上贡献了很多代码,它不再是一个小型的项目,你已经不能清晰地记得每块代码的逻辑是怎样的了。这时你的每次commit可能都会使该项目中你未考虑到的一部分代码产生bug。当然,你可以在push之前编译,运行测试,但这一方面很繁琐,另一方面你很难根除本地环境对项目的影响,举例来说:

  • 你有一个关键的文件没有add、没有commit,淹没在你另一堆确实不需要add、不需要commit的文件海中,这时你在本地一切正常,但push到master之后master就不能运行了。
  • 你的代码需要设置一些环境变量才能按照想要的方式运行,在本地因为你已经设置过所以测试通过,但是push到master上之后别人无法重复你的结果。

想象一下,在以上两种情况下,你在本地测试成功,心满意足地push到了master上,随后两年没有再动这个项目。而等你两年后从Git下载下来这个项目想使用的时候,却发现代码完全不能work,而当时可以work的本地版本因为换了电脑已经找不到了。看起来,最好是有一个合作者能在你每次commit之后拉取你的分支,然后在他的机器上跑一遍测试。这样才会能比较可靠地保证master的稳定性。

当项目不再是单人的

如果有多人同时直接向master上push,那可以肯定即使是小型的项目master也会变得一团糟。为什么?假如贡献者甲和贡献者乙同时clone了master分支开始开发,贡献者甲修改了某个库函数并commit到了master上,而贡献者乙并不知道这个库函数被修改了,继续按照他分支中的库函数的用法来调用。当贡献者乙完成了他的代码后,他在本地跑了详尽的测试并通过,但是当他的代码commit到master上以后,因为master上的库函数被修改了,合并后代码完全不能work。
假如贡献者乙是一个脾气很好的细心的开发者,在push到master分支之前,他每次都会rebase到最新的master(对于不了解rebase的同学,可以把这句话理解为将最新的master合并到贡献者乙的分支上)并运行测试,但他也不能保证在他rebase后跑测试时master没有新的更新。master分支的稳定性成为了俄罗斯轮盘赌的赌注。

使用分支和Pull Request(PR)

所以,对于大型的、多人的项目,上述问题交织在一起,使直接push到master成为了一种很可能破坏master稳定性的方式。而如果使用建立自己的分支进行开发最后发起PR来更新master的方式,那么这些问题就可以在很大程度上避免,这一原理其实在CS中非常常见:

All problems in computer science can be solved by another level of indirection

因为本文主要是讲不能push到master的原因,如何采用分支-PR进行Git项目开发的流程就不再介绍,网上有大量的资料。
当你发起一个PR时,你可以找一个合作者(哪怕是一台机器Travis)帮助你从头验证代码的正确性。确认将PR合并可以由一个人统一管理,这个人同时负责验证你的commit合并到master上之后是否是正确(这个人也可以是一台机器),如果不正确则不能合并。
简而言之,PR就像是一个质检员,保证了每次commit合并到master后,其结果是稳定的master。
不过,使用PR时也会有代码冲突的问题,但是PR时的代码冲突是可以有效处理和管理的。

Pull Request(PR)的其它好处

为了保证master的稳定性,我们选择了建立分支-PR的方式来更新master,这一流程除了可以保证master稳定性以外还有其它好处。这些可能需要掌握Git的一些高级用法或者用了一段时间的PR才能体会到。

在个人分支中可以修改历史

Git允许你修改它所记录的文件历史。这个功能有很多高级的用法,但最常见的使用场景可能是以下这个:你在commit后发现有个很简单的bug(或者合作者不满意,等等),你立刻察觉了bug的来源改了一行代码并没有跑测试(可能要花几十分钟)就commit了,结果你的判断有误,仍然有个很简单的bug,如此循环。结果你的真正贡献只有一个commit,倒是花了5个commit来修一个无聊的bug。要知道,每个commit都是要写信息的。这时你很希望能把这些commit合并成一个或者两个。在你自己的分支上,你的愿望可以用一两行命令简单地实现,整个过程没有任何痛苦,甚至还有点愉悦。但假如你是直接commit到master上的,那么擅自修改历史将会使其它所有人的工作出现严重的冲突问题,他们可能会顺着网线来揍你,因此是需要严格避免的。

在PR中commit拥有丰富的上下文

和写代码同样重要(如果不是更重要)的是理解当初代码为什么写成这个样子。在一个大型的PR中,开发人员会开展热烈的讨论,不同观点激烈交锋,相关的issue和PR会被交叉引用。在GitHub上,任意一段代码都可以很方便地追溯到与它相关的PR。阅读这个PR的历史会对理解这段代码很有帮助:是谁提了怎样的需求导致了这一段代码的产生?这一解决方案是如何在其它方案中脱颖而出的?在实现的过程中有哪些需要注意的细节?在一个维护良好的GitHub仓库中,这些非常有意义的问题会得到清晰的回答。通过issue和PR,GitHub的历史在commit信息之外又多出了一个丰富的维度,整个项目也因此显得更有生命力。

总结

本文列出了四条为什么分支-PR比直接push到master好的理由:

  • 通过PR可以保证代码质量
  • 通过PR可以缓解多人合作的冲突问题
  • 在自己的分支中可以修改历史
  • 通过PR代码的更新含有更丰富的上下文

其实,分支-PR的好处还有很多,只要入了这个坑,就好比从简单代数进入到方程,回不去了。