数据是如何存储在源码库中的

对大部分使用者来讲,不必太在意CVS是如何在源码库中存储数据的。实际上,在过去的时间中存储的格式一直在变化,而且以后也会继续变化。因为在大多数情况下用户是通过CVS命令来访问源码库,所以这些变化带来的影响不大。

但是,在某些情况下,了解CVS是如何在源码库中存储数据就显得十分必要,例如,你需要追踪CVS锁[多人同时运行CVS 在 第 10 章],或者你需要处理源码库中的文件权限。

源码库中的文件目录结构

源码库中全部目录结构完全对应于工作目录中的目录结构。比如,假设源码库在

/usr/local/cvsroot

下面是一个可能的目录结构(只显示目录):

/usr
 |
 +--local
 |   |
 |   +--cvsroot
 |   |    |
 |   |    +--CVSROOT
          |      (administrative files)
          |
          +--gnu
          |   |
          |   +--diff
          |   |   (source code to gnu diff)
          |   |
          |   +--rcs
          |   |   (source code to rcs)
          |   |
          |   +--cvs
          |       (source code to cvs)
          |
          +--yoyodyne
              |
              +--tc
              |    |
              |    +--man
              |    |
              |    +--testing
              |
              +--(other Yoyodyne software)

在版本控制中,存储在源码库中的文件统称为历史文件(history files),这些文件的名称是在对应在工作目录中的文件名称后面加上,v,下面是源码库中yoyodyne/tc目录的可能情况:
  $CVSROOT
    |
    +--yoyodyne
    |   |
    |   +--tc
    |   |   |
            +--Makefile,v
            +--backend.c,v
            +--driver.c,v
            +--frontend.c,v
            +--parser.c,v
            +--man
            |    |
            |    +--tc.1,v
            |
            +--testing
                 |
                 +--testpgm.t,v
                 +--test2.t,v

历史文件中包括有足够的信息来再创建文件的任何一个修订版,另外还记录有所有提交信息的日志,其中包括提交者的用户名。这些历史文件就是以前的RCS files,因为第一个以这种存储文件格式方式来进行版本控制的系统是RCS。关于文件格式的详细描述请看RCS发布的man中关于[rcsfile(5)]的信息,或者是CVS源代码中的doc/RCSFILES文件。这种文件格式已经应用的十分广泛,除了CVS和RCS之外还有很多其它的版本控制系统可以输入用这种格式的历史文件。

CVS中用的文件格式与标准的RCS文件格式有一些差别。最大的区别在于分支,关于这些的更多信息请看分支的帮助[内部分支号码 在 第 5 章]。另外CVS中有效的标签名称是RCS所能接受范围的一个子集,关于这些请看CVS中标签的帮助[标签形式的版本号 在 第 4 章]。

文件权限

创建的,v文件全部被设置成只读,并且,你不能改变那些文件的权限。在源码库里的目录对于在每个目录里都有修改文件权限的人来说才是可写的。这通常意味着你必须建立由一个能在项目里编辑文件的人组成的unix组(参见group(5)),并且设置源码库以便确保确实是这个组拥有这一目录。(在有些系统上,你还需设置源码库的set-group-ID-on-execution标识位(参见chmod(1)),这样才能使新建的文件和目录正确得到父目录的group-ID而不是当前进程的).

这也意味着你仅仅能控制每个目录下文件的存取。

注意:那些用户必须也有写权限(write access)来检出文件,因为CVS需要建立锁定文件(lock file)[多人同时运行CVS 在 第 10 章].对于一些只读访问的目录, 你也可以利用CVSROOT/config的LockDir来将锁定文件放到源码库以外的地方 [The CVSROOT/config configuration file 在 附录 C].

还要注意用户必须有CVSROOT/val-tags文件的写(write access)权限,CVS用它纪录标签的标记名是有效的(和它们被建立时一样,使用中有时被更新)。

每一个RCS文件将被最后一个提交它的人拥有。其实这并不重要,它的实质在于谁拥有这目录。

CVS试图为每一个添加在这个目录树里的新目录设置合理的文件权限,但是当你需要一个新目录与它的父目录有不同的权限的时候,必须手动设置它。假如你设置那些在源码库里由CVS建立目录或者(和)文件所使用的控制文件权限的CVSUMASK 环境变量,CVSUMASK不影响工作目录里的文件权限,这样文件拥有的权限是典型的新建文件所有的权限, 除非有时CVS建立它们是只读的时候。(参见[用cvs观察(watch)指定的文件 在 第 10 章]; -r,[全局选项 在 附录 A];或者CVSREAD,[附录 D])

