UML软件工程组织

 

 

版本控制与CVS
 
2007-12-29 来源:chinaunix.net
 
CVS与版本控制
1.版本控制
2.CVS的历史
3.CVS的特点
4.CVS的术语
5.CVS的目录文件结构
6.CVS的命令及用法
7.结束语

参考:
1.《谈谈CVS的由来与发展》 北京信息工程学院 李宁 王慧思
2.CVS wiki
3.CVSNT wiki
4.CVS Manual
5.CVS入门 卧龙小三

我们假设有一个叫HappyBirthday的项目,由wuya提出,这个项目的功能就是根据日期提醒用户其亲人及好朋友的生日,并自动发送电子贺卡,整个项目的开发和维护现在都由wuya一个人来做。

由于这个wuya是菜鸟,所以他会犯错,而且会犯很严重的错误。本来HappyBirthday项目有一个很不错的功能,就是根据过生日的主角的兴趣爱好(当然,这得用户告诉计算机)自动从网上商城选择推荐生日礼物。wuya同学实现了对生日主角的兴趣爱好的分析部分,但在选择生日礼物时遇到了困难,觉得自己唯有写一个搜索引擎方能搞定,于是心灰意冷,一冲动就把这个功能全删了(所以说冲动是魔鬼)。过了一段时间,wuya发现其实不需要自己写搜索部分,有现在的可以用,肠子都悔青了,可惜代码已经删了,于是他想假如时间可以倒流。

过了一段时间,HappyBirthday项目有了新的成员参加,新成员竟然自称Arthur King,我们暂时称之为arthur吧。

人多力量大,项目的进度快了不少,但人多也带了一个问题,wuya和authur的代码会有小的冲突,比如wuya同学设定HappyBirthday更新的时间为自己的生日(一年更新一次,可真够长的),但arthur同学不满,偷偷改成了自己的生日,作为项目发起人的wuya同学却不知道,想象有一天他发现事实的真相可能会冲动作出一些破坏和谐的事情,所以最好的办法还是提前解决冲突,项目改为一年更新两次。

有一天,wuya同学发现一个叫版本控制的东西,可以解决上述问题,我们就跟着他来看看解决之道。

1.版本控制

软件的开发和维护过程,离不开版本控制。

版本控制(Revision Control)是一种软件工程技巧,籍以在开发的过程中,确保由不同人所编辑的同一档案都得到更新。

版本控制透过文档控制(Documentation Control)记录程序各个模组的改动,并为每次发动编上序号。这种方法是维护工程图的标准做法,它伴随着工程图的诞生一直到图的定型。一种简单的版本控制形式,如赋给图的初版一个版本等级"A",当做了第一次改变后,版本等级改为"B",以此类推。

对于同一份文件,我们经常需要按不同的版本进行归档,或者从资料库里找出反映文件修改历史的不同版本。这样一方面可以使各个阶段的代码和文档变得井井有序,另一方面可以在当前版本出现问题时,找回先前的版本。当然人们希望的还不止这些,人们希望可以规定谁在什么时候可以如何存取某个版本的内容;也希望差异不大的版本按增量的方式存成一个文件,以节省存储空间;还希望某两个版本的文件可以合二为一;因此,我们需要版本控制工具。

2.CVS的历史


早在CVS之前,就出现过对变化前后的文件进行比较,并根据异同形成"补丁"(Patch)的工具。例如Unix上使用的Diff和Patch,这两个工具对程序代码的传播和维护起到了重要的作用。但是,后来出现许多要求Diff和Patch都显得无能为力,例如发现修补出错而需要退回到以前未修改的状态等。这要求有一个保存项目历史纪录的系统。

当初具备这个功能的工具是SCCS(Source Code Control System),是贝尔实验室的Marc Rochkind在1972年写成的。SCCS是一种基本的源文件版本控制工具,适用于正文文件的版本维护。它基于单一文件的版本控制,代码库和要维护的文件通常在同一目录下。SCCS有一个专门的SCCS文件保留源文件的各编码版本。其中记录了足够的信息来恢复 一个版本,并记录了谁对文件有修改、有版本锁的功能。SCCS是AT&T Unix发行版的一部分。

