CVS使用介绍 
作者:Jim Blandy 翻译:<a href=mailto:chatme@263.net>limodou</a> 
CVS是做什么的? 
  CVS 维护着一棵资源树的历史,这个历史是根据一系列的修改而来的。它记录下每一次修改,并且记录 
下修改的时间和做修改人的姓名。通常,那个人也会提供一小段文本,描述了为什么要做改动。给出那样的 
信息,CVS就可以帮助开发者回答如下的问题: 
谁作出的修改? 
他们什么时候做的? 
他们为什么要做修改? 
同时他们还做了什么修改? 
如何使用CVS -- 梗概 
  在讨论太多的含糊的术语和原理之前,让我们看一下基本的CVS命令。 
设置你的仓库 
  CVS将每个对给定项目所做的改变记录在一个叫作"仓库(repository)"的目录树中。在使用CVS之前,应该将环境变量CVSROOT设成为仓库的路径。无论谁负责你的项目配置管理都将知道它是什么;也许他们已经在某个地方给CVSROOT作了一个全局的定义。 
  在我们的系统中任何情况下,CVS仓库都是"/u/src/master"。在那种情况下,如果你的shell为csh或它的派生物,需要输入命令 
setenv CVSROOT /u/src/master 
  否则如果你的shell为Bash或Bourne Shell的某个变种,则输入: 
CVSROOT=/u/src/master 
export CVSROOT 
  如果忘了做这个,当你试图使用它时,CVS将会作出"抱怨": 
$ cvs checkout httpc 
cvs checkout: Now CVSROOT specified! Please use the '-d' option 
cvs [checkout aborted]: or set CVSROOT environment variable. 
$ 
检出[注1]一个可工作目录 
  CVS不能在原始的目录树下工作;你需要工作在一个由CVS为你创建的目录下。正好象你先要从图书馆借出书来,才可以把书带回家看。要使用cvs checkout 命令从CVS中得到一个目录树,才可以对其进行处理。 
例如,假设你正在进行一个名为httpc的项目,一个微不足道的HTTP客户端程序: 
$ cd 
$ cvs checkout httpc 
cvs checkout: Updating httpc 
U httpc/.cvsignore 
U httpc/Makefile 
U httpc/httpc.c 
U httpc/poll-server 
$ 
  cvs checkout httpc命令表示,"从由CVSROOT环境变量所指的仓库中检出名为httpc的源代码树。" 
  CVS将这棵树放在名为httpc的子目录中: 
$ cd httpc 
$ ls -l 
total 8 
drwxr-xr-x 2 jimb 512 Oct 31 11:04 CVS 
-rw-r--r-- 1 jimb 89 Oct 31 10:42 Makefile 
-rw-r--r-- 1 jimb 4432 Oct 31 10:45 httpc.c 
-rwxr-xr-x 1 jimb 460 Oct 30 10:21 poll-server 
  大部分的文件是你正在进行的httpc源代码的拷贝。然而,叫做CVS的子目录(在上面)是不一样的。 
CVS用它来记录目录中每一个文件的额外的信息,用于帮助它确定自从你检出之后做了什么修改。 
对文件进行修改 
  一旦CVS创建完工作目录树,你就可以按通常的方式对目录中的文件进行编辑、编译和测试了(它们只是 
文件)。 
  例如,假设我们想要编译我们刚检出的包: 
$ make 
gcc -g -Wall -lnsl -lsocket httpc.c -o httpc 
httpc.c: In function `tcp_connection': 
httpc.c:48: warning: passing arg 2 of `connect' from incompatible pointer type 
$ 
  看上去httpc.c还没有被移植到这个操作系统上。我们需要转化connect中的一个参数。为了改正,第48行必须从: 
if (connect (sock, &name, sizeof (name)) >= 0) 
  改成为: 
if (connect (sock, (struct sockaddr *) &name, sizeof (name)) >= 0) 
  现在它应该可以编译了: 