注意:使用客户机/服务器的CVS[远程源码库],没有好的方法来设置CVSUMASK,设置在客户机上没有效果,如果你正与rsh连接着,那正如你的操作系统文档中说明的那样,你可以把CVSUMASK设置在.bashrc.cshrc里。这个操作也许会在CVS将来的版本里发生变化,不要依靠在客户机上的没有效果的CVSUMASK设置。

使用pserver,在CVSROOT目录,以及在这个目录树里那些目录上,你通常需要更加严格的设置权限。参见[密码认证中的安全事项]。

有些操作系统具有这种特性: 允许让某一特定程序运行时完成程序访问者不能完成的任务。比如,unix中的设置用户id(setuid)或设置组id(setgid)特性, 或者VMS的安装映像特性. CVS就不是为使用这类特性而编写的,因此,试图以这种方式安装CVS将只能提供针对偶然性失误的保护;任何想要获得这一方法的人都可以轻易做到,而且,根据你设置的不同,你还可以获得比CVS更多的东西。或者,你也许希望考虑 pserver。就可能提供虚假的安全感或者打开一个比你试图要弥补的安全漏洞更大的漏洞方面而言,它也具有相同的特性。因此,如果你考虑这一功能的话,一定要仔细阅读有关pserver安全方面的文档[密码认证中的安全事项]。

针对Windows操作系统的文件权限问题

针对Windwos操作系统(如Windows95、WindowsNT,还有这个系列以后的操作系统,有可能还针对OS/2操作系统,但不确定)的一些文件权限问题。

如果你正在使用一个在区域网中的CVS系统,源码库在一个由Samba提供服务的网络文件系统上,你可能会碰到一些文件权限问题。在Samba配置中允许WRITE=YES据说可以解决这个问题。不过需要申明的是,我没有研究过允许这个选项会带来什么样的影响,同时我也不知道有没有其他的方法可以解决这个问题。如果你发现有解决的方法,请及时告诉我们[附录 H]。

Attic目录

你可能会注意到CVS时常会把RCS文件存放在Attic目录中。例如,源码库在/usr/local/cvsroot中,我们讨论的在yoyodyne/tc目录中的文件backend.c,一般情况下这个文件应该在下面这个位置上:

/usr/local/cvsroot/yoyodyne/tc/backend.c,v

但是,如果它在Attic目录中,就应该是这个位置:

/usr/local/cvsroot/yoyodyne/tc/Attic/backend.c,v

从用户的角度来看,这是无所谓的。CVS会自动跟踪文件的,如果需要它会到Attic目录中寻找。不过你需要知道的是,当且仅当在主干中的主修订版(head revision)处于dead状态,CVS才会把RCS文件存放在Attic目录中。dead状态指在这个修订版中文件被删除或没有被加入进来。例如,你在一个分支中加入一个文件,那么在主干中就会存在一个处于dead状态的修订版,而分支中的修订本处于非dead状态。

在源码库中的CVS目录

在源码库中的每一个目录中都存在一个CVS目录,其中存放一些象文件属性这样的信息(在CVS目录中有一个CVS/fileattr文件)。在将来会有更多的文件放入到这个目录中,执行时会忽略这些新加的文件。

这个特性只会出现在CVS 1.7版本之后,详细请看[在旧版本的CVS上使用观察 在 第 10 章]。

fileattr文件有一系列如下格式的条目(entriese)组成(其中{}意思是括号内文字可以没有或重复多次):

ent-type filename <tab> attrname = attrval {; attrname = attrval} <linefeed>

ent-typeF 表示文件, 该条目设置文件的属性.

ent-typeD, 并且 filename 为空, 给新添加的文件设置属性.

其余ent-type留给以后扩展使用, cvs 1.9和更老的版本会删除它们, cvs 1.10和后续版本将其保留.

注意,每行的先后次序不重要, 任何程序可以按它的习惯重新排列.

当前无法作到:文件名中带TAB或换行, attrname里的=, attrval里的; , 等等. 注意: 有些操作还无法处理NULL字符, 但不为错.

习惯上在attrname名前加_表示在CVS中有特殊含义; 其余为用户自定义类型的属性(前题是该操作支持).

内含属性(Builtin attributes):

_watched

意思是该文件已被监视(watched), 检出为只读(read-only).

_watchers

监视此文件的用户. 值为 watcher > type { , watcher > type } 其中 watcher 是用户名, type 是空或由+隔开的edit,unedit,commit (什么都没有就是空,无需"none"或 "all" 关建字).

_editors

编辑此文件的用户. 值为 editor > val { , editor > val } 其中 editor 是用户名, valtime+hostname+pathname, 其中 timecvs edit 执行的时间 (或类似的), hostnamepathname 是工作目录.

例如:

Ffile1 _watched=;_watchers=joe>edit,mary>commit
Ffile2 _watched=;_editors=sue>8 Jan 1975+workstn1+/home/sue/cvs
D _watched=

意味着文件file1检出为只读. 另外joe监视此文件的edit而mary监视此文件commit. 文件file2检出为只读; sue在8 Jan 1975开始编辑它, 目录是/home/sue/cvs , 机器为workstn1. 其余文件都是检出为只读. 作为演示, D, Ffile1, Ffile2后面有一个空格, 其实该用TAB字符而无空格.

源码库中的CVS锁

关于CVS锁用于用户可见特性方面的介绍, 请参看[多人同时运行CVS 在 第 10 章]. 接下来的内容是针对那些想制作工具软件的朋友,这些软件可以访问CVS源码库,但不会与其它工具软件的冲突问题。如果你发现你被这里的一些名词,如“读锁(read lock)”、“写锁(write lock)”和“死锁(deadlock)”,弄的混淆不清,建议你可以去阅读一些关于操作系统和数据库的资料。

在源码库中任何一个以#cvs.rfl.开头的文件都具有一个读锁,以#cvs.wfl.开头的文件就有一个写锁。老版本的CVS(在CVS1.5之前的版本)也会创建一些以#cvs.tfl开头的文件,在这里我们不会讨论它们。目录#cvs.lock的作用是一个主锁(master lock),它的意思是在创建其它种类锁之前你必须获得这个主锁。

为了得到一个读锁,第一步必须先创建#cvs.lock目录。这样的一个操作必须是一个原子操作(在大多数操作系统下面创建目录都是可以的)。如果失败, 是因为这个目录已经存在,这个操作会等待一会再重新执行。在得到#cvs.lock之后,会创建一个文件,以#cvs.rfl.开头,后面为你选择的信息(例如主机名和进程识别号);接着删除#cvs.lock目录就会释放主锁;下来开始读源码库。读操作完成后删除#cvs.rfl文件,释放读锁。

为了得到一个写锁,同上面一样第一步必须先创建#cvs.lock目录;接下来检查是否不存在以#cvs.rfl.开头的文件,如果存在就删除#cvs.lock目录,等待一会再重试。如果没有人正在进行读操作,就创建一个文件,以#cvs.wfl开头,后面为你选择的信息(例如主机名和进程识别号);继续保持住主锁,开始进行写源码库操作;当操作结束,首先删除#cvs.wfl文件,接着删除#cvs.lock目录。需要说明的是不像#cvs.rfl文件,#cvs.wfl文件仅仅只有提示作用,不能锁住操作,而这个功能是由#cvs.lock来完成的。

注意,每一个锁(读锁或者写锁)仅仅只锁住源码库中的单一目录,包括Attic目录和CVS目录,但是不包括在版本控制中代表其它目录的子目录。如果要锁住整个目录树,你必须锁住每一个目录(如果你在锁任何一个目录时出错,为了避免死锁就必须在重试之前释放整个目录树)。

还需注意的是CVS希望用写锁来控制任何一个foo,v文件的访问。RCS有一个计划,让,foo,文件具有锁的作用,但CVS并没有这样实现,而且建议使用CVS的读锁。关于此方面更多的信息请参看CVS源码中的rcs_internal_lockfile。

文件在CVSROOT目录中是如何保存的

$CVSROOT/CVSROOT目录包含有各种管理文件。在某些方面这个目录就象源码库中任何其它目录一样;它包含了RCS文件,后缀名为,v,许多CVS命令以同样的方式对它进行操作。当然,还是有点小小的区别。

对每一个管理文件,还有RCS文件, 都有一个检出过的备份。例如,有一个RCS文件,名为loginfo,v 和一个包含有对loginfo,v的最新修改的文件loginfo。当你提交一个管理文件时,CVS就会显示:

cvs commit: Rebuilding administrative file database

并在$CVSROOT/CVSROOT目录中更新已检出的文件。如果它没这样做,那肯定出了问题[附录 H]一章。为了用这种方式使你自己的文件更新,你可以把它们加入到管理文件的checkoutlist中[The checkoutlist file 在 附录 C]。

在默认的情况下,modules文件按上面所说的方式运作。如果modules文件很大,把它作为一个纯文本文件存储可能会使得查找模块变慢(我不知道现在人们是否还象当初CVS有此特点时那样关心这个问题;我也没有看过有关的评测)。因此,对CVS源代码进行合适的编辑,人们可以把模块文件保存在一个使用ndbm界面如Berkeley db 或者GDBM的数据库中。如果使用这个选项,那么模块数据库将被存在文件modules.db,modules.pag和/或modules.dir中。

关于各种管理文件的意义,请参考[附录 C]。