然后自由软件项目最终选择了Walter F. Tichy的版本控制系统RCS来满足他们的需要。RCSdSCCS的基础上加以改进,界面也更加友好,属BSD Unix发行版的一部分。它可以追踪文件的改变,在工作组中对文件的共享和访问进行控制,通常用于维护源代码;也能追踪文件的历史。RCS包含一套命令,用于设置RCS源码库中的文件属性、检入检出文件、清除文件、比较修订版本,以及合并文件等。由RCStpgjr文件可以是纯文件文件,也可以是二进制文件。

然后,RCS仍存在几个重要缺陷,其中最重要的是由于使用单一目录控制与档案锁,无法让多个编程人员同时开发。因为RCS本身不是针对网络环境的,开发者只能在RCS代码库所在的机器上工作。这些缺点后来在CVS中都得到了改进。

下面三段话摘自Dick Grune的主页:
CVS的诞生是为了方便我和我的学生Eric Baalbergen和Maarten Waage在ACK C编译器项目上协同工作,因为我们三个人的工作日程安排大相径庭(一个是典型的朝九晚五,一个是不确定,而我只能在晚上有时间做这个项目)。这个项目从1984年6月持续到1985年8月。我们刚开始称CVS为cmt,因为它允许我们独立地提交不同的版本(commit versions independently)。

当Baalbergen-Waage项目结束以后,我开始着手整理cmt的shell脚本,因为我发现它好像非常有用,我改称它为CVS。

我在1986年6月23日把改进后的脚本发布到了comp.sources.unix上。

1989年3月,Brian Berlinor用C语言重新设计并编写了CVS的代码。后来,Jett Polk帮助Brian完成了CVS模型设计,增加了一些关键特性。1993年前后,Jim Kingdon最终将CVS设计成基于网络的平台,开发者们可以从Internet任何地方获得程序源代码。

3.CVS的特点

CVS使用CS结构:服务器负责存储项目的所有版本和修改历史,客户端可以连接到服务器,check out整个项目到本机,然后在本机上进行修改,修改完成后可以存入(check in)到服务器上。通常,客户端通过LAN或Internet连接,但如果要追踪版本历史的项目只有一位本地开发者,客户端和服务器也可以在同一台物理机器上。但如果要追踪版本历史的项目只有一位本地开发者,服务器软件通常运行在Unix上(CVSNT可以运行在不同的Windows和Unix上,它是从CVS中分出去的另一个独立的项目),CVS客户端一般可以在任何主流的操作系统平台运行。

多个开发者可以并行开发项目,每一个人编辑自己从服务器取出(check out)到本机的工作版本(working copy),然后把修改的结果存入(check in)到主机。为了避免开发者做重复的工作,服务器只接收对一个文件最新版本的更新。所以,开发者应该保证他们的工作目录经常更新,以拥有别的开发者最新的修改。整个过程可以由CVS自动完成,需要人工介入的情况只有两种:一是存入(check in)过程中出现冲突(conflicts),一种是存入服务器上不存在的本地文件。

每当一个存入(check in)操作成功时,所以涉及到的文件的版本号会自动加1,同时CVS服务器会在日志中记录存入的日期、作者的名字、及存入用户对于此次存入内容的描述。CVS也可以在存入(check in)时执行用户定义的外部的日志处理脚本。可以通过CVS的loginfo文件来安装这些脚本。

客户端通常可以比较算法、请求整个修改历史,或者取出(check out)某个固定日期或版本号的历史快照。许多开源项目允许匿名读取(anonymous read access),这个特点首先是OpenBSD拥有的。这即意味着可以使用空密码或者公开的密码从服务器取出(check out)源码并比较版本间的差别,唯有需要存入(check in)修改部分的人拥有帐号密码。

客户端可以使用"更新"(update)命令来更新他们本地的工作目录,需要更新的文件会被自动更新,整个项目不会被重新下载。

CVS可以维护同一个项目的不同"分支"(branches)。比如一个正式版本(released version)可以形成一个分支,这个分支现在只修正bug即可;还有一个当前正在开发的版本,可以形成另一个分支,较之前的版本有了大的改变和一些新的特征。

CVS使用delta算法来更有效的存储同一文件的不同版本。这种实现比较偏爱多行文件(通常是文本文件)。

4.CVS的术语