$ make 
gcc -g -Wall -lnsl -lsocket httpc.c -o httpc 
$ httpc GET 
http://www.cyclic.com ... HTML text for Cyclic Software's home page follows ... 
$ 
合并你的修改 
  因为每一个开发者使用他们自已的工作目录,所以在你的工作目录下所做的修改并不能自动地被你的小组的其他人看到。CVS 在你没准备好之前是不会公布你的修改的。当你测试完你的修改之后,你必须向仓库提交[注2]所做的修改,以便让它们对于组内的其他人有效。我们将在下面描述一下cvs commit命令。 
  然而,如果另一个开发者修改了你所改的同一个文件,或同一行会怎么样呢?哪一个改变应该成功?一般不可能自动回答这个问题;CVS当然没有能力做出判断。 
  所以,在提交修改之前,CVS要求你的源代码要与由其它组内成员所提交的任何修改保持同步。cvs update命令可以做到这一点: 
$ cvs update 
cvs update: Updating . 
U Makefile 
RCS file: /u/src/master/httpc/httpc.c,v 
retrieving revision 1.6 
retrieving revision 1.7 
Merging differences between 1.6 and 1.7 into httpc.c 
M httpc.c 
$ 
  让我们一行行地看一下: 
U Makefile 
形如"U file"的行表示file被简单地更新了;某人也已经对文件作了修改,并且CVS将改过的文件拷贝 
到你的根目录下。 
RCS file: ... 
retrieving revision 1.6 
retrieving revision 1.7 
Merging differences between 1.6 and 1.7 into httpc.c 
这些信息说明某人也已经修改了httpc.c;CVS 将他们的修改与你的合并,并且没有发现任何正文的冲 
突。数字1.6和1.7是版本号,用来标识在一个文件历史中的特定的点。注意CVS只是将修改合并到你正 
在处理的拷贝中;仓库和其它的开发者目录没有受到影响。这时你应该测试合并后的文本, 并且确认它是有效的。 
M httpc.c 
形如"M file"的行表示file 已经被你修改了,并且包含了还不能被别的开发者看到的修改。这些修改你需要将其提交。在这个例子中,httpc.c现在包含了你的修改和其它用户的修改。 
  因为CVS已经将其他人的修改合并到你的源文件中了,最好确定这些代码还能工作: 
$ make 
gcc -g -Wall -Wmissing-prototypes -lnsl -lsocket httpc.c -o httpc 
$ httpc GET 
http://www.cyclic.com ... HTML text for Cyclic Software's home page follows ... 
$ 
  看上去还能工作。 
提交你的修改 
  既然你已经将你的源代码保持为同组内的其它人一样的最新代码,并且测试过了,你已经准备好将修改提交到仓库中,让这些改变对组内的其他人也是可见的。你只修改了httpc.c,但是为了安全起见,运行cvs 
update从CVS得到修改过的文件列表: 
$ cvs update 
cvs update: Updating . 
M httpc.c 
$ 
  正如期望地那样,CVS提示只有一个文件httpc.c被修改了;指出httpc.c包含了还没有被提交的修改。 
你可以象这样提交修改: 
$ cvs commit httpc.c 
  此时,CVS将启动你喜欢的编辑器并且提示你一个日志信息,描述所做的修改。当你退出编辑器,CVS将提交你的修改: 
Checking in httpc.c; 
/u/src/master/httpc/httpc.c,v <-- httpc.c 
new revision: 1.8; previous revision: 1.7 
$ 
  既然你已经提交了修改,这些修改就可以被组内其他人看到了。当另一个开发者运行cvs update命令时,CVS将合并你的修改到他的工作目录下的httpc.c中。 
检查修改 
  此时,你可能会好奇别的开发者对httpc.c作了哪些修改。为了查看一个给定文件的日志记录,你可以使用cvs log命令: 
$ cvs log httpc.c 
RCS file: /u/src/master/httpc/httpc.c,v 
Working file: httpc.c 
head: 1.8 
branch: 
locks: strict 
access list: 
symbolic names: 
keyword substitution: kv 
total revisions: 8; selected revisions: 8 
description: 
The one and only source file for the trivial HTTP client 
---------------------------- 
revision 1.8 
date: 1996/10/31 20:11:14; author: jimb; state: Exp; lines: +1 -1 
(tcp_connection): Cast address structure when calling connect. 
---------------------------- 
revision 1.7 
date: 1996/10/31 19:18:45; author: fred; state: Exp; lines: +6 -2 
(match_header): Make this test case-insensitive. 
---------------------------- 
revision 1.6 
date: 1996/10/31 19:15:23; author: jimb; state: Exp; lines: +2 -6 
... 
$ 
  在这里的大部分文本你可以忽略;需要注意的部分是在第一个连字符行(译注:由'-'构成的行 )后面的日志记录。日志记录按时间的反序排列,这是根据最近的修改通常要更加感兴趣的假设。每一个记录描述了对文件的一个修改,可以如下分析: 
