



   在几年前,“变更集”可是个时髦词汇。软件配置管理几大工具厂商争相宣布对它的支持。而如今的热门词汇是“分布式版本控制”。越来越多的软件开发团队在考虑迁移到分布式版本控制工具上。在这一章里,我们来看看,这些概念在本质上是什么,它究竟能带来什么好处,而工具能够提供什么样的支持。
一般来讲,我们应该将一件事情做完,再交活儿。干到一半的工作,对别人意义不大,甚至有负面影响。编程序、改代码也一样。比如,张大侠这次本该修改三个文件,来添加一个功能。如果他刚修改了一个文件的时候,可能软件产品整体编译还未通过,就交上去,那就会出问题。当王大侠和李大侠拿到张大侠的代码的时候,他们也编译不过去。等找到原因,他们会对张大侠怒目而视,说不定结下江湖恩怨。所以说,我们应该在完成一项工作任务后,或者至少是取得了阶段性的工作成果后,再公布于众,把对各个文件的修改变更作为一个集合一股脑地提交上去。这是变更集这个思想的重要理由之一。
    
     变更集
    
    (Change Set),又称变更包(Change Package),有时简称为变更(Change)
    
    。一个变更集,对应着一项完成了的工作任务。变更集通常有一个标题,甚至更多的描述性文字,用来大致说明这个变更集完成的工作任务。这可能是修复一个Bug,可能是小幅调整一个功能,也有可能是一项大的工作的分解,比如,为实现一个新特性(Feature),需要由几个开发人员协作完成,每个人分头完成自己的一个或几个变更集。总之,变更集通常对应于一个明确的工作任务,带有明确的目的,可以和其他变更集区分开。
   
从源代码实际变动的角度看,变更集是一个或多个文件上的一处或多处具体的代码改动的集合。也就是说,一个变更集涉及一个或多个文件,而每个文件的内容发生了变化,从修改前的版本变为修改后的版本(见图3-1)。
    图3-1 变更集
这样的一个一个变更集,累加在一起,实现了整个软件的从无到有,直到开发完成(见图3-2)。
    图3-2 变更集的累加
用变更集来记录软件开发活动,便于明确做了哪些工作、谁完成的、什么时候完成的等团队协作必然遇到的问题。这样,便于交流,也便于管理。看一下张大侠这周完成的变更集的列表,喔,他真的是劳苦功高啊。
当开发人员把程序送交测试团队测试的时候,测试人员通常会问,这一版,你们改动了什么内容,增加了什么功能,修复了哪些Bug?当程序发布给客户的时候,客户通常也会问相同的问题。如果是用变更集来记录软件开发的,那么,最近完成的变更集的列表,会对回答这类问题有很大的帮助。
而当我们在源代码改动的层面回顾工作的时候,同样能感受到变更集带来的好处。我们能够明确知道,某个Bug究竟是怎么修复的,某个新增加的功能,究竟对应于哪些文件的修改,修改了哪些内容。这样,如果怀疑以前的工作有疏漏,可以方便地追溯到过去的某个变更集——对代码的具体修改,看看是不是当时的改动有问题。如果你打算向编程高手学习,也一样——考察变更集各文件中代码内容的变化,可以体会高手编程时的良苦用心。
版本控制的重要目标是记录历史,提供参考,便于回退。变更集在这一点上也很有价值。相关的代码修改作为一个变更集,要么提交,要么不提交,要么回退,要么不回退,泾渭分明,绝不会出现中间状态,产生编译不通过之类的问题。
可以看出,项目管理人员、开发人员、测试人员和最终用户,都会从变更集中受益。
    注意,每个变更集所对应的工作量,不要太大,它所对应的源代码改动,不要太多。否则,不好查阅修改情况,也不好管理
    
    。如果要产生一个很大的变更集,那就要考虑把它划分成若干小一些的变更集,分别提交。另一方面,变更集的划分,也要避免没有必要的琐碎细节。这个火候如何把握?要从实践中不断获得经验。
   