一些相关的源码组合在一起,在CVS中称之为一个模块(module)。CVS服务器把它负责管理的模块存储在仓库(repository)中。获得一个模块的拷贝称之为取出(check out)。被取出(check out)的文件称之为一个工作集(working copy)。对工作集所做的修改要通过存入(commit)反映到服务器的仓库(repository)中。从仓库把最新的修改下载到本地的工作集中称之为更新(update)。

5.CVS的目录结构

5.1仓库(repository)的目录结构
仓库中全部目录结构完全对应于工作目录(working directory)中的目录结构。比如,假设仓库在/usr/local/HappyBirthday
下面是一个可能的目录结构
/usr
  |
  +--local
  |    |
  |    +--HappyBirthday
  |    |     |
  |    |     +--CVSROOT
             |        (管理文件)
             |
             +--conf.d
             |   |
             |   +--configure.example
             |   |
             |   +--configure
             |   |
             |   +--test.pl
             |
             +--other HappyBirthday modules

与目录一直的是版本控制下的每个文件的历史文件(history files)。这些文件的是名称是在对应的文件名后面加上",v"。下面是仓库中conf.d目录的可能情况:
$CVSROOT
   |
   +--conf.d
   |     |
   |     +--configure.examples,v
   |     +--configure,v
   |     +--test.pl,v
历史文件中有足够的信息来重新创建文件的任何一个版本,另外历史文件还记录了所有提交信息的日志,其中包括提交者的用户名、提交时间等。这些历史文件就是以前的RCS files,因为第一个以这种文件格式存储来进行版本控制的系统是RCS。这种文件格式的应用已经非常广泛了,除了CVS和RCS之外还有很多其它的版本控制系统,它们至少可以导入这种格式的历史文件。

5.2工作目录(working directory)的目录结构
CVS目录包含以下若干个文件(这里只列出最常用的)。文件以符合当前系统规范的文本形式保存,这意味着在拥有不同规范的系统之间工作目录不可移植。这是故意为之,所以理论上CVS的管理文件在系统之间无法移植。

Root
    该文件包含当前CVS根目录。

Repository
    该文件包含当前目录对应的仓库里的目录。该路径可以是绝对路径也可以是相对路径;从1.3版本开始,CVS可以读取这两种格式的路径。相对路径名相对于要目录并且容易解析,但是绝对路径更通用。
例如:执行以下命令后
    cvs -d :local:/usr/local/HappyBirthday checkout conf.d
Root应该包含
    :local:/usr/local/HappyBirthday
Repository包含
    /usr/local/HappyBirthday/conf.d
    或
    conf.d
如果特定的工作目录不与仓库的目录相一致,Repository应当包含CVSROOT/Emptydir。

Entries
    该文件列出了工作目录中的文件和子目录。每一行的第一个字符代表该行的类别。为了保证未来版本的可扩展性,如果一行的第一个不可识别,读取文件的程序默认忽略该行。
如果第一个是"/",则格式如下:
    /name/revision/timestamp[+conflict]/option/tagdate
name 是目录中文件的名字。
revision 是正在编辑的文件派生的修订版本号,'0' 代表新添加的文件,'-'revision 代表删除的文件。
timestamp 为时间戳,表示 cvs 创建文件的时间;如果时间戳和文件修改的时间不一致,意味着文件已经被修改过。时间戳以 ISO 标准的 C 函数 asctime() 的格式存储(例如,'Sun Apr 7 01:29:26 1996')。
conflict 表示是否存在版本冲突。如果 conflict 和文件实际的修改时间相同表示用户还没有解决版本冲突问题。
options 包含可选项(例如对二进制文件可以使用'-kb')。
tagdate 含有'T'后面跟标签名,或'D'表示日期,后面跟是sticky标签或日期。

6.CVS的基本命令及其用法
CVS的基本命令格式如下:
    cvs [cvs_options] command [command_options] [command_args]
经常用到的命令有:login、logout、commit、checkout、update、status、diff、log、add、remove等,大部分命令都有简写。

下面我们仍以HappyBirthday为例,来看看如何使用这些命令。

6.1建立仓库
首先,我们得建立一个仓库,仓库建立在CVS服务器上,相对于CVS客户端,服务器可以是本地的,也可以是远程的。这里为了简单,我们决定使用本地仓库。假设此时HappyBirthday项目的开发人员又变成了wuya同学一个人了。

