9.3 源代码控制
如果你做的不是一个简单的项目,特别是项目的开发人员不止一个时,为避免文件修改的冲突并跟踪对源文件所作出的修改,对源文件改动方面的管理就变得非常重要。
UNIX中有几个被广泛使用的用于管理源文件的系统,如下所示。
❑ SCCS:源代码控制系统。
❑ RCS:版本控制系统。
❑ CVS:并发版本控制系统。
❑ Subversion。
SCCS是由AT&T在系统V版本的UNIX中引入的最初的源代码控制系统,现在它已是X/Open标准的一部分了。RCS是在这之后开发的,它作为SCCS的一个免费替换系统,由自由软件基金会发布。RCS的功能与SCCS非常类似,但它有着更直观的接口和一些其他的选项,所以SCCS基本上已被RCS所取代。
RCS工具是Linux发行版中的一个常见套件,你也可以从自由软件基金会的网址http://directory.fsf.org/rcs.html上下载它及其源代码。
CVS是一个比SCCS和RCS更高级的工具,它用于基于互联网的协同开发。你可以在大多数Linux发行版中找到它,你也可以通过网址http://www.nongnu.org/cvs/下载它。与RCS相比,它有两个显著的优势:可以通过网络使用,并且允许并发开发。
Subversion是一个新开发的工具,它旨在最终替换CVS。它的主页是http://www.subversion.org。
在本章中,我们将重点介绍RCS和CVS。介绍RCS是因为它对个人的开发项目来说易于使用,并且它与make整合得很好。介绍CVS是因为它是用于合作项目的最常见的源代码控制形式。鉴于SCCS作为POSIX标准的地位,我们还将简要地比较RCS命令和SCCS命令,并对CVS和Subversion中的一些用户命令进行比较。
9.3.1 RCS
版本控制系统(RCS)提供了许多用于管理源文件的命令。它能够跟踪并记录下源文件的每一次改动,并将这些改动都记录在一个文件中,该文件中记录的改动信息足够详细,你可以通过这些信息重建出任何一个以前的版本。它还允许你为每次改动保存一个与之对应的注释信息,这对了解文件改动的历史非常有用。
随着项目的进展,你可以将每次对源文件进行的大的改动或漏洞的修补分别进行记录,并针对每次改动保存注释。当需要回顾对文件曾经做过的改动、检查何时修补过漏洞或何时引入漏洞时,这就非常有用。
因为RCS只保存版本之间的不同之处,所以它非常节省存储空间。万一不小心误删了文件,RCS还可以帮助你找回以前的版本。
1.rcs命令
为便于说明,我们从一个需要管理的文件的初始化版本开始介绍。在本例中,我们使用的文件为important.c,它实际上是文件foo.c的一份副本,但在文件的开头加上了如下的注释:
第一个任务是用rcs命令来初始化该文件的RCS控制。命令rcs -i的作用是初始化RCS控制文件。
你可以使用多行注释,结束输入需要在一行中单独使用一个英文句号(.)或输入文件结束字符(通常是组合键Ctrl+D)。
执行完这条命令后,rcs将创建一个新的只读文件,该文件的后缀带有,v,如下所示:
如果希望能把RCS文件保存到另一个目录中,你只需在第一次使用rcs命令之前建立一个名为RCS的子目录,这样所有的rcs命令都会自动地把RCS文件保存到该子目录中。
2.ci命令
现在可以使用ci命令将源文件的当前版本“签入”(check in)到RCS中了:
如果先前忘记执行rcs-i命令了,在执行ci命令时,RCS会要求输入一段对该文件的描述。如果现在查看目录中的内容,你将会发现文件important.c已被删除:
文件内容及其控制信息都已经被保存到RCS文件important.c,v中了。
3.co命令
如果想修改文件,你必须首先“签出”(check out)该文件。如果只是想阅读该文件,你可以用co命令重建当前版本的该文件并将它的权限改为只读。如果想对其进行修改,你就必须用命令co -l锁定该文件,因为在一个项目组中,必须确保任一时刻只有一个人可以修改指定的文件,这也是指定版本的文件只能有一份副本拥有写权限的原因。当文件以可写方式被“签出”时,对应的RCS文件将被锁定。
然后查看目录内容:
现在有了可以进行编辑的文件,你对其进行修改,把新版本存盘,然后再次用ci命令保存改动。现在文件important.c中的输出部分代码如下所示:
以如下方式使用ci命令:
如果想在“签入”该文件时仍然保留文件的锁定状态,使得可以继续对该文件进行修改,你就需要在调用ci命令时加上-l选项。这样,在“签入”该文件的同时它会被自动“签出”来供同一用户使用。
现在,你已保存了该文件的修订版本。如果查看目录内容,你就会发现文件important.c再次被删除了:
4.rlog命令
查看一个文件的改动摘要通常是很有用的。你可以用rlog命令来完成这一功能:
输出结果中的第一部分给出了对该文件的描述以及rcs使用的选项。接着,rlog命令列出对该文件的修改情况和你“签入”该文件时输入的注释内容,最近的修改列在最前面。版本1.2中的line:+1-0表明在这一修订版本中增加了一行,未删除行。
注意,文件修改时间在存储时不会进行夏令时调整,这是为了避免在改变时钟时可能会带来的问题。
如果现在想取出该文件的第一个版本,你可以在调用co命令时指定需要的版本号,如下所示:
ci命令也有一个-r选项,它的作用是强制指定主版本号,例如命令ci -r2 important.c将把文件important.c“签入”为版本2.1。RCS和SCCS默认都用数字1作为第一个次版本号。
5.rcsdiff命令
如果只是想了解两个版本之间的区别,你可以使用命令rcsdiff:
上面的输出结果表明在原文件的第11行后插入了一行。
6.标识版本
RCS系统可以在源文件中使用一些特殊的字符串(宏)来帮助跟踪文件所做的改动。最常用的两个宏是$RCSfile$和$Id$。宏$RCSfile$将扩展为该文件的名字,而宏$Id将扩展为一个标识版本号的字符串。RCS系统支持的特殊字符串的完整列表请查看在线帮助手册。这些宏将在文件被“签出”时扩展,并且在文件被“签入”时自动更新。
下面我们对文件important.c进行第三次修改,增加一些宏:
修改后的文件如下所示:
现在“签入”该版本,看看RCS是如何管理这些特殊字符串的:
如果查看目录内容,你将发现只有RCS文件存在:
如果“签出”(使用co命令)该文件并检查该源文件的当前版本,你就会发现宏已被扩展。
实 验 GNU make和RCS
GNU的make命令已内置了一些用于管理RCS文件的规则。在本例中,你将看到make命令是如何处理缺少源文件的情况的。
实验解析
make命令有这样一条默认规则:当make制作的目标是一个没有后缀名的文件时,make将编译具有同样的名字但加上.c后缀名的源文件。make命令具有的第二条默认规则允许make命令通过RCS系统从文件important.c,v创建出文件important.c。在这个例子中,由于文件important.c不存在,make命令就用co命令“签出”该文件的最新版本。编译完成后,它还会删除文件important.c来清理目录。
7.ident命令
你可以用ident命令查找包含$Id$字符串的文件的版本。因为你将字符串保存到一个变量中,所以它也会出现在最终的可执行程序中。你可能会发现,如果在源代码中加入一些特殊字符串,但未使用它们,一些编译器就会出于优化的目的将其删除。为解决这个问题,你可以在代码中增加一些对这些字符串的“假”访问,但随着编译器越来越好,解决这个问题也会变得越来越困难!
下面这个简单的例子将显示,你如何使用ident命令来验证用于建立一个可执行文件的源文件的RCS版本。
实 验 ident命令
实验解析
通过执行程序,你看到字符串确实已合并到可执行文件中。接着,你用ident命令从可执行文件里提取出$Id$字符串。
使用RCS系统及出现在可执行文件中的$Id$字符串的技巧可以成为一个功能非常强大的工具,它有助于确定客户报告有问题的文件的版本。你还可以将RCS(或SCCS)作为项目跟踪工具的一部分,用它来跟踪报告的问题及其解决方法。如果你做的是软件销售工作,或者哪怕是赠送软件,了解不同版本之间的改动情况也是非常重要的。
如果还想了解有关RCS的更多信息,除标准的RCS手册页以外,使用手册中的rcsintro页面给出了完整的RCS系统介绍。ci、co等命令也有其各自的手册页。
9.3.2 SCCS
SCCS提供了与RCS非常类似的功能。SCCS的优势在于,它得到了X/Open规范的定义,因此所有正规的UNIX系统都应该支持它。但较现实的情况是,RCS的可移植性非常好,并且可以自由发布。所以,如果你有一个类UNIX系统,不管它是否遵循X/Open规范,你都可以在其上获取并安装RCS。因此,我们不准备对SCCS做进一步的介绍,而只对这两个系统各自使用的命令进行简单的比较,以方便那些准备在这两个系统之间进行切换的用户。
9.3.3 RCS和SCCS的比较
直接比较这两个系统所提供的命令是很困难的,所以表9-2只能被看作是一个简单的起点。这里列出的两个系统的命令在完成同一项任务时并不使用相同的选项,如果不得不使用SCCS系统,你就必须自己查找合适的选项,但至少现在你应该知道从哪里开始查找了。
表 9-2
除上面列出的那些命令外,SCCS系统中的sccs命令在功能上与RCS系统中的rcs和co命令有些相交。例如,命令sccs edit和sccs create就分别相当于命令co -1和rcs -i。
9.3.4 CVS
除使用RCS系统外,管理文件改动的另外一种方法是使用CVS系统,即并发版本控制系统。CVS系统现在变得非常流行,可能是因为与RCS系统相比,它有一个明显的优势:人们可以通过互联网使用CVS系统,而不像RCS系统只能用在一个共享的本地目录中。CVS还支持并行开发,即许多程序员可以在同一时间修改同一个文件,而RCS在任一时间只允许一个用户修改一个特定文件。CVS的命令与RCS的类似,这是因为CVS最初是作为RCS的一个前端程序来开发的。
由于CVS系统能够以灵活的方式跨网络运行,所以它适用于软件开发者之间唯一的网络连接方式就是通过互联网这种情况。许多Linux和GNU项目就是通过CVS系统来帮助不同的开发者协同工作的。一般而言,通过CVS系统对远端文件进行操作与通过它处理本地文件并无区别。
在本章中,我们将简要地介绍CVS的基础知识,通过学习,你可以开始使用本地版本库,并知道如何通过互联网,从CVS服务器上获取项目的最新源文件副本。有关CVS的更详细信息请参考由Per Cederqvist等撰写的CVS使用手册,该手册位于网址http://ximbiot.com/cvs/manual/,你还可以在该网址上找到FAQ文件和其他一些有帮助的文件。
首先,你需要创建一个版本库,CVS系统将其控制文件和它管理的文件的主副本保存在这个版本库中。版本库的结构是树状的,所以你不仅可以把一个项目的完整目录结构保存在一个版本库中,还可以在同一个版本库中保存多个项目。当然,你也可以将彼此没有关联的项目分别保存到不同的版本库中。你将在后面看到如何告诉CVS系统你要使用哪一个版本库。
1.CVS的本地使用
我们首先创建一个版本库。为保持简单,这将是一个本地版本库,并且因为你将只使用这一个版本库,所以适宜将其放到/usr/local目录下。在大多数的Linux发行版中,所有的普通用户都属于组users,所以将该版本库的属组也设为users,这样所有用户都可以访问它了。
以超级用户的身份为版本库创建目录:
切换为普通用户,将该目录初始化为一个CVS版本库。如果你不属于users组,那么需要拥有目录/usr/local/repository的写权限,才能执行这一操作。
-d选项告诉CVS你希望版本库创建在哪个目录中。
现在版本库已创建好,你可以将项目的初始版本保存到CVS中了。做这项工作时,你可以利用一个小技巧来节省一些打字的时间。所有的cvs命令在查找CVS目录时都可以使用两种方法:一是在命令行中使用-d<path>选项(就像你刚才使用init命令时那样);如果未使用-d选项,cvs命令就会去查看环境变量CVSROOT的值。你不想在每次执行cvs命令时都加上-d选项,所以你将用第二种方法设置环境变量CVSROOT。如果使用的shell是bash,则设置环境变量的方法如下所示:
首先,切换到项目所在的目录,然后告诉CVS导入该目录下的所有文件。对CVS系统而言,一个项目就是相关文件和目录的集合。一般来说,它包括用于创建应用程序所需的所有文件。术语导入的含义是,将文件置于CVS的控制之下,并将它们复制到CVS版本库中。对本例来说,你有一个名为cvs-sp(即CVS simple project的缩写)的目录,它包含两个文件:hello.c和Makefile:
CVS导入命令是cvs import,它的使用方法如下所示:
上面这条命令告诉CVS导入当前目录(cvs-sp)下的所有文件,同时为其加上一条日志信息。
参数wrox/chap9-cvs告诉CVS保存新项目的位置,这里给出的是相对于CVS树根的路径。请记住,只要你愿意,CVS可以在同一个版本库中保存多个项目。选项wrox相当于厂商标签,它用于标识导入文件的初始版本的提供者。选项start是一个版本标签,它用于标识一组相关的文件,例如构成一个软件特定版本的一组文件。CVS对上面命令的响应如下所示:
输出结果表明它成功地导入了两个文件。
现在是查看能否从CVS系统中获取文件的好时机。你可以先建立一个junk目录,然后导出文件以确认一切工作正常。
你向CVS给出与导入文件时相同的路径。CVS在当前目录中创建wrox/chap9-cvs子目录,然后将文件放到该子目录中。
现在开始对项目做一些改动。编辑目录wrox/chap9-cvs中的文件hello.c,并对它做一个小的修改,在该文件中添加下面一行内容:
然后重新编译并运行程序以保证一切顺利。
你可以询问CVS这个项目有哪些改动。你并不需要告诉CVS你关心的文件具体是哪个,它能够一次性完成对整个目录的检查:
CVS响应如下:
你对自己做的改动很满意,所以决定将其提交给CVS。
当把改动提交给CVS时,它会启动一个编辑器让你输入一条日志信息。你可以在运行commit命令之前,通过设置环境变量CVSEDITOR来强制使用一个特定的编辑器。
CVS的响应表明它正在导入的内容:
现在可以询问CVS,这个项目自第一次导入后的改动情况。你询问的是项目wrox/chap9-cvs自版本1.1(即初始化版本)以来的所有改动情况。
CVS给出的结果如下:
假设在CVS系统之外的本地目录中还有一份代码的副本,现在你想刷新该目录中的文件以更新那些你没有修改过、但已被其他人改动过的文件。CVS的update命令可以帮助你完成这一工作。首先移动到项目路径的顶层,在本例中就是包含wrox子目录的目录,然后执行下面的命令:
CVS开始刷新相关文件,它把其他人修改过而你未动过的文件从版本库中提取出来,并放到你的本地目录中。当然,其中一些修改可能与你做的修改有冲突,但这是需要你解决的问题,CVS是好东西,但它并不是无所不能!
至此,你应该可以看出,CVS的用法与RCS相当接近。但它们之间其实有一个我们还未提及的十分重要的区别,那就是CVS具备在不事先挂载文件系统的情况下跨网络操作的能力。
2.跨网络访问CVS
前面已经介绍过,你可以通过为每个命令加上-d选项或设置环境变量CVSROOT来告诉CVS版本库所在的位置。如果想跨网络操作,你只需要使用这个参数的一个更高级的语法即可。例如,在写作本书的时候,GNOME(GNU网络对象模型环境,一个流行的开源图形桌面系统)的开发源代码都是通过CVS系统在因特网上访问的。你只需在路径说明符的前面添加上一些网络信息即可指定正确的CVS版本库的位置。
作为另外一个例子,你可以通过设置环境变量CVSROOT为:pserver:anonymous@dev.w3.org:/sources/public,将CVS指向Web标准组织W3C的CVS版本库。这个设置告诉CVS,该版本库使用密码验证(pserver),且位于服务器dev.w3.org上。
在访问源代码之前,你需要先登录,如下所示:
在提示输入密码时输入anonymous。
现在可以使用cvs命令了,命令的用法和你对本地版本库进行操作时一样,只有一个小区别:需要给每个cvs命令加上-z3选项以强制执行数据压缩,这可以节约网络带宽。
假设想要获取W3C HTML验证程序的源代码,使用的命令是:
如果想把自己的版本库设置为可以通过网络访问,你就需要在自己的机器上启动CVS服务。这个任务可以通过xinetd或inetd来完成,具体使用哪个进程取决于你的Linux系统配置。对xinetd来说,你需要编辑文件/etc/xinetd.d/cvs来反映CVS版本库的位置,并使用系统配置工具来激活和启动cvs服务。对inetd来说,你只需在文件/etc/inetd.conf中添加如下一行语句,然后重启inetd即可。
这条语句告诉inetd进程为连接到本机2401端口的客户自动启动一个CVS会话,端口2401是标准的CVS服务器监听端口。有关如何通过inetd启动网络服务的更详细资料请参考inetd和inetd.conf的手册页。
如果想通过网络访问的方式使用CVS版本库,你必须正确地设置环境变量CVSROOT。例如:
目前为止,我们仅简单介绍了CVS的功能。如果想用好CVS系统,我们强烈建议你设置一个本地版本库来进行练习,并获取更全面的CVS文档。请记住,CVS的源代码是开放的,如果搞不懂代码的作用和目的,或者(虽然不太可能,但确实有可能!)认为自己发现了一个bug,你总是可以获取源代码并自己进行分析。CVS的主页是http://ximbiot.com/cvs/cvshome/。
9.3.5 CVS的前端程序
许多图形前端程序可用于访问CVS版本库。网址http://www.wincvs.org提供了可能是最好的多操作系统前端程序集合。该网址上有用于Windows、Macintosh、Linux系统的客户端程序。
CVS前端程序通常都允许创建和管理版本库,包括远程访问基于网络的版本库。
图9-1显示了我们的简单应用程序的开发历史,这是在一个Windows网络客户端上使用WinCVS前端程序显示的。
图 9-1
9.3.6 Subversion
Subversion旨在成为开源社区中用于强制替换CVS的版本控制系统。根据Subversion主页http://subversion.tigris.org上的说法,它被设计为一个“更好的CVS”。因此,它具有CVS的大多数功能,并且其接口的工作方式也与CVS类似。
Subversion正变得日益普及,尤其对于社区开发的项目更是如此。因为在这些项目中,开发人员都是通过因特网来共同开发一个应用程序。大多数Subversion用户都是连接到一个由开发项目的管理员建立的基于网络的版本库。个人或小团队的项目使用Subversion的并不多,CVS仍然是他们的首选工具。
表9-3比较了CVS和Subversion中一些完成同样功能的命令。
表 9-3
有关Subversion的完整文档见网址http://svnbook.red-bean.com上的在线书籍Version Control with Subversion(使用Subversion进行版本控制)。