`revision 1.8' 
每一个文件的版本都有一个不重复的修订版本号。修订版本号看上去象'1.1','1.2','1.3.2.2'或'1.3.2.2.4.5'。缺省的修订版本号1.1是一个文件的第一个版本。每一个后继的修订版本号通过将最右边的数字加1而给出一个新的版本号。 
`date: 1996/10/31 20:11:14; author: jimb; ...' 
这一行给出修改的日期,和谁提交它的人的用户名;本行其它的内容就不太感兴趣了。 
`(tcp_connection): Cast ...' 
这(相当明显)是日志记录的修改描述。 
  cvs log命令可以通过日期范围来选择日志记录,或者通过修订版本号来选择;关于这一点更多的细节请参阅手册。 
  如果你实际上想看一下有些怀疑的修改,可以使用cvs diff命令。例如,如果你想看一下Fred在1.7修订版中做的修改,可以使用下面的命令: 
$ cvs diff -c -r 1.6 -r 1.7 httpc.c 
  在查看这个命令的输出之前,让我们看一下不同部分的意思: 
-c 
这个要求cvs diff使用人类可读的格式来进行输出。(我不明白为什么它不是个缺省值。) 
-r 1.6 -r 1.7 
这个告诉CVS 显示从httpc.c的1.6修订版到1.7修订版所做的修改。你可以根据喜好指定更广泛的修订版范围。例如,-r 1.6 -r 1.8将显示Fred的修改和你的最近修改。(你甚至可以要求修改向后被显示(好象正在被恢复一样)通过向后指定修订版:-r 1.7 -r 1.6。这个听上去挺奇怪的,但有时是有用的。) 
httpc.c 
这是想要查看的文件名。如果你不给出想要得到报告的指定文件名,CVS将为整个目录产生一个服告。 
  这是命令行的输出: 
Index: httpc.c 
=================================================================== 
RCS file: /u/src/master/httpc/httpc.c,v 
retrieving revision 1.6 
retrieving revision 1.7 
diff -c -r1.6 -r1.7 
*** httpc.c 1996/10/31 19:15:23 1.6 
--- httpc.c 1996/10/31 19:18:45 1.7 
*************** 
*** 62,68 **** 
} 
! /* Return non-zero iff HEADER is a prefix of TEXT. HEADER should be 
null-terminated; LEN is the length of TEXT. */ 
static int 
match_header (char *header, char *text, size_t len) 
--- 62,69 ---- 
} 
! /* Return non-zero iff HEADER is a prefix of TEXT, ignoring 
! differences in case. HEADER should be lower-case, and 
null-terminated; LEN is the length of TEXT. */ 
static int 
match_header (char *header, char *text, size_t len) 
*************** 
*** 76,81 **** 
--- 77,84 ---- 
for (i = 0; i < header_len; i++) 
{ 
char t = text
; 
+ if ('A' <= t && t <= 'Z') 
+ t += 'a' - 'A'; 
if (header != t) 
return 0; 
} 
$ 
  这个输出需要作出一点努力才能习惯,但是肯定值得去理解它。 
  需要注意的部分是前两行以"***"和"---"开始的;这些行描述了旧版与新版文件的比较。剩下的由两大块[注3]组成,每一个都以一个星号行开始。这是第一大块: 
*************** 
*** 62,68 **** 
} 
! /* Return non-zero iff HEADER is a prefix of TEXT. HEADER should be 
null-terminated; LEN is the length of TEXT. */ 
static int 
match_header (char *header, char *text, size_t len) 
--- 62,69 ---- 
} 
! /* Return non-zero iff HEADER is a prefix of TEXT, ignoring 
! differences in case. HEADER should be lower-case, and 
null-terminated; LEN is the length of TEXT. */ 
static int 
match_header (char *header, char *text, size_t len) 
  旧版本的文本从*** 62,68 ***行后面开始显示;新版本的文本从--- 62,69 ---行后面开始显示。每一个数字对指示了显示的行的范围。CVS提供了修改周围的上下文,并且用'!'字符来标记真正变化的行。所以, 
可以看出上一半的单行被替换成了下一半的两行。 
  这是第二大块: 
*************** 
*** 76,81 **** 
--- 77,84 ---- 
for (i = 0; i < header_len; i++) 
{ 
char t = text; 
+ if ('A' <= t && t <= 'Z') 
+ t += 'a' - 'A'; 
if (header != t) 
return 0; 
} 
  这一大块描述了两行的插入,用'+'字符进行标记。在这种情况下,CVS省略掉了旧版的文本,因为它是多余的。CVS使用相似的大块格式来描述删除。 
  如同Unix的diff命令,从cvs diff 得到的输出通常叫做一个补丁,因为开发者按照传统使用这个格式来描述错误修订或少量的新特性。虽然对于人类是相当可读的,同时一个补丁也包含了足够的信息,可以由一个程序来将它所描述的改变应用到一个未修改过的文本文件中。实际上,Unix 
下的patch命令就是这样做的,将一个给定的补丁作为输入。 
[注1]检出 
英文为check out,意指从仓库中取出文件,用于查看或修改。 
[注2]提交 
英文为commit,意指对所作的修改交给CVS确认的过程。 
[注3]大块 
英文为hunk,表示不同修订版本间的每一个修改的显示,如文中所述,是在执行cvs log后得到的输出结果中的一部分。 
增加和删除文件 
  CVS象对待其它的修改一样来看待文件的创建和删除,将这种事件记录在文件的历史中。看待这种处理的一种方式是CVS记录着目录的历史,也包括其中的文件。 
  CVS 并不假定新建的文件应该置于它的控制之下,在很多情况下这将引发错误。例如,不需要记录下目标文件和可执行文件的改变,因为它们的内容总可以从源文件中重新创建出来。相反,如果你创建了一个新文件,cvs update将用一个'?'符号标记它,直到你告诉CVS你想对它做什么为止。 
  为了向一个项目增加一个文件,你必须首先创建那个文件,然后使用cvs add命令来将其标记为增加。然后,接着调用cvs commit命令,将增加文件到仓库中。例如,这里是如何向httpc项目中增加一个README文件: 
$ ls 
CVS Makefile httpc.c poll-server 
$ vi README 
... 输入一个httpc的描述 ... 
$ ls 
CVS Makefile README httpc.c poll-server 
$ cvs update 
cvs update: Updating . 
? README --- CVS 还不知道这个文件呢。 
$ cvs add README 
cvs add: scheduling file `README' for addition 
cvs add: use 'cvs commit' to add this file permanently 
$ cvs update --- 现在CVS在想什么? 
cvs update: Updating . 
A README --- 文件被标记为增加 
$ cvs commit README 
... CVS 提示你作为一个日志记录 ... 
RCS file: /u/jimb/cvs-class/rep/httpc/README,v 
done 
Checking in README; 
/u/src/master/httpc/README,v <-- README 
initial revision: 1.1 
done 
$ 
  CVS以相似地方式看待文件删除。如果你删除一个文件,然后运行cvs update,CVS 并不假设你打算删除文件。相反,它做出很好的事情 -- 它使用以前记录的内容重建了这个文件,并且用一个'U' 字符来标记它,就象用于其它的修改。(这就意味着如果你想恢复在你的工作目录对一个文 
件所做的改变,你可以简单地删除文件,然后让cvs update重建它们。) 
  为了从一个项目中删除一个文件,你必须首先删除文件,然后使用cvs rm命令来将其标记为删除。然后,接着调用cvs commit命令将从仓库中删除文件。 
  使用cvs rm来提交一个标记过的文件并不破坏文件的历史。它简单地增加一个新的修订版本,它被标记为"non-existent"(不存在)。仓库仍然保留了文件以前的内容,并且可以在需要的时候重新调出来 -- 例如,通过cvs diff或cvs log命令。 
  有几种给文件改名的方法;最简单的是简单地在你的工作目录下给文件改名,并且在旧的文件名上运行cvs rm,接着在新的文件名上运行cvs add。这个方法的缺点是,旧文件内容的日志记录不能带到新文件中去。其它的方法则避免了这件事情,但是会有其它的奇怪的问题。 
  你可以象对普通的文件那样增加目录。 