建立仓库文件夹。
    $mkdir /usr/local/HappyBirthday

添加一个用户组happy_birthday,处于这个组中的成员都可以存取仓库。把wuya添加到这个用户组中。
    $groupadd happy_birthday
    $gpasswd -a wuya happy_birthday

切换到用户wuya,看看他所属的组有哪些。
    $su wuya
    $groups
我们可能会看到happy_birthday组并没有被groups命令列出,所以我们需要重新登陆一下。

设置项目文件夹的组和权限。
    $chmod 2775 /usr/local/HappyBirthday
    $chgrp happy_birthday /usr/local/HappyBirthday
    $ls -ld /usr/local/HappyBirthday

初始化仓库
    $cvs -d /usr/local/HappyBirthday init
这里给出了仓库的绝对路径,也可以不给出,但需要设置CVSROOT环境变量,可以在~/.bash_profile中添加
    export CVSROOT=/usr/local/HappyBirthday

6.2建立工作目录
建立一个将来工作的目录,可以随便建立在任何你习惯的地方。
    $mkdir /home/wuya/Projects/HappyBirthday

因为整个项目分成好几个模块,各司其职也方便管理,现在我们先创建一个管理模块(关于模块的定义,术语解释中有给出),其中是项目的配置文件。
    $cd /home/wuya/Projects/HappyBirthday
    $mkdir conf.d
    $touch configure.example

我们这里是在一个项目HappyBirthday中创建不同的模块,因为在建立仓库时我们就打算这个仓库只存储HappyBirthday项目的内容,所以仓库的名字就叫/usr/local/HappyBirthday。其实也可以在同一个仓库下面管理多个项目。这些其实都只是逻辑上的概念了。

在本地创建了conf.d模块,必须更新到仓库里。
    $cd conf.d
    $cvs -d /usr/local/HappyBirthday import -m "configuration files" conf.d wuya start
这样仓库里面也就有了conf.d配置模块的目录结构。目录的结构就是前面讲过的结构。

6.3取出模块
虽然现在我们的HappyBirthday项目中空空如也,就一个conf.d/configure.example,但是这也足以说明问题了。
先装模作样的取出conf.d模块(这里的模块概念是个逻辑概念)。
    $cvs -d /usr/local/HappyBirthday checkout conf.d
这时,我们就从仓库中取出了最新的conf.d模块。

感觉每次都指出仓库太麻烦了,可以在~/.bashrc中定义CVSROOT
    export CVSROOT="/usr/local/HappyBirthday"

6.4修改程序,更新仓库
我们先看看当前唯一的文件conf.d/configure.example状态
    $cvs status conf.d
显示结果如下
    cvs status: Examining conf.d
    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.1.1.1 2007-07-16 14:10:11 +0800
       Repository revision: 1.1.1.1 /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   6007469b0bc34567
       Sticky Tag:          (none)
       Sticky Date:         (none)
       Sticky Options:      (none)

下面我们来修改下configure.example的内容
    $vi conf.d/configure.example
输入
    #This is a configuration file
:wq保存退出vi。

再看看文件当前的状态有无变化。
    $cvs status conf.d/configure.example
    ===================================================================
    File: configure.example Status: Locally Modified

        Working revision:    1.1.1.1 2007-07-16 14:10:11 +0800
        Repository revision: 1.1.1.1 /usr/local/HappyBirthday/conf.d/configure.example,v
        Commit Identifier:   6007469b0bc34567
        Sticky Tag:          (none)
        Sticky Date:         (none)
        Sticky Options:      (none)
注意这里的状态由前面的"Update-to-date"变成了"Locally Modified"。

下面我们把修改结果提交给仓库。
    $cvs commit -m "增加了一行注释" conf.d/configure.example
    /usr/local/HappyBirthday/conf.d/configure.example,v  <--  conf.d/configure.example
    new revision: 1.2; previous revision: 1.1
我们看到configure.example已经成功存入仓库,版本号由原来的1.1变为1.2。注意,这里的"-m"后面是关于此次提交的描述,如果上面命令为
    $cvs commit conf.d/configure.example
就会打开默认的编辑器来要求你输入描述内容。

查看一下当前的configure.example状态
    $cvs status conf.d/configure.example
    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.2     2007-07-16 14:53:32 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   60b5469b16fa4567
       Sticky Tag:          (none)
       Sticky Date:         (none)
       Sticky Options:      (none)

