就我对身边同学的观察来看,版本控制的使用率还是不高。就我个人经历和收集到的反馈来看,没有人喜欢用Subversion(即svn,原因下面会细讲)。既然svn都没人喜欢用,分布式版本控制又是啥玩意?

希望通过本文,让大家知道,版本控制不是那么恶心的事情,甚至可以很舒服。:)

为什么需要版本控制

在开发的时候,你是否遇到过以下情况:

  • 想增加一些新特性,但风险很大,怕影响已有的代码
  • 上一次运行还好好的,修改了代码就出错了,悲剧的是不知道哪儿的修改导致了错误,回不到上一个健康的状态了
  • 新增的代码出了bug,越改越乱,bug越来越多
  • 用U盘拷贝代码,拷来拷去失去同步(尤其是几个人一起做的时候!)

如果你的答案是Yes,版本控制就是你需要的。

集中式版本控制的劣势

集中式版本控制(Centralized Version/Revision Control System, CVCS)解决了上面说的一些问题,还有一些解决地不够好。以当前最流行的CVCS工具svn为例,碰上以下问题你还是会崩溃:

  1. 每个目录下都有个.svn,导出一份干净的源代码很麻烦1
  2. commit了才发现一些新增的文件忘了加了,又要再次commit
  3. 手贱commit了一下,变更同步到服务器,回不去了
  4. 擦!服务器崩了,unable to connect,整个团队的活又要停滞了
  5. 擦!连不上服务器,unable to connect,自己的活儿无法继续
  6. 自己想搞个代码库,还要配服务器神马的好讨厌**
  7. 同事和我改了同一个文件,每次update都conflict

1 这个问题在svn 1.7版中已经解决

** Google Code是个不错的开源项目托管站点

可见,集中式版本控制的悲剧之处就在于它有个集中的服务器,所有本地文件都只是服务器上代码的临时拷贝而已。真正的代码只有一份——并且它在服务器上。所以,服务器一旦出了问题,对所有本地库都会产生影响。

最近一例悲剧是GCC的官方svn库,开发者在一次commit中居然一个失误就把trunk给删了,后来又恢复了。可见,本地和服务器同步后,误操作的传播是零成本的

另外,svn自身的设计也有很大的缺陷,比如conflict的问题,其实大多数情况下,改动同一个文件的不同部分基本不会冲突,可惜svn一视同仁当作冲突,然后你就要面对如下纠结的抉择*:

  1. 用我的代码
  2. 用别人的代码
  3. 先用我的再用TA的
  4. 先用TA的再用我的
  • 如果你使用TortoiseSVN的话

分布式版本控制的好处

虽然集中式版本控制比“裸奔”好了不少,但用久了也难免处于精神崩溃的边缘。这不是你的错哦,所以分布式版本控制(Distributed Version/Revision Control System, DVCS)来救你咯!一旦你熟悉了DVCS,你就会觉得集中式版本控制这玩意儿简直就是反人类的。

总的来说,DCVS比CCVS提升的地方有:

  1. 独立的本地代码库,每个代码库都是平等的
  2. 本地commit,不需要和服务器同步
  3. 随意clone,分支版本想有多少有多少
  4. 神志正常的merge,再也不用每次update都要进行选择了
  5. 更细粒度的commit控制
  6. 很大程度上你终于可以后悔

具体来说,就是:

本地的代码库不再从属于服务器

首先,你可以在本地建版本库,爱建多少建多少,在每一个版本库内都可以进行commit,update,revert等操作。因为所有的操作都在本地进行,不需要网络传输,你再也不用等待龟速的网络,或是因为中途连接断开而惆怅了。

Q: 咦,那是不是就没有服务器了?那团队怎么协作啊?

A: 你已经被svn毒害太深了。记住,在DVCS里,不管是你的,TA的,还是服务器上的代码库,地位都是平等的。这就意味着,你可以任意在几个代码库中间pull和push*。所以,你可以在一台电脑上commit,push到U盘上;然后在另一台电脑上从U盘pull变动。即使没有中心服务器,不同版本库之间不同步的问题也可以解决!所以说,版本库可以位于任何地方。