在本节中,我们来看一看开发人员是怎样使用版本控制工具的。当然,这里说的是支持变更集的版本控制工具。
在有些工具中,在改动代码之前,开发人员需要首先进行一个操作,创建一个变更集。也就是说,告诉版本控制工具,你要开始一个新变更集的开发了。这时候,你可以大致描述一下这个变更集对应的任务,版本控制工具会帮你记录。而其他不少版本控制工具不要求这一步,相关信息可以在完成一个变更集之后,提交的时候再补录。
一般来说,在完成代码改动后,提交代码改动到版本库。但有的时候,开发人员可能有这样的需求——能不能帮我保存个中间版本呢?也就是说,在改代码的过程中,在尚未完成任务时,能不能把阶段性的工作成果保存一下?一些版本控制工具提供了这样的功能,在安全的地方(比如版本库里的某个隐秘地方)暂存(Stage)它。这样,既保存了版本,又不至于干扰到别人,将来版本历史也显得齐整。类似的方法还有一些,在此不一一介绍。
在这个工作任务完成后,提交相关的修改到版本库。在进行提交操作时,可以撰写或补充该变更集的任务描述。
从下载版本库里的源代码,到提交修改,全过程如图3-3所示。
[Subversion] 用相对简单的方法支持变更集。变更集有标题,这对应于变更集提交(Commit)时输入的注释。一个变更集,对应一次提交,也就是一次签入,不支持工作中文件的中间版本的保存。工作区中是一个未完成的变更集,完成后就提交。 [Git] 与Subversion相比,有更多的缓冲。首先,从工作区正式提交(commit命令)到本地私有版本库之前,可以先暂存(add命令)。其次,可以在本地版本库里累积几个变更集,一起提交(push命令)到服务器上的公共版本库,让别人看见。后者是分布式版本控制方法(第3.5节)带来的功能。当然,分布式版本控制方法也意味着,提交变复杂了,包括从工作区到本地私有版本库,再从本地私有版本库到服务器上的公共版本库这两步。
    
     [ClearCase UCM]
    
    用相对复杂的方法支持变更集。在ClearCase UCM里,变更集大致对应于活动(Activity)。活动有标题,在活动创建时输入。而每个文件的修改又可以有注释。支持工作中文件中间版本的保存:工作区后面,对应着一个开发人员私有的流(Stream)。中间版本和最终版本都保存(checkin命令)在私有流上,不会给别人捣乱。而把变更集从私有流提交(deliver命令)到公共流后,大家就都能看到啦
    
    。总之,提交包括两步,从工作区到私有流,再从私有流到公共流。流这个概念,类似于分支,分支的概念在第7章讲。
   
    图3-3 运用变更集
完成一个变更集的时间,可能很短,可能比较长。修复一个Bug,可能几分钟就搞定了,而开发一个新功能,可能要几天的时间。在你埋头工作的时候,版本库可能已经发生了很大的变化。在自己工作于一个变更集的过程中,别人会不会向版本库提交了代码?如若这样,自己手上的代码会不会过时,不能和别人的最新代码共同工作了?自己的变更集开发持续时间越长,这样的危险越明显。还记得讲过的Timer的故事么?即使使用了版本控制工具,如果不及时进行更新操作,这样的事情同样可能发生。
    为了保证你的工作成果能融入到当前的整个软件产品里,你需要适时更新你的工作区,得到版本库里的最新版本。让你的工作,你的改动,是在最新版本之上
    
    (见图3-4)。
   
[Subversion] 更新就叫更新。
[Git] 一个pull命令就可以完成这里所说的更新概念,但在原理上其实有三件事:从服务器上的远程版本库同步到本地版本库中的远程分支;从远程分支合并到本地分支;让工作区反映本地分支上的情况。
[ClearCase UCM] 这里所说的更新,大致对应于ClearCase UCM里变基(Rebase)私有流,并相应地更新私有流对应的工作区。这里的变基是指用公共流的一个基线来更新私有流的起点,因此改变了工作区的基础。嗯,ClearCase确实有点复杂。
更新可能会带来合并的情况,如果你所修改的代码本身,在版本库里已经有了别人提交的新版本。别担心,如上一章所述,这样的合并通常是自动化的,不会牵扯你太多精力。
那么,什么时候需要更新呢?首先,如上一节所述,在变更集开始之前,可能需要更新。以保证你是基于版本库里比较新的版本开始工作的,不要在开始的时候就落伍。这时候的更新,建立了你的关于这个变更集的初始工作环境。当然,如果你在开始新变更集之前刚做过更新,那就不是很有必要再更新一次了。
    图3-4 更新工作区