6.5添加新的文件
现在我们决定开始写一个正式的配置文件。
    $touch conf.d/configure
    $vi conf.d/configure
在文件中写入
    #This is empty now.
:wq保存退出vi。

我们已经知道了如何把修改结果提交给仓库,但相同的动作再作一次却对新文件没有作用。因为仓库中根本没有configure这个文件的存在,自然也不会对其进行更新了。所以我们先得把新文件添加到仓库中。
    $cvs add conf.d/configure
    cvs add: scheduling file `conf.d/configure' for addition
    cvs add: use `cvs commit' to add this file permanently
根据提示,我们还需要作commit动作。
    $cvs commit -m "添加conf.d/configure" conf.d/configure
    /usr/local/HappyBirthday/conf.d/configure,v  <--  conf.d/configure
    initial revision: 1.1
可以看到,configure已经被成为添加到仓库中,版本为1.1。

我们再添加一个文件,方便后面删除。
    $touch conf.d/test.pl
    $cvs add conf.d/test.pl
    $cvs ci -m "添加conf.d/test.pl" conf.d/test.pl

6.6 删除文件
现在我们又不想要之前添加的test.pl了,如何删除呢?
    $rm conf.d/test.pl
    $cvs remove test.pl
    cvs remove: scheduling `conf.d/test.pl' for removal
    cvs remove: use `cvs commit' to remove this file permanently
根据命令结果
    $cvs ci -m "删除conf.d/test.pl" conf.d/test.pl
    /usr/local/HappyBirthday/conf.d/test.pl,v  <--  conf.d/test.pl
    new revision: delete; previous revision: 1.1
虽然在commit之前,本地的test.pl已经被删除了,但仓库并不知道,所以变化必须提交到仓库。

6.7 解决冲突
之前的代码都是wuya一个人在维护,所以代码的作者全是他,不存在冲突的问题。可是项目中忽然又多了一个人zhuqin同学,他对项目中的文件作了一些自己的修改。下面全是zhuqin的动作。
    $cvs co conf.d
    $vi conf.d/confiugre
并在其中添加
    zhuqin say hello
然后把结果提交到仓库
    $cvs -d /usr/local/HappyBirthday ci -m "更新conf.d/configure" conf.d/configure
    /usr/local/HappyBirthday/conf.d/configure,v  <--  conf.d/configure
    new revision: 1.2; previous revision: 1.1

我们这里,zhuqin与wuya是工作于同一台物理机器上的,呵呵。实际项目中这种情况比较少见。

wuya同学并不知道zhuqin对conf.d/configure所做的修改,他编辑自己工作目录下的conf.d/configure
    $vi conf.d/configure
在其中添加
    wuya say hello
然后提交给仓库
    $cvs ci -m "wuya更新conf.d/confiugre" conf.d/confiugre
