SubVersion开发信息
SubVersion是一个开源项目,它的财政支持来自CollabNet Inc,这是位于加州的一个软件公司。本项目欢迎程序员加入开发,鼓励您为它做点事情,无论是出点主意,还是修正Bug,或者是提炼代码。
本章是针对那些打算直接利用这个软件的源代码帮助这个软件发展的人。我们会在这里公布一些该软件的内部细节,这些技术细节对你继续开发subversion有帮助,也可以让你利用SubVersion的库写出全新的工具。
分层设计的库
Subversion采用模块化设计,是一组C库的集合。每个库都有定义良好的接口和Purpose,而且大多数模块都处于三个主要的层之一中,这三个层分别是:存储层(Repositiry layer)、存储访问层(Repository Access layer)和客户层(Client Layer)。在学习这些层之前,先学习下表中列出的库的详细目录。为了保持一致性,这里使用它们的Unix 库名称(如:libsvc_fs , libsvn_wc, mod_dav_svn)。
表:subversion库的目录
库
|
介绍
|
libsvn_client
|
客户端程序的主接口
|
libsvn_delta
|
Tree and text differencing routines
|
libsvn_fs
|
Subversion文件系统库。
|
libsvn_ra
|
Repository Access commons and module loader
|
libsvn_ra_dav
|
WebDAV存储访问模块
|
libsvn_ra_local
|
本地存储访问模块
|
libsvn_ra_svn
|
一个私有协议存储访问模块
|
libsvn_repos
|
存储(仓库)接口
|
libsvn_subr
|
各种有用的子程序
|
libsvn_wc
|
工作版本(副本)管理器
|
mod_dav_svn
|
将WebDAV操作映射到Subversion的Apache模块
|
在上面的表中,单词"miscellaneous"只出现了一次是个好事情。Subversion的开发团队很严谨的让各种功能放合适的层和库中。模块化设计最大的好处是,从开发者的角度看,它不复杂。作为一个开发者,你可以很快地找到你所需要的功能的位置。下面的图表表示,Subversion库是怎样组装在一起的。
图表:Subversion的层的结构
使用模块化设计的另外一个好处是:我们可以用一个全新的库去替换一个指定的模块,在不影响其它的代码的情况下,实现相同的API。其实这种情况已经发生过了。Libsvn_ra_dav, libsvn_ra_local 和实现的是相同的接口。三个库都与存储层通信,其中Libsvn_ra_dav和libsvn_ra_svn通过网络通信,而libsvn_ra_local则是直接连接。
Subversion的客户端的设计也是高度模块化的。目前已经可以使用的客户端只是个基于命令行的程序,但是有很多第三方已经在开发GUI程序了。这些客户端都是使用着相同的API函数,对于大多数的客户端程序设计来说,libsvn_client一个库就足够了。
存储层
与存储层有关的是两个库,一个是repository库,一个是filesystem库。这些库提供了存储和那些版本控制数据的修订报告。这个层是通过存储访问层连接client层的,而且从用户的角度来看,素材来自网络的另外一端。
Subversion文件系统可以通过libsvn_fs API函数访问,这个文件系统其实是一个虚拟的文件系统,不是那种在操作系统中使用的真正的文件系统。在这里所谓的存储,不是真的创建一个文件或者目录,相反,它是使用一个数据库系统作为后台。当前使用的数据库系统是BDB。但是随着以后的开发,它将与其他的数据库系统兼容,也许还可以通过ODBC访问。
这个文件系统的API函数提供了在大多数的文件系统中能够使用的功能:你可以创建、移动文件和目录,复制和移动它们,编辑文件的内容等等。某些功能还需要继续完善,比如:添加、删除和移动文件或文件夹的原数据(属性)。此外,这个文件系统还有版本控制功能,就是说,当你改变了目录树以后,它能够记忆在更动以前,或者以前以前的目录树的样子—一直到最初的样子。
你对你的目录树的任何更动都是在一个Subversion事务中完成的。下面是一个简化的例子,用于描述如何编辑你的文件系统:
1、 Begin:启动一个Subversion事务;
2、 进行修改(添加、删除、属性编辑等);
3、 Commit:提交事务。
一旦你提交了事务,对文件系统的修改将永远地存储在historical artifacts中。每次这样的操作都会形成一个单一的新的目录树的修正版本,而且每个版本都有一个永远存在的snapshot,这样你就总是可以访问到它们。
有关事务的解释
在靠近数据库代码的libsvn_fs库中使用事务,特别容易将它与数据库系统本身使用的事务混淆。这两种事务都支持原子操作和事务隔离。换句话说,使用事务,可以让你的一组操作要么全部成功,要么全部失败,如果失败,就像什么事情也没有发生过一样—这样做,也不会影响到其它过程对数据的操作。
数据库事务通常只处理针对数据库自身一些小的操作。而Subversion的事务的范围要大一些,它处理一些高层次的操作,像修改一个目录和文件的集合,并生成文件系统库新的版本。如果你还没有明白,就这样考虑,在使用数据库事务前,需要先创建Subversion事务(这样,如果Sunversion事务创建失败,数据库根本就不会创建任何事务)。
对用户来说幸运的是:在文件系统的API函数中,数据库系统自己的事务已经几乎完全被隐藏了。只有在你深入挖掘文件系统具体的实现的时候,它才有意义。
与一般的文件系统相似,你要使用Subversion的文件系统,访问或者描述一个修正版本的文件或目录的时候,同样是使用路径字符串,像/foo/bar这样,就像你在某个经常使用中的shell程序中进行的操作一样。通过给恰当的API函数传递文件或目录的名称,你就可以创建一个新的文件或目录。查询信息的时候也是一样。
与大多数的文件系统不同的是,仅仅一个路径并不能提供足够的描述一个文件或者文件夹的信息。普通的文件夹结构是下面这样的(以一个两层的结构为例):
图表:2层结构的目录结构
Subversion的文件系统有一个属于自己的属性,那是其他的文件系统所没有的,这就是“时间”。在这个文件系统的接口,几乎每个有path参数的函数,都同时需要一个root参数。这个svn_fs_root_t参数即描述了一个修订版本也描述了一个事务,并且提供了关于版本32的/foo/bar与版本98的/foo/bar的不同。下面的图表显示了这个特性:
图表:修订时间,Subversion的特性
正如前面所说的,lib_svn的API函数与其他的文件系统很相似,只是它有很有趣的版本控制能力。使用这些API函数已经能够满足基本的对文件和目录版本控制的要求,尽管如此,SubVersion还是需要更多的功能,这样就引入了libsvn_repos库。
Libsvn_repos基本上就是文件系统函数库的封装。这个库负责创建仓储层,确保文件系统是否初始化等等。这个库也提供一些钩子脚本,这些脚本在某些行为发生的时候,被存储代码执行。这些脚本可以用于通知、授权,以及仓库管理员想要做的其他事情。这些功能与版本控制系统并没有直接的关系,所以它们被放置在单独的库中。
使用libsvn_repos库的开发人员会发现,它并没有完全封装文件系统的全部接口。只有一些主要的文件系统的活动才会被存储接口封装。其中包括创建和提交Subversion事务,以及对修订版本的修改。在以后,剩余的事件也将被封装起来。没有封装的还是要直接调用libsvn_fs API函数。
下面是一个代码片断,它同时使用了存储和文件系统接口,通过增加一个目录,创建了一个新的文件系统修订版本。注意,在这个例子里面(包括以后所有的例子),SVN_ERR宏简单地检测来自某个函数的未成功的错误,如果存在错误就返回它。
例:使用存储层
/* 在REPOS_PATH目录中(Subversion仓库目录),创建一个新的目录NEW_DIRECTORY。在POOL中进行所有的内存分配。这个函数会创建一个增加了NEW_DIRECTORY的修正版本。*/
static svn_error_t * make_new_directory (const char *repos_path,
const char *new_directory,
apr_pool_t *pool)
{
svn_error_t *err;
svn_repos_t *repos;
svn_fs_t *fs;
svn_revnum_t youngest_rev;
svn_fs_txn_t *txn;
svn_fs_root_t *txn_root;
const char *conflict_str;
/* 打开仓库中 REPOS_PATH目录 */
SVN_ERR (svn_repos_open (&repos, repos_path, pool));
/* 获取存储在REPOS中的文件系统对象的指针*/
fs = svn_repos_fs (repos);
/*查询文件系统,获得当前存在的最新的修订版本*/
SVN_ERR (svn_fs_youngest_rev (&youngest_rev, fs, pool));
/* 基于YOUNGEST_REV启动一个事务。因为我们是基于文件系统最新的Snapshot,因此在后面提交事务的时候,发生冲突的可能性就少很多*/
SVN_ERR (svn_fs_begin_txn (&txn, fs, youngest_rev, pool));
/* 现在,我们已经启动了一个新的Subversion事务,先获得一个表示这个事务的root对象 */
SVN_ERR (svn_fs_txn_root (&txn_root, txn, pool));
/*在事务的Root下创建一个新的目录 NEW_DIRECTORY */
SVN_ERR (svn_fs_make_dir (txn_root, new_directory, pool));
/* 提交这个事务,创建一个包含了我们新添加的目录的新的修正版本*/
err = svn_repos_fs_commit_txn (&conflict_str, repos,
&youngest_rev, txn);
if (! err)
{
/* 没有错误?很好,输出一个简要的报告*/
printf ("Directory '%s' was successfully added as new revision "
"'%" SVN_REVNUM_T_FMT "'./n", new_directory, youngest_rev);
}
else if (err->apr_err == SVN_ERR_FS_CONFLICT))
{
/* Uh-oh. 因为发生了某个冲突,我们的提交失败了(我们正在更改的区域,可能有别的人先更改了)。输出错误信息。 */
printf ("A conflict occurred at path '%s' while attempting "
"to add directory '%s' to the repository at '%s'./n",
conflict_str, new_directory, repos_path);
}
else
{
/* 出现了其他错误,输出错误信息 */
printf ("An error occurred while attempting to add directory '%s' "
"to the repository at '%s'./n",
new_directory, repos_path);
}
/* 返回结果 */
return err;
}
在前面的代码片断中,我们调用了repository和filesystem的接口。我们可以简单地使用svn_fs_commit_txn提交一个事务,但是,文件系统的API并不知道repository库的hook机制。如果你想同时自动执行一些与Subversion无关的任务(例如发送一个邮件给别人,告诉他们你做的改动),那么,就必须使用libsvn_repos封装的提交函数—svn_repos_fs_commit_txn。这个函数首先会执行一个“pre-commit”hook脚本(如果有的话),然后提交事务,最后会执行一个“post-commit”hook脚本。这个hook提供了一种并不真正属于核心的文件系统库的报告机制。(参阅HOOK Script章节)
Hook机制是将repository库从filesystem代码中分离出来的原因之一。Libsvn_repos API函数还提供了其它几个重要的功能,其中包括:
1. 在Subversion repository和包含在repository中的filesysytem中,创建、打开、销毁和执行recovery steps。
2. 描述在两个文件系统树中的不同。
3. 查询所有存在文件被编辑的修订版本的提交日志的信息。
4. 产生一个可读的文件系统的“dump”,这是在文件系统中修订版本的完整描述。
5. 解析dump的格式,并将一个“dump”出来的修正版本导入到一个不同的Subversion repositiry中。
Repository 访问层
如果Subversion Repository层是在“line的底端”,那么Repository Access层就是line本身。它负责负载着在client库和Repository之间的数据。这个层包含了libsvn_ra模块导入库,RA模块本身(当前包括libsvn_ra_dav, libsvn_ra_local,和libsvn_ra_svn库),以及被这些RA模块所需要的其它的库,像mod_dav_svn ,或者libsvn_ra_svn的服务svnserve。
Subversion使用URL标示存储资源,因此,协议的URL架构部分(通常是file:、http:、https:、或svn:)用于确定用哪个RA模块处理通信。每个模块都注册了一个它所知道如何“talk”的协议列表,因此,RA loader可以在运行时确定究竟使用哪个模块处理任务。你可以执行SVN—version,确定哪个RA模块在Subversion命令行客户端可用,以及它们都支持什么协议(例子如下):
$ svn --version
svn, version 0.25.0 (dev build)
compiled Jul 18 2003, 16:25:59
Copyright (C) 2000-2003 CollabNet.
Subversion is open source software, see http://subversion.tigris.org/
The following repository access (RA) modules are available:
* ra_dav : Module for accessing a repository via WebDAV (DeltaV) protocol.
- handles 'http' schema
* ra_local : Module for accessing a repository on local disk.
- handles 'file' schema
* ra_svn : Module for accessing a repository using the svn network protocol.
- handles 'svn' schema
RA-DAV(使用HTTP/DAV的Rrpository Access)
当客户端机器运行在与服务器不同的机器上,使用包含着http:或https:协议的URL机制进行通信的时候,可以使用这个库。要明白这个模块如何工作,我们必须先说明其它的一些在Repository Access层中配置的关键部分—强大的Apache HTTP 服务器和Neon HTTP/WebDAV client库。
Subversion主要的网络服务是Apache HTTP服务。Apache是一个久经考验的,可扩展的源代码开放服务器。它可以承受很高的网络负荷,并且可以运行在多个平台上。Apache服务器支持很多的标准的验证协议,并且可以通过加载其它的模块进行扩展。它也支持网络pipeling和缓存优化。通过使用Apache服务器,Subversion获得了所有的这些性能。另外,由于大多数的防火墙是允许Http协议的,因此,系统管理员不必为了让Subversion工作就更改防火墙的配置。
Subversion使用HTTP和WebDAV(与DeltaV一起)与Apache服务器通信。在本章的WebDAV节中有关于它的详细解释,简单地说,WebDAV和DeltaV是标准HTTP1.1版本的扩展,可以支持Web上文件的共享和传输。Apache2.0本身有mod_dav模块,这个模块可以理解HTTP的DAV扩展。Subversion本身支持mod_dav_svn模块,它是另外一个Apache模块,作为后台与mod_dav协同工作,以便支持WebDAV和DeltaV的特殊操作。
当通过HTTP访问Repository的时候,RA Loader库选择libsvn_ra_dav作为访问模块。Subversion客户端调用通用的RA接口,libsvn_ra_dav将这些调用影射为一组HTTP/WebDAV request。通过使用Neon库,libsvn_ra_dav将这些request传输到Apache服务器。Apache接收到以后,发现这些request定位的URL配置为DAV location(在http.conf中使用Location指示),就会将这些request剥离自己的mod_dav模块。如果配置是正确的,mod_dav会使用Subversion自己的mod_dav_svn处理相关的文件系统需求,而不是使用Apache中自带的通用mod_dav_fs。最后的结果是:客户端直接与mod_dav_svn通信,而后者是直接绑定在Subversion Repository层上的。
这是一个实际发生的事情的简要介绍。例如:Subversion Repository可能不被Apache的权限系统所保护。这样,在客户端连接的时候,可能会被Apache的权限系统所拒绝。这个时候,libsvn_ra_dav会从Apache获取权限不足的信息,返回客户端获取更新的授权数据。如果能够正确提供数据,用户就具有了查询Apache的权限,libsvn_ra_dav下一步自动尝试执行的命令将被接收,一切OK。如果权限信息不充分,request最终将会失败,客户端将会给用户报告错误。
通过使用Non和Apache,Subversion在一些复杂的领域也有充分的处理能力。比如,如果Neon发现OpenSSL库,它将允许客户端使用SSL加密协议与Apache服务器通信(Apache服务器自己有mod_ssl“认识这个语言”)。另外,Neon本身和Apache的mod_deflate都能够理解“deflate”算法,因此可以压缩request,使传输的数据更小。以后还将支持重定向服务。
RA-SVN(专用得Respository Access协议)
作为标准的HTTP/WebDAV协议的补充,Subvesion还提供了一个专用的协议。Libsvn_ra_svn模块使用自己的Socket,并且与一个独立的服务器通信(svnServer程序),这个服务器与Repository在一个主机上。客户端使用svn://结构访问Repository。
这个机制缺乏上面一节中使用Apache服务器的大部分的优点,但是,它对某些系统管理员来说可能有一定的吸引力。它很容易配置并运行,安装一个svnServer进程速度很快。相比于Apache来说,它也小很多,很容易检查。另外,一些系统管理员已经有了自己的SSH底层结构,而且希望Subversion使用它。使用ra_svn的客户端很容易调整为基于SSH协议。
RA-Local(直接访问Respository)
不是所有对Repository的通信都需要一个服务器进程和一个网络层。对于只想在本地磁盘上访问Repository的用户,只需要使用file:URL格式,以及libsvn_ra_local库提供的功能。这个模块直接绑定了Repository和filesystem库,因此完全不需要网络通信。
Subversion要求在file:中包含的服务器名称或者是空,或者是localhost,而且不需要通信端口。你的URL应该看上去是这样的:file://localhost/path/to/repos或者file:///path/to/repos。
需要注意的是,file:这种URL不能在正规的web浏览器中使用,否则浏览器将直接在本地的文件系统中查询这个文件,但是,Subversion的文件系统实际上是一个虚拟的文件系统,你的浏览器并不能理解这个系统。
你自己的RA库
如果你还想用另外一个协议,那正好。这就是为什么要设计一个Repository Access层的原因!开发人员可以重新设计一个网络层,一端实现RA接口,另外一端实现与Repository的通信。新的库可以使用现成的协议,你也可以自己发明一个。你可以用IPC或者let’s get crazy, shall we? 设置你可以设计一个基于email的协议。Subversion提供API函数,而你负责提供创造力。
Client层
在客户端,客户端库的大多数功能是用来管理工作版本,包括容纳文件的目录和作为本地服务存在的子目录,可以编辑Repository的本地“影像”,并且反映来自/发送到Repository Access层的改变。
Subversion的工作版本库—libsvn_wc,它的责任就是管理工作版本的数据。为了完成这个功能,这个库在一个特定的子目录中存储了所有的工作版本目录的管理信息。这个子目录的名称是.svn,存放在每个工作版本的目录内,而且包含了一些文件和子目录,用于记录状态和提供一个私有的用于管理的工作空间。与CVS很相似的是,在CVS工作目录中也有一个CVS目录。
Subversion的客户端库,libsvn_client的功能非常广泛;它混合了工作版本库与Repository Access层的功能,然后提供了最高层的API函数,可以用于任何想执行修订版本控制功能的应用程序。例如:函数svn_clinet_checkout有一个URL参数。它将一个URL传递到RA层,并且打开针对特定的Repository的authenticated session。然后它向Repository查询某个特定的树,并将这个树发送到工作版本库,工作版本库将把整个的工作版本写入到磁盘上。
客户端库的设计目的就是用于应用程序。虽然Subversion源代码中包含一个基于命令行的客户端,但是在客户端库的基础上设计一个GUI客户端是很容易的。新的GUIs没有必要封装或包含命令行的客户端,它可以使用libsvn_client API函数访问与命令行客户端相同的功能,数据和回调机制。
Binding Directly(直接绑定)
为什么你的GUI程序应该直接绑定libsvn_client库而不是去封装一个命令行的客户端程序?除了效率问题,它还关系到潜在的正确性问题。一个绑定了客户端库的命令行程序并须有一个高效的翻译引擎,可以将C的数据转换成可读的信息,最后输出的是这个可读的信息。而在这个转换过程可能会造成数据的损耗。就是说,命令行程序输出的可能并不是API函数获取的全部的信息,或者为了表达得更加紧凑,将一些信息合并起来了。
如果你封装了这样一个命令行程序,那么封装它的程序能够得到的信息,就是那些经过这个命令行客户端解析过的信息(就象前面所说,这些信息可能使不完整的),而这样的信息又会再次按照新的程序的意图进行解析。每逢装一层,原始信息的完整性就可能损失一些。
使用API函数
使用Subversion的API函数开发应用程序很简单。所有公用的头文件都保存在源代码树subversion/include目录中。当你构建和安装Subversion的时候,这些头文件将复制到你的系统目录下。这些头文件中,显示了所有的用户可以访问的Subversion库中的函数和数据类型。
你需要注意的第一件事情是,Subverion的数据类型和函数是受命名空间保护的。任何一个公用的Subversion符号的名称都是从svn_开始,后面是这个名称所在库的名称,如“wc”、“client”、“fs”等,然后是一个下划线,剩下的部分是符号的名称。
Semi-public函数(在给定的库的源代码文件中使用,但是并不是库的输出编码,可以在库目录中找到的函数)与上面的命名架构不同,它是在库的代码后面加双下划线(“__”)。源文件私有的函数并没有特殊的前缀,而且被声明为static。当然了,编译器对这些命名规范并没有什么兴趣,但是它可以帮助我们区分这些函数和数据类型的使用范围。
Apache轻量级运行时库
除了Subversion自己的数据类型,你还可以看到一些引用的数据类型,它们用“apr_”开头,这些符号来自于Apche 轻量级运行时(APR)库。这是来自于将依赖于特定平台的代码从服务器中分离出来的努力,结果就是一个提供了通用的,跨平台的API函数,虽然Apache是第一个使用这个库的,但是Subversion的开发人员立即发现了使用ARP的价值。这意味着,在Subversion本身没有依赖特定平台的代码。当然这意味着,Subversion的客户端和服务器可以在任何地方编译运行。目前它支持的平台包括:Unix,Win32、BeOS、OS/2和Mac OS X。
除了提供跨平台调用外,APR还给Subversion提供了对某些自定义数据类型的访问权限,像动态数组和hash表。Subversion在代码中广泛地使用了这些数据类型。但是可以几乎在Subversion代码中随处可见的数据类型是ARP内存池—apr_pool_t。Subversion在需要分配内存的地方都使用pool(除非一个扩展库需要不同的数据管理结构,用于在API中传递它),虽然在使用API编码的时候不需要这样做,但是要求用户给需要的API函数提供pool。这意味着,使用API函数的用户也必须连接到APR,必须调用apr_initialize()函数来初始化APR子系统,然后必须获取一个pool用于API调用。
URL和路径的要求
远程的版本控制操作贯穿了整个的Subversion,这样,国际化的问题就值得关注了。当然,远程的概念可能指“整个办公室”,但是也可能是指“全球”。为了解决这个问题,所有的接受路径为参数Subversion公用接口,都要求路径遵循UTF-8规范。这就意味着,新的、用livsvn_client驱动的客户端程序,在将路径传递给Subversion库的时候,必须将本地的路径格式转化为UTF-8格式,而且在接收库输出的路径的时候,还要将UTF-8标准的路径格式再转化为本地格式。幸运的是,Subversion提供了一组函数,(参考:subversion/include/svn_utf.h)可让任何程序完成这种转换。
同样,Subversion API函数要求所有的URL参数都是URI编码。因此,对于这个URL:file:///home/username/my file.txt,它的文件名称是my file.txt,必须转换为:file///home/ysername/my%20file.txt。另外,Subversion也提供了两个帮助函数:-svn_path_url_encode和svn_path_url_decode,分别用于编码和译码。
在工作版本管理区域内部
如同我们前面所说的,在每个工作版本目录下都存在一个子目录.svn,它用于存放工作版本的管理信息。Subversion使用这些信息跟踪以下事务:
· 在工作版本目录中文件或子目录的Repository位置。
· 当前在工作版本的文件和目录的修订版本。
· 任何可以附加到这些文件和目录的,用户定义的属性。
· 原始的(没有编辑过)的工作版本文件副本。
如果有几个不同的数据存储在.svn目录中,我们将使用最重要的几个项目。
Entries文件
也许在.svn目录中最重要的文件就是entries文件。这个文件是一个XML文档,其中包含了大量的关于工作版本的版本资源管理信息。这个文件用于跟踪Repository 的URLs,原始的修订版本,文件的checksums,原始文本和属性时间戳,时序安排和冲突状态信息,最后一次提交的信息(作者,修订版本、时间戳),本地副本的历史记录—几乎Subversion客户端要了解的一切关于版本控制的资源在这里都可以找到!
以下是一个实际的entries文件的例子:
Example 典型的.svn/entries文件
xmlns="svn:">
committed-rev="1"
name="svn:this_dir"
committed-date="2002-09-24T17:12:44.064475Z"
url="http://svn.red-bean.com/tests/.greek-repo/A/D"
kind="dir"
revision="1"/>
committed-rev="1"
name="gamma"
text-time="2002-09-26T21:09:02.000000Z"
committed-date="2002-09-24T17:12:44.064475Z"
checksum="QSE4vWd9ZM0cMvr7/+YkXQ=="
kind="file"
prop-time="2002-09-26T21:09:02.000000Z"/>
name="zeta"
kind="file"
schedule="add"
revision="0"/>
url="http://svn.red-bean.com/tests/.greek-repo/A/B/delta"
name="delta"
kind="file"
schedule="add"
revision="0"/>
name="G"
kind="dir"/>
name="H"
kind="dir"
schedule="delete"/>
正如你所见,entries文件本质上是一个项目列表。一个项目标签描述了3件事情:工作版本本身的路径(name属性设置为“svn:this-dir”),表示这是一个位于工作目录中的文件(kind 属性设置位file),或者这是一个在工作版本中的目录(kind属性设置为“dir”)。存储在这个文件中的文件或者子目录可以是原来就在版本控制之下的,也可以是在工作版本中新增加的,在下一次提交工作版本的修改的时候,要加入版本控制的(在上面的例子中,文件的名称标签为zate的就是这种情况)。每个项目都有一个唯一的名称,而且每一个项目都有一个节点类型。
开发人员在读写entries文件的时候,要注意Subversion使用的一些特殊的规则。虽然每个项目都有一个revision和URL,注意并不是所有的entry标签都有recision和url属性。当这两个属性的值与项目中的“svn:this-dir”相同,或者可以通过它简单地计算出来的时候,Subversion允许不明确地指定这两个值。注意,同样的,对于子目录项目来说,Subversion值存储了至关重要的name,kind,revision和schedule属性。为了减少信息的冗余,Subversion规定,要获得子目录的全部信息,可以深入这个子目录,在它自己的.svn/ertries文件中的“svn:this-dir”项目中获取相应的信息。然而,如果饮用了一个保存在父entries文件中的子目录,当这个子目录实际上并不在硬盘上了,它还是有足够的信息执行基本的版本控制操作。
原始副本和属性文件
如前面所说的,.svn目录中还保存了原始的,给予文本的文件的版本。它们可以在.svn/text-base中找到。原始副本的好处是多样的—可以脱离网络检测本地的修改和“diff”报告,脱离网络恢复修改过的,或者删除的文件,只给服务器发送修改过的内容—但是坏处是每个修正版本都需要在磁盘上存储两次。这样的问题对于大多数文件来说,可以忽略不计。
与“text-base”相似的是属性文件和它们的原始的“prop-base”副本,它们分别位于.svn/props和.svn/prop-base文件中。因为目录也可以由属性,因此还存在.svn/dir-props和.svn/dir-prop-base文件。每个属性文件(工作版本或者基础版本)都使用简单的“hash-on-disk”文件格式来存储属性的名称和它们的值。
WEBDAV
WebDAV("Web-based Distributed Authoring and Versioning")是一个标准的HTTP协议的扩展,主要的设计目的是,让web可以读写,从而改变现在只读的状态。原理是文件和目录可以通过Web共享为可读写的对象。REC2518和3253描述了WebDAV/DelteV对HTTP的扩展。有关它的详细信息可以在www.webdav.org中得到。
很多操作系统的浏览器都已经能够使用WebDAV装载网络目录。在Win32上,IE可以像普通的共享文件夹一样显示一种“WebFolders”。Mac OS X也有这种功能。
那么这些怎么应用在Subversion上呢?mod_dav_svn Apache模块使用WebDAV和DeltaV扩展的HTTP作为它主要的网络协议。Subversion的最初的设计人员决定直接采用RFCs2518和3253实现Subversion的功能,而不是去创建一个新的私有协议。
使用内存池编程
几乎每个使用C设计程序的开发人员,对内存管理都很头疼。如何分配足够的内存,跟踪它们,并且在不需要的时候释放它们是个很复杂的事情。如果处理不好就可能使程序崩溃,甚至更糟,让计算机崩溃。幸运的是,Subversion所使用的APR库中提供了一个apr_pool_t数据类型,它提供了一个内存池。
内存池是分配给程序使用的内存块的抽象概念。与其直接要求操作系统分配一块内存(使用mallc()函数),连接到APR的程序不如简单地要求创建一个内存池(使用apr_pool_create()函数)。APR将从操作系统分配一块适当大小的内存,这块内存程序可以立即使用。在任何时候,在程序需要池中的内存的时候,只要使用APR pool API 函数之一:apr_palloc(),就会返回池中的一个普通的内存位置三。程序可以持续地请求一点或者一块内存,而APR总是会满足这些请求。因为程序的不断申请,内存池的大小会逐渐增加,直到系统没有内存可以分配为止。
如果上面所说的是内存池的全部特性,那么不会有多少人感兴趣的,幸运的是,事情不是这样的 。Pool不仅仅能被创建,而且可以分别使用apr_pool_clear()和apr_pool_destory()清除和销毁Pool。这就给了程序员一个很大的自由,你可以在内存池中创建几个,甚至几千个事务,然后用一个函数就全部清除它们!以后,Pool会分层次,你可以在一个pool中创建Subpool,在清除或者销毁Pool的时候,Subpool会同时被清除和销毁。
在进行下一步之前,开发人员应该注意,我们前面所提及的Pool函数,在Subveresion中可能找不到。这是因为ARP Pool 提供了一些附加的机制,如可以将自定义的“user data”附加到Pool,在销毁Pool的时候,调用寄存器清除函数的机制等。Subversion有时候需要使用这些扩展。因此,Subversion提供了封装函数:svn_pool_vreate(),svn_pool_clear()和svn_pool_destory。
Pool在基本的内存管理中有很大的用处,Pool的结构在循环和递归中非常有用。因为循环的次数和递归的层次一般来说都是不可预知的。而使用嵌套的Pool可以很容易地管理这些情况。
下面的例子,用来示范,在一个递归的,不规则的目录树中,对树中的所有分支都执行一个操作的权威方法,在这里面使用了嵌套的Pools。
例子: 有效地使用Pool
/* 遍历目录DIRECTORY,将所有的文件子目录添加到FILES数组中,对每个目录执行一些任务。使用Pool作为所有的临时存储,而且在相同的Pool中存储hash paths。*/
static apr_status_t
crawl_dir (apr_array_header_t *files, const char *directory,
apr_pool_t *pool)
{
apr_pool_t *hash_pool = files->pool; /* 数组 pool */
apr_pool_t *subpool = svn_pool_create (pool); /*反复使用的 pool */
apr_dir_t *dir;
apr_finfo_t finfo;
apr_status_t apr_err;
apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;
apr_err = apr_dir_open (&dir, directory, pool);
if (apr_err)
return apr_err;
/* 遍历目录实体,在每次循环清空Subpool*/
for (apr_err = apr_dir_read (&finfo, flags, dir);
apr_err == APR_SUCCESS;
apr_err = apr_dir_read (&finfo, flags, dir))
{
const char *child_path;
/* 跳过子目录('.')和父目录('..') */
if (finfo.filetype == APR_DIR)
{
if (finfo.name[0] == '.'
&& (finfo.name[1] == '/0'
|| (finfo.name[1] == '.' && finfo.name[2] == '/0')))
continue;
}
/*利用DIRECTORY和FINFO.name构建CHILD_PATH*/
child_path = svn_path_join (directory, finfo.name, subpool);
/* 针对该目录执行一些任务 */
do_some_task (child_path, subpool);
/* 利用递归处理子目录,将Subpool传递进去,用于临时存储*/
if (finfo.filetype == APR_DIR)
{
apr_err = crawl_dir (files, child_path, subpool);
if (apr_err)
return apr_err;
}
/* 将文件的目录添加到FILES数组中*/
else if (finfo.filetype == APR_REG)
{
/* 将文件的路径复制到FILES数组pool中*/
child_path = apr_pstrdup (hash_pool, child_path);
/* 将路径添加到数组中 */
(*((const char **) apr_array_push (files))) = child_path;
}
/* 清除每个反复使用的SUBPOOL. */
svn_pool_clear (subpool);
}
/* 检测循环正常退出 */
if (apr_err)
return apr_err;
/* 正常退出则关闭dir */
apr_err = apr_dir_close (dir);
if (apr_err)
return apr_err;
/* 销毁SUBPOOL. */
svn_pool_destroy (subpool);
return APR_SUCCESS;
}
上面的例子,示范了在循环和递归两种情况下,Pool的使用方法。每个递归都需要将Pool的Subpool传递到函数中。这个subpool则用于循环,而且在每次循环开始的时候清空。Pool对于Subversion是非常重要的,作为一个Subversion的开发人员,应该增长这方面的能力。
一个不错的SVN中文指南:http://i18n-zh.googlecode.com/svn/www/svnbook-1.4/index.html