其次,在变更集完成的过程中,可能需要更新,以保证能跟上时代的步伐。你埋头工作的时间越长,这样的需求就会越明显。当然,过于频繁的更新也没有必要,甚至有害。每次更新后,大概你需要重新编译,这可能很耗费时间。而且,版本库里的最新代码,难免有编译不通过,程序转不起来的时候。你不想每次都赶上吧?
最后,在变更集完成,即将提交的时候,最好做一次更新,并且测一下。以保证你新写的代码,是可以与别人的代码一同工作的,提交上去以后,是可以让其他人放心使用的。我们将在第4章里详细讲关于质量的事。另一方面,不少版本控制工具要求你更新到最新(或足够新)的版本之后才能提交,倒不是因为质量,而是因为工具本身的工作原理。
[Subversion] 在Subversion的工作原理中经常需要提交前的更新。由于合并必须发生在工作区里,因此一旦有文件(可能)需要合并,就必须先更新再提交。
[Git] 在Git的工作原理中也经常需要提交前的更新。在Git中,改动必须基于服务器上的远程版本库中的最新版本,否则无法从本地私有版本库提交到服务器上的公共版本库。
[ClearCase UCM] 提交到公共流之前,不需要因为ClearCase UCM的工作原理本身的缘故而更新私有流及对应的工作区。因为除了开发人员自己的私有流有对应的工作区,公共流也有对应的工作区,提交到公共流时,可以在那儿完成代码合并工作。
在很久以前,还没有变更集这个概念的时候,版本控制工具是以文件为单位进行管理的。第2.4节里介绍的签出和签入其实是那个时代遗留下来的概念。在那个时代,签出是告诉版本控制工具,我要修改某个文件啦。相对应的,签入是告诉版本控制工具,修改完成,请保存该文件的新版本。如今,签入演变成了提交,提交一个变更集到版本库。而签出演变成把所有的源代码,而非单个文件,从版本库里拿出来,填充工作区。
在那个以文件为单位进行管理的古老时代,每个文件有它的一个又一个版本,形成版本演进的历史。然而要注意,不光每个文件需要保存版本,一个程序的所有的源代码作为整体,也要记录和保存版本,这些版本也形成版本演进的历史。因为大家关心:测试测的是哪个版本;交给用户的是哪个版本;这个版本测没测过;用户报告了一个Bug,是指的哪个版本上的Bug,等等。
那么,如何记录源代码作为整体的版本呢?在早期的版本控制工具中,是这么一个思路:若干文件组成了产品,每个文件的特定版本组合在一起,是产品的特定版本。所以,记录产品的某个版本,只需要记录这个版本,是由哪些文件、具体哪个版本组成的。在操作上,可以在相关文件的特定版本上,打个 标签 (Label / Tag),标签的名字,就是整体版本的名字。这样,将来搜索所有的以这个整体版本命名的标签,就能找到这个整体版本对应的所有源文件的正确版本,也就找到了这个整体版本对应的全部源代码(见图3-5)。
    图3-5 文件版本组成整体版本
这招不错,解决了记录整体版本的问题。但它也有一些弱点。比如,在每个文件上都要打标签,创建一个包含众多文件的程序的整体版本就可能会比较费时间。另一方面,不同整体版本的标签很可能打在某个文件的相同的版本上。这个版本上,可能挂着几个标签,可能挂着几十个标签,也可能挂着几百个。
    当代版本控制工具不再使用上述方法记录源代码整体版本
    
    。它们用的是另外一套思路,从另外一个视角看问题。还记得讲变更集的时候我们用的图吗?(重画于图3-6)
   
    图3-6 变更集的累加
每个变更集提交到版本库后,自然而然地形成了产品源代码的新的整体版本。一个又一个变更集提交上来,产品向前不断演进,或者说,不断形成新的整体版本。
版本控制工具通常会给这样自然形成的整体版本编号,比如1、2、3……8573、8574、8575……或者更神奇一点,49A36B2、E331D55、8B52E13、94CC76F……
好像这些编号没有什么实际含义,不太好记……想好记一点也简单:给某个自然形成的整体版本本身打个标签,或者说起个别名(Alias)。比如,告诉版本控制工具,49A36B2这个版本也叫Rel2.3。
    遥想当年,可能需要在成百上千个文件上打标签,可能耗时数十分钟或更长。如今,只需要打一个标签,通常秒杀
    
    。
   