却得到如下结果
    cvs commit: Up-to-date check failed for `conf.d/configure'
    cvs [commit aborted]: correct above errors first!

发生了什么事,wuya赶快查看configure文件的状态
    $cvs status conf.d/configure
    ===================================================================
    File: configure         Status: Needs Merge

       Working revision:    1.1     2007-07-16 15:11:49 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   617e469b251a4567
       Sticky Tag:          (none)
       Sticky Date:         (none)
       Sticky Options:      (none)
我们看到,文件当前的状态为"Needs Merge",说明当前wuya手中的1.1版本的文件与仓库中1.2版本的文件有冲突。

wuya同学使用diff命令来看到底冲突何在
    $cvs diff -c conf.d/configure
    Index: conf.d/configure
    ===================================================================
    RCS file: /usr/local/HappyBirthday/conf.d/configure,v
    retrieving revision 1.1
    diff -c -r1.1 configure
    *** conf.d/configure    16 Jul 2007 07:15:41 -0000      1.1
    --- conf.d/configure    16 Jul 2007 08:02:46 -0000
    ***************
    *** 1 ****
    --- 1,2 ----
      #This is empty now.
    + wuya say hello
"+"表示wuya手中的文件仓与库中的文件在这一行发生了冲突。

wuya赶快执行update命令,获得仓库中的最新版本。
    $cvs update conf.d/configure
执行结果
    RCS file: /usr/local/HappyBirthday/conf.d/configure,v
    retrieving revision 1.1
    retrieving revision 1.2
    Merging differences between 1.1 and 1.2 into configure
    rcsmerge: warning: conflicts during merge
    cvs update: conflicts found in conf.d/configure
    C conf.d/configure

此时,conf.d/configure的内容变为
    #This is empty now.
    <<<<<<< configure   <------ wuya的configure
    wuya say hello      <------ wuya修改的部分
    =======
    zhuqin say hello    <------ zhuqin修改的部分
    >>>>>>> 1.2         <------ 仓库中的1.2版本

既然知道了冲突在哪里,wuya就和zhuqin磋商了一下,最终决定configure文件内容如下所示
    #This is empty now
    wuya and zhuqin say hello

冲突解决后,wuya把新的configure文件提交到仓库中。
    $cvs ci -m "已经解决和zhuqin的冲突" conf.d/configure
    /usr/local/HappyBirthday/conf.d/configure,v  <--  conf.d/configure
    new revision: 1.3; previous revision: 1.2
此时仓库中的configure文件版本变为1.3,这个版本解决了wuya与zhuqin之间的冲突。

6.8 查询历史
zhuqin刚刚加入HappyBirthday项目,想看看项目的历史,想了解项目中文件改动的纪录,于是使用log命令查询。
    $cvs log
于是得到下面的内容
    cvs log: Logging conf.d

    RCS file: /usr/local/HappyBirthday/conf.d/configure,v
    Working file: conf.d/configure
    head: 1.3
    branch:
    locks: strict
    access list:
    symbolic names:
    keyword substitution: kv
    total revisions: 3;     selected revisions: 3
    description:
    ----------------------------
    revision 1.3
    date: 2007-07-16 16:23:57 +0800;  author: wuya;  state: Exp;  lines: +1 -1;  commitid: 61c4469b2b1d4567;
    已经解决和zhuqin的冲突
    ----------------------------
    revision 1.2
    date: 2007-07-16 15:58:52 +0800;  author: zhuqin;  state: Exp;  lines: +1 -0;  commitid: 617e469b251a4567;
    更新conf.d/configure
    ----------------------------
    revision 1.1
    date: 2007-07-16 15:15:41 +0800;  author: wuya;  state: Exp;  commitid: 60e6469b1b1d4567;
    添加conf.d/configure
    ======================================================================
    RCS file: /usr/local/HappyBirthday/conf.d/configure.example,v
    Working file: conf.d/configure.example
    head: 1.2
    branch:
    locks: strict
    access list:
    symbolic names:
        start: 1.1.1.1
        wuya: 1.1.1
    keyword substitution: kv
    total revisions: 3;     selected revisions: 3
    description:
    ----------------------------
    revision 1.2
    date: 2007-07-16 14:58:20 +0800;  author: wuya;  state: Exp;  lines: +1 -0;  commitid: 60b5469b16fa4567;
    增加了一行注释
    ----------------------------
    revision 1.1
    date: 2007-07-16 14:10:11 +0800;  author: wuya;  state: Exp;  commitid: 6007469b0bc34567;
    branches:  1.1.1;
    Initial revision
    ----------------------------
    revision 1.1.1.1
    date: 2007-07-16 14:10:11 +0800;  author: wuya;  state: Exp;  lines: +0 -0;  commitid: 6007469b0bc34567;
    configureation module
    =======================================================================
    RCS file: /usr/local/HappyBirthday/conf.d/Attic/test.pl,v
    Working file: conf.d/test.pl
    head: 1.2
    branch:
    locks: strict
    access list:
    symbolic names:
    keyword substitution: kv
    total revisions: 2;     selected revisions: 2
    description:
    ----------------------------
    revision 1.2
    date: 2007-07-16 15:44:08 +0800;  author: wuya;  state: dead;  lines: +0 -0;  commitid: 612d469b21c84567;
    删除conf.d/test.pl
    ----------------------------
    revision 1.1
    date: 2007-07-16 15:41:26 +0800;  author: wuya;  state: Exp;  commitid: 6124469b21264567;
    添加conf.d/test.pl
    ======================================================================
可以看到,我们曾经做过的所有动作都被详细地记录了下来。

6.9 分支
假设我们的HappyBirthday当前版本为A,然后我们在其上添加了许多新的功能,形成了版本B,虽然功能强大,可惜Bug多多,甚至影响到版本A的功能,于是我们开始怀念以前版本A的日子,我们想到从版本A到版本B我们犯了若干错误,如果没有这些错误,那现在的版本B就是完美的。

OK,CVS可以帮助你回到美好的版本A的日子。

要回到版本A,就必须知道什么是版本A,即各文件处于什么状态下的集合体才能称之为版本A。一个直观的想法就是在曾经版本A出现的时刻,我们把它标记下来,我们肯定得告诉系统,这个状态就是版本A。然后某一天,我们告诉系统,把我当时定义的那个版本A的状态重新还原出来,于是就回到了版本A,CVS正是这样做的。

CVS提供了两种不同的方法来获得项目不同的历史状态,一种是依时间来取,一种是依标记来取。

6.9.1
我们先看如何依时间来取出项目的版本A。
假设我们现在要取出2007-07-16 15:42:00时的版本(汗,被大家发现我这篇文章是什么什么才写的,因为马上回家了,归心似箭,所以效率奇低,请原谅),使用下面的命令
    $cvs update -D "2007-07-16 15:42:00 +0800" conf.d
其中"-D"后面跟的是时间,这个时间格式一看就明白,其中的"+0800"指的是东八区,也就是我们现在所处的时区。这个可以加也可以不加,CVS可以自动检测出系统所处的时区,换算成正确的时间,当然前提是你系统的时区设置是正确的。如果系统使用的是GMT时间,则此处的"+0800"应该换成"GMT"。

查看前面的log可以知道,在"2007-07-16 15:42:00"这个时间点,conf.d/test.pl仍然在,还没有被删除,而conf.d/configure仍处于版本1.1,conf.d/configure.example处于版本1.2,我们看看我们是不是真的回到过去了:)
    $cvs status conf.d
执行结果如下
    cvs status: Examining conf.d
    ===================================================================
    File: configure         Status: Up-to-date

       Working revision:    1.1     2007-07-16 21:28:23 +0800
       Repository revision: 1.1     /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   60e6469b1b1d4567
       Sticky Tag:          (none)
       Sticky Date:         2007.07.16.07.42.00
       Sticky Options:      (none)

    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.2     2007-07-16 21:28:23 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   60b5469b16fa4567
       Sticky Tag:          (none)
       Sticky Date:         2007.07.16.07.42.00
       Sticky Options:      (none)

    ===================================================================
    File: test.pl           Status: Up-to-date

       Working revision:    1.1     2007-07-16 15:41:26 +0800
       Repository revision: 1.1     /usr/local/HappyBirthday/conf.d/Attic/test.pl,v
       Commit Identifier:   6124469b21264567
       Sticky Tag:          (none)
       Sticky Date:         2007.07.16.07.42.00
       Sticky Options:      (none)

我们看到,真回到过去了,注意这时候文件的"Sticky Date"不再为"(none)",而是变成了"2007.07.16.07.42.00"。

那现在的版本B呢,不会消失了吧,如果真是这样,那CVS也就不是CVS了,使用下面的命令我们即可以回到版本B。
    $cvs update -A

需要指出的是,在回到版本A的时候,如果你修改了conf.d的内容,将会无法提交。为什么,因为这是已经过去的事,过去的事怎么可以改变,这时候分支的概念就被引入来解决这个问题。打个比方,玩过魔兽的人都知道有个剑圣,剑圣有分身术,我们可以把剑圣的分身看做项目的分支,剑圣的真身看做项目的主干,在使用分身术之前只存在一个剑圣,如我们的版本A,使用分身术之后真身与分身可以各做各的事,但他们始于同一起点。如果真身不幸遇到了敌人大部队挂掉了,但分身不会受影响。

6.9.2
我们接着来看如何依标记取出项目的版本A。

要使用标记来取出过去的项目,则必须存在标记。
恰好zhuqin同学定义了一个叫versionA的标记,做标记时工作目录中的conf.d/configure.example和conf.d/configure均还处于版本1.2。
    $cvs tag versionA conf.d

看看现在zhuqin的工作目录下conf.d的状态
    $cvs status conf.d
执行结果
    cvs status: Examining conf.d
    ===================================================================
    File: configure         Status: Up-to-date

       Working revision:    1.3     2007-07-16 22:06:23 +0800
       Repository revision: 1.3     /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   61c4469b2b1d4567
       Sticky Tag:          (none)
       Sticky Date:         (none)
       Sticky Options:      (none)

    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.3     2007-07-16 22:06:23 +0800
       Repository revision: 1.3     /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   63cc469b6cb84567
       Sticky Tag:          (none)
       Sticky Date:         (none)
       Sticky Options:      (none)
    
现在我们让我们回到版本A,再看看conf.d中文件的状态如何
    $cvs update -r versionA conf.d
    $cvs status conf.d
    cvs status: Examining conf.d
    ===================================================================
    File: configure         Status: Up-to-date

       Working revision:    1.2     2007-07-16 22:07:52 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   617e469b251a4567
       Sticky Tag:          versionA (revision: 1.2)
       Sticky Date:         (none)
       Sticky Options:      (none)

    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.2     2007-07-16 22:07:52 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   60b5469b16fa4567
       Sticky Tag:          versionA (revision: 1.2)
       Sticky Date:         (none)
       Sticky Options:      (none)

注意,此时的"Sticky Tag"内容由"(none)"变为"versionA (revision: 1.2)",显然我们的确回到了版本A。

再回到版本B
    $cvs update -A
我们就又回到现在正常的状态。

6.9.3 分支
前面两种方法最终都得靠分支方能完美地解决问题,马上来看如何做分支。
首先取出版本A到old_conf.d文件夹中
    $cd /home/zhuqin/Projects/HappyBirthday
    $cvs co -d old_conf.d -r versionA conf.d
"-d"会在HappyBirthday目录下创建old_conf.d文件夹。

然后我们使用版本A来产生一个分支
    $cd old_conf.d
    $cvs tag -b versionA_branch
"-b"表示产生一个分支。

不过这时只是仓库中的内容做了标记,对当前工作目录下的文件还没有实质性的影响,即此时old_conf.d目录下的文件还不是独立的versionA_branch,查看它们的"Stick Tag"即可知道。
    $cvs update -r versionA_branch
现在old_conf.d目录下的所有文件就实实在在地属于分支versionA_branch了。

我们来看看当前分支的状态
    $cvs status old_conf.d
执行结果
    cvs status: Examining old_conf.d
    ===================================================================
    File: configure         Status: Up-to-date

       Working revision:    1.2     2007-07-16 15:58:52 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   617e469b251a4567
       Sticky Tag:          versionA_branch (branch: 1.2.2)
       Sticky Date:         (none)
       Sticky Options:      (none)

    ===================================================================
    File: configure.example Status: Up-to-date

       Working revision:    1.2     2007-07-16 14:58:20 +0800
       Repository revision: 1.2     /usr/local/HappyBirthday/conf.d/configure.example,v
       Commit Identifier:   60b5469b16fa4567
       Sticky Tag:          versionA_branch (branch: 1.2.2)
       Sticky Date:         (none)
       Sticky Options:      (none)
注意"Sticky Tag"的内容---"branch: 1.2.2",这表示这个versionA_branch是由版本1.2分出来的。

现在我们可以自由修改old_conf.d目录下的内容了。修改后old_conf.d/configure,执行commit:
    $cvs ci -m "更新versionA branch" conf.d

查看old_conf.d/configure当前的状态
    $cvs status old_conf.d/configure
    ===================================================================
    File: configure         Status: Up-to-date

       Working revision:    1.2.2.1 2007-07-16 22:31:12 +0800
       Repository revision: 1.2.2.1 /usr/local/HappyBirthday/conf.d/configure,v
       Commit Identifier:   64d3469b814b4567
       Sticky Tag:          versionA_branch (branch: 1.2.2)
       Sticky Date:         (none)
       Sticky Options:      (none)
可以发现,当前的工作版本变为1.2.2.1了。

7.结束语
至此,CVS的基本用法大概介绍完了。
虽然说了很多,不过做起来还是很简单的东西,而且的确是非常有用的工具。在实践过程中的遇到的问题差不多都可以通过查CVS的手册获得答案。
    $info cvs
不过这个打开的手册页默认停留的节点是"CVS commands",可以使用'u'来回到上层节点,查看整个CVS手册的结构和内容。
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号