编写良好的日志记录 
  如果一个人可以使用cvs diff来取得变化的确切文本,为什么一个人还要报怨写一个日志记录呢?很明显,日志记录可能要比一个补丁短得多,并且允许阅读者得到关于修改的一般性的理解,而不必钻进它的细节中。 
  然而,一个好的日志记录描述了开发者进行修改的原因。例如,显示在上面的1.7 修订版的一个坏的日志记录可能写着,"转换t变成小写。"这个可能是正确的,但是一点用也没有;cvs diff提供了所有的同样的信息,而且更清楚。一个好的日志记录应该是,"让这个判断大小写无关,"因为它使得修改的目的对任何人都很清楚,给出对于代码的一般性理解。HTTP客户端程序当分析应答头时应该忽略大小写。 
处理冲突 
  就象上面所提到的,cvs update命令将其它开发者所做的修改合并到你的工作目录下。如果你和其它的开发者修改了同一文件,CVS将他们的修改与你的合并到一起。 
  很容易想象当修改是应用于文件较远的地方时,修改的合并是如何工作的。但是当你和其它的开发者修改了同一行会发生什么事呢?CVS把这种情况称作冲突,并且把它留给你来解决它。 
  例如,假设你刚刚加入了一些对查找主机名字代码的错误检查。在提交你的修改之前,你必须运行cvs update,将你的代码进行同步: 