至于协作的问题嘛,你可以指定一个版本库把它当作中心版本库就好啦!

  • 由于commit和update是本地操作,不同版本库之间数据传输是这两个命令

clone & merge原来可以这么常用

还在为增加一个有风险的新功能而困扰吗?没事,clone一下,你可以在新的代码库中,顺利了就push到原来的代码库中;搞砸了把新建的代码库删掉就好啦,对原先的代码库一点影响都没有!

除此之外,你还能在原来的代码库进行那些“相对安全”的更新。谁在开发的时候没有一点新想法但又不急于实现呢?所以clone一下,按部就班的开发和新想法开发可以同步进行。

Q: 不同版本库之间进行了不同的修改不就不同步了吗?再合并会不会……

A: DVCS的本质就在于不同步,就是因为不同代码库之间不需要同步,所以开发人员才有更大的灵活性对版本进行更加强的控制。只要是本地的操作都是可以退回的,所以上面说的误删trunk的悲剧完全可以避免。

不同步的代码合并时需要merge操作,大多数DVCS没有svn那么蠢,同一文件不同部分的改动不会被视作conflict。和svn完全相反,DVCS中merge is silent,只有真正发生冲突时,才会提示用户去resolve。所以,大多数情况下,即使代码库不同步,你也不会意识到合并时发生了merge!

你是可以后悔的,方式还不止一种

出问题了、越改越乱,想回到没改动前的状态?没事,revert一下。

忘了添加新增的文件到commit中?手贱commit了一下?没事,rollback一下再commit。

不知道哪个版本引入了bug?没事,bisect一下。

版本历史中有一个引入了bug的commit?没事,backout/revert一下再merge。*

版本树太乱了?没事,rebase一下。*

  • WARNING: 列出这两个指令只是为了证明你可以后悔,但这些不常用的指令一定要慎用

So, where to start?

DVCS有很多,但最流行的无疑是Git,接着是Mercurial。两者都是自由软件,也都是为了解决当时Linux内核版本控制工具缺失而产生的,前者的作者是Linus·你一定知道我是谁·Torvalds用C写的,后者则是Matt Mackall用Python写的。Git指令复杂,完全掌握需要耗费不少精力;Mercurial则相对小巧。不过大多数人平时也就用那么几个常用命令,所以初学的难度差不多。不过,从当前的形势上来看,学习Git前途无疑更加光明,由于GitHub的到来,Git已经成为social coding的事实标准。

这里有一个Git的教程,一个Mercurial的教程供读者参考。

Tips

要用版本控制,先有其Mindset

如果你没有版本控制上的需求,不要大张旗鼓进行版本控制。另外,不是说你在用版本控制工具就说明你真的在进行版本控制。每个命令的正确用途、多人开发时的配置等问题都需要关注。因为这部分不属于本文讨论范围,就不展开了。

最好不要将二进制文件纳入版本控制

虽然一些版本控制系统为二进制文件(即非文本文件)进行了优化,不过很大程度上,版本控制还是为源代码服务的,将二进制文件纳入版本控制容易影响效率。

一些编译产生的文件就更加不用说了。我一般的做法是这样的:

  1. Makefile是标准配置,这样编译或清洁工具一步就搞定了
  2. 配置文件不纳入版本控制*
  3. 数据库的初始数据dump成SQL文本文件
  • .ignore文件是好朋友。

总之,尽量保持你的代码库中只有源代码。

保持commit的完整性和独立性

什么叫完整性?就是说,无论你更新/退回到哪个版本,那个版本都至少是可编译和运行的。

如何保证完整性?1. 该增加的文件别忘了add,该删除的文件别忘了remove;2. 单元测试

什么叫独立性?就是说,你的一次commit只改动一处内容,比如重构了一类方法,新增了一个功能,改动了一个模块。为什么要保持独立性?如果一次commit包含了不同内容的变更,一旦出现bug,定位就更加困难;反之,如果一次commit只有一种变动,出现了bug,一旦定位到具体的一次commit,原因就显而易见了。

如何保证独立性?Mercurial中commit默认提交所有变动(新增/删除/修改),所以这部分overhead就在程序员这边,需要自己做好权衡。而Git中,每次commit都需要显式指定需要提交的部分,这对保证独立性非常有好处!