[Subversion] 用自然数列来为自然形成的整体版本编号。如果想有一个有意义的名字,那就用copy命令,把某个产品对应的源代码从版本库中的一个目录下复制到另一个比如叫Rel2.3的目录下。当然,这里说的复制,并不是真的复制,是逻辑上的,类似于链接(Link),因此是秒杀。
[Git] 用一个比较长的随机数(被称做SHA1)来为自然形成的整体版本编号。如果想有一个有意义的名字,那就贴个标签(Tag)。没错,秒杀。
[ClearCase] Base ClearCase是以文件为单位进行版本管理的。因此打标签可能会成为一个费时间的工作。当然,使用一些技巧,比如增量地打标签(只在自上次整体版本以来有变化的文件上打标签),可以改善性能,但同时也增加了复杂性。ClearCase UCM是基于Base ClearCase的封装,它使用类似的技巧来改善性能,常能在几秒钟之内完成打标签的工作。
在这样的解决方案下,单个文件的版本怎么表达呢?当你询问版本控制工具的时候,它会告诉你,该文件在整体版本8573中被修改了,相关的变更集里记录着,这是为了不仅显示GIF图片而且显示JPG图片。后来该文件在整体版本37420中又被修改了,相关的变更集里记录着,这是为了把刷新频率提高一倍。
还记得我们讲过的星形结构(重画于图3-7)那张图吗?
    图3-7 星形结构
在前面的描述里,我们给出了这样一种方式:星形结构中心那个点,是版本库,存储着源代码的所有版本,放在服务器上。周围的那些点,是工作区,里面塞的是代码的某个历史断面及正在进行的少量修改,存储在每个开发人员的计算机上。从周围到中间,是提交;从中间到周围,是更新。这是典型的集中式版本控制工具的工作方式。
然而,历史的车轮滚滚向前,一种新的工作方式崭露头角: 分布式版本控制 (Distributed Version Control)方式。在这种新的方式中,在星形结构中心那个点,仍然是一个版本库公共的版本库,存储在服务器上。而星形结构周边的那些点,在每个开发人员的计算机上,不再仅仅是工作区,即代码的某个历史断面,而是还有一个完整的私有版本库。这个私有版本库的内容与公共版本库存储的内容几乎一样。在开发人员看来,这个私有版本库是本地版本库,而服务器上的公共版本库是远程版本库。
开发人员在绝大多数时间里,只和本地的私有版本库打交道:把私有版本库中的代码拿到本地的工作区,修改代码后,放到私有版本库保存。查看代码历史,查看各种信息时,基本上只与本地私有版本库打交道。而在需要与其他人交换信息时,就在服务器上的公共版本库和本地的私有版本库之间进行同步操作:从公共版本库到私有版本库,或者反方向。
这种新方式的优点在哪里呢?主要有两个优点:首先,对个人外出工作、工程团队在现场修改源代码、项目研发分布在多个地点等多地点开发场景有很好的支持。类似于在一个站点内部,每个程序员可以有自己的私有版本库,与公共版本库相互独立并相互同步,不同站点可以有该站点自己的公共版本库,这些公共版本库之间相互独立并相互同步。此外,还可以考虑私有版本库与另一个站点的公共版本库直接同步。第16章将会详细介绍多地点开发的解决方案,分布式版本控制工具对此有很好的支持。
这种新方式的第二个优点是,版本控制工具具有更好的性能,可以支持更大规模的研发。在集中方式下,大量的操作都需要与公共版本库打交道,它的负荷很重,性能容易出现问题。而在分布方式下,只有很少操作需要与公共版本库打交道,通信量不大,因此开发人员会觉得版本控制工具的性能很好。
分布式版本控制还有很多优点。比如,由于每个开发人员都有自己的版本库,他就有机会改写历史,把本地版本库中的历史记录整理得清楚些,好看些,再同步到公共版本库。限于篇幅,这里不再细讲。
对分布式版本控制了解不多的读者可能会对它有些疑虑:每个开发人员都有一个本地版本库,那么会不会很占磁盘空间?每个开发人员都有一个版本库,那么权限是不是很难控制,源代码容易外泄?
对于第一个问题,由于采用增量存储和压缩算法,版本库的尺寸可能比工作区还小。对于第二个问题,如果“坏人”没有公共版本库的读权限,他就无法基于公共版本库建立本地版本库并获得源代码,就好像在集中式版本控制工具中,如果他没有公共版本库的读权限,就无法基于公共版本库建立本地工作区并获得源代码。而如果他有公共版本库的读权限,那么无论是分布式版本控制工具还是集中式版本控制工具,他都能取得源代码。因此,集中式版本控制工具并不比分布式版本控制工具更安全。
[Git] Git是分布式版本控制系统的典型代表。在拥有众多的优点的同时,大概唯一的弱点是,有点复杂,需要多花一些时间来学习。
在上一章的基础上,本章介绍了当代版本控制工具中的三个常见重要特性:变更集(第3.1至3.3节)、整体版本(第3.4节)、分布式版本控制(第3.5节)。当然,版本控制工具的功能不止这些。比如分支,就还没有介绍。第7章将介绍分支概念和典型用法。