$ cvs update 
cvs update: Updating . 
RCS file: /u/src/master/httpc/httpc.c,v 
retrieving revision 1.8 
retrieving revision 1.9 
Merging differences between 1.8 and 1.9 into httpc.c 
rcsmerge: warning: conflicts during merge 
cvs update: conflicts found in httpc.c 
C httpc.c 
$ 
  在这个例子中,另一个开发者已经修改了你所拥有文件的同一个区域,所以CVS作出了一个冲突的"抱怨"。不象通常所做的那样打印出"M httpc.c",而是打印出"C httpc.c",用来指示在文件中发生了一个冲突。 
  为了解决冲突,在编辑器中打开这个文件。CVS以这种方式标记冲突: 
/* Look up the IP address of the host. */ 
host_info = gethostbyname (hostname); 
<<<<<<< httpc.c 
if (! host_info) 
{ 
fprintf (stderr, "%s: host not found: %s 
", progname, hostname); 
exit (1); 
} 
======= 
if (! host_info) 
{ 
printf ("httpc: no host"); 
exit (1); 
} 
>>>>>>> 1.9 
sock = socket (PF_INET, SOCK_STREAM, 0); 
  理解CVS所做的和不能处理冲突是很重要的。CVS不能理解你的程序的语义,它简单地将程序的源代码看作一棵文本文件树。如果一个开发者向函数中增加了一个新参数,并且改正了它的调用者,当另一个开发者同时增加了对那个函数的一个新的调用,但没有传递新的参数,这当然是一个冲突(两种修改是不相容的)。但是CVS将不会报告它。CVS对冲突的理解严格地按照正文进行。 
  幸运地,在实际应用中冲突是很少的。通常,生成冲突的原因看上去是由于:两个开发者试图处理同一问题,或由于在开发者之前很少交流,或者由于对程序设计的分歧所致。以一种合理的方式给开发者分配任务可以减少冲突的可能性。 
  许多版本控制系统允许一个开发者锁住一个文件,阻止其他人对其进行修改,直到他已经提交了他的修改。尽管锁机制在某些情况下是很合适的,但比起CVS 所使用的方法明显不是一个好的解决办法。修改通常可以被正确地合并,并且开发者有时会忘记释放锁;在这两种情况下,很明显锁机制会引起不必要的拖延。而且,锁只能阻止文本冲突,如果两个开发者对不同的文件进行修改,它们并不能阻止上面所描述的语义冲突。