第 8 章 开发者信息

Subversion是一个开源的软件项目,使用Apache样式的软件许可证。这个项目由位于加利福尼亚的CollabNet, Inc.软件开发公司资助。这个关于Subversion开发的社区一直欢迎新成员贡献自己的时间和注意力。鼓励志愿者做他们能做的任何帮助,不管是发现和诊断bug,精炼已存的代码还是补充新的特性。

本章是为那些希望实际参与源代码编写来帮助Subversion不断进步的人们准备的。我们要知道,在这里我们会涉及到许多软件内在的细节,在开发Subversion本身—或利用Subversion库开发全新工具时—所用到的许多核心技术。如果你无法预测你是否会以这种层级参与到这个软件中来,那么也可以随意跳过这一章,而你作为一个Subversion用户的体验不会受到任何影响。

Subversion有一个模块化的设计,通过一套C库[42]来实现。每一个库都有一套定义良好的目标与接口,据称,大部分模块都属于三层中的某一层—版本库层、版本库访问(RA)层或是客户端层。我们很快就会考察这些层,但首先让我们看一下表 8.1 “Subversion库的摘要目录”中的有关于Subversion库的摘要目录,为了一致性,我们将通过它们的无扩展Unix库名(例如libsvn_fs、libsvn_wc和mod_dav_svn)来引用它们。

单词“各色各样的”只在列表表 8.1 “Subversion库的摘要目录”中出现过一次是一个好的迹象。Subversion开发团队非常注意将功能归入合适的层和库,或许模块化设计最大的好处就是从开发者的角度看减少了复杂性。作为一个开发者,你可以很快就描画出一副“大图像”,以便于你更精确地,也相对容易地找出某一功能所在的位置。

模块化的另一个好处是我们有能力去构造一个全新的,能够完全实现相同API功能的库,以替换整个给定的模块,而又不会影响基础代码。在某种意义上,Subversion已经这样做了。libsvn_ra_dav、libsvn_ra_local和libsvn_ra_svn all都实现了相同的接口,三者均与版本库层进行通讯—libsvn_ra_dav和libsvn_ra_svn通过网络,而libsvn_ra_local则是直接连接,libsvn_fs_base和libsvn_fs_fs是另外两个例子。

客户端设计本身就给模块化设计理念增色不少,Subversion本身只是附带了一个命令行方式的客户端,但已经出现了一些由第三方开发的不同GUI的客户端,这些GUI客户端程序全都使用了与原装命令行客户端程序相同的API。为了开发一个实用的Subversion客户端程序,对于绝大部分功能,仅使用Subversion的libsvn_client库就够了(见“客户端层”一节)。

当提到Subversion版本库层时,我们通常会讨论两个库—版本库(函数)库和文件系统(函数)库。这两个库为你的版本控制数据的各个修订版本提供了存储和报告机制,该层通过版本库访问层连接到客户层,而且,从Subversion用户的角度看,这是资料存储过程中的“链接的另一端”。

Subversion文件系统通过libsvn_fs API来访问,它并不是一个安装在操作系统之上的内核级的文件系统(例如Linux ext2或NTFS),而是一个虚拟文件系统。它并未将“文件”和“目录”保存为真实的文件和目录(也就是用你熟知的shell程序可以浏览的那种),而是采用了一种抽象的后端存储方式,这个后端存储方式有两种—一个是Berkeley DB数据库环境,另一个是普通文件表示。(要了解更多关于版本库后端的信息,请看“版本库数据存储”一节)。除此之外,开发社区也非常有兴趣考虑在Subversion的未来版本 中提供某种使用其它后端数据库系统的能力,也许是开放式数据库连接(ODBC)的机制。

libsvn_fs支持的文件系统API包含了所有其他文件系统的功能:你可以创建和删除文件和目录、拷贝和移动、修改文件内容等等。它也包含了一些不太常用的特性,如对任意文件和目录添加、修改和删除元数据(“properties”)的能力。此外,Subversion文件系统是一个版本化的文件系统,意味着你修改你的目录树时,Subversion会记住修改以前的样子。等等,可以回到所有初始化版本库之后(且仅仅之后)的版本。

所有你对目录树的修改包含在Subversion事务的上下文中,下面描述了修改文件系统的例程:

  1. 开始Subversion事务。

  2. 作出修改(添加、删除、属性修改等等。)。

  3. 提交事务。

一旦你提交了你的事务,你的文件系统修改就会永久的作为历史保存起来,每个这样的周期会产生一个新的树,所有的修订版本都是永远可以访问的一个不变的快照。

大多数文件系统接口提供的功能作为一个动作发生在一个文件系统路径上,也就是,从文件系统的外部,描述和访问文件和目录独立版本的主要机制是经过如/foo/bar的路径,就像你在喜欢的shell程序中定位文件和目录。你通过传递它们的路径到相应的API功能来添加新的文件和目录,查询这些信息也是同样的机制。

然而,不像大多数文件系统,一个单独的路径不足以在Subversion定位一个文件或目录,可以把目录树看作一个二维的系统,一个节点的兄弟代表了一种从左到右的动作,并且递减到子目录是一个向下的动作,图 8.1 “二维的文件目录”展示了一个典型的树的形式。

当然,Subversion文件系统有一个其它文件系统所没有的第三维—时间! [43] 在一个文件系统接口,几乎所有的功能都有个路径参数,也期望一个root参数。svn_fs_root_t参数不仅描述了一个修订版本或一个Subversion事务(通常正好是一个修订版本),而且提供了用来区分修订版本32的/foo/bar和修订版本98在同样路径的三维上下文环境。图 8.2 “版本时间—第三维!”展示了修订版本历史作为添加的纬度进入到Subversion文件系统领域。

像之前我们提到的,libsvn_fs的API感觉像是其它文件系统,只是有一个美妙的版本化能力。它设计为为所有对版本化的文件系统有兴趣的程序使用,不是巧合,Subversion本身也对这个功能很有兴趣。但是虽然文件系统API一定必须对基本的文件和目录版本化提供足够的支持,Subversion需要的更多—这是libsvn_repos到来的地方。

Subversion版本库库(libsvn_repos)是文件系统功能的一个基本包裹库,这个库负责创建版本库布局,确定底层的文件系统已经初始化等等。Libsvn_repos也实现了一组钩子—当特定动作发生时版本库执行的脚本。这些脚本用来通知,授权或者任何版本库管理员期望的目的。版本库库提供的这些功能和小工具与版本化的文件系统关系不大,所以放到了自己的库里。

希望使用libsvn_repos的API的开发者会发现它不是文件系统的一个完全包裹,只有文件系统常规周期中的主要事件使用版本库接口包裹,如包括Subversion事务的创建和提交,修订版本属性的修改。这些特别的事件使用版本库库包裹是因为它们有一些关联的钩子,在将来,别的事件也将会使用版本库API包裹。所有其它的文件系统交互会直接通过libsvn_fs的API发生。

举个例子,这里是使用版本库和文件系统接口创建文件系统新修订版本的代码块,新版本包括添加一个新目录。注意这个例子(和其它本书中的代码),这个SVN_ERR()宏只是简单的检查是否有一个非成功的错误从包裹的函数中返回,如果存在就会返回错误。

例 8.1. 使用版本库层

/* Create a new directory at the path NEW_DIRECTORY in the Subversion
   repository located at REPOS_PATH.  Perform all memory allocation in
   POOL.  This function will create a new revision for the addition of
   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;

  /* Open the repository located at REPOS_PATH.  */
  SVN_ERR (svn_repos_open (&repos, repos_path, pool));

  /* Get a pointer to the filesystem object that is stored in
     REPOS.  */
  fs = svn_repos_fs (repos);

  /* Ask the filesystem to tell us the youngest revision that
     currently exists.  */
  SVN_ERR (svn_fs_youngest_rev (&youngest_rev, fs, pool));

  /* Begin a new transaction that is based on YOUNGEST_REV.  We are
     less likely to have our later commit rejected as conflicting if we
     always try to make our changes against a copy of the latest snapshot
     of the filesystem tree.  */
  SVN_ERR (svn_fs_begin_txn (&txn, fs, youngest_rev, pool));

  /* Now that we have started a new Subversion transaction, get a root
     object that represents that transaction.  */
  SVN_ERR (svn_fs_txn_root (&txn_root, txn, pool));
  
  /* Create our new directory under the transaction root, at the path
     NEW_DIRECTORY.  */
  SVN_ERR (svn_fs_make_dir (txn_root, new_directory, pool));

  /* Commit the transaction, creating a new revision of the filesystem
     which includes our added directory path.  */
  err = svn_repos_fs_commit_txn (&conflict_str, repos, 
                                 &youngest_rev, txn, pool);
  if (! err)
    {
      /* No error?  Excellent!  Print a brief report of our success.  */
      printf ("Directory '%s' was successfully added as new revision "
              "'%ld'.\n", new_directory, youngest_rev);
    }
  else if ((err->apr_err == SVN_ERR_FS_CONFLICT)
    {
      /* Uh-oh.  Our commit failed as the result of a conflict
         (someone else seems to have made changes to the same area 
         of the filesystem that we tried to modify).  Print an error
         message.  */
      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
    {
      /* Some other error has occurred.  Print an error message.  */
      printf ("An error occurred while attempting to add directory '%s' "
              "to the repository at '%s'.\n", 
              new_directory, repos_path);
    }

  /* Return the result of the attempted commit to our caller.  */
  return err;
} 

在前面的代码片断中,同时调用了版本库和文件系统接口,我们可以正像这样简单的用svn_fs_commit_txn()提交事务。但是文件系统的API对版本库库的钩子一无所知,如果你希望你的Subversion版本库在每次提交一个事务时自动执行一些非Subversion的任务(例如,给开发者邮件组发送一个描述事务修改的邮件),你需要使用libsvn_repos包裹的功能版本—svn_repos_fs_commit_txn()。这个功能会实际上首先运行一个如果存在的pre-commit钩子脚本,然后提交事务,最后会运行一个post-commit钩子脚本。钩子提供了一种特别的报告机制,不是真的属于核心文件系统库本身。(关于Subversion版本库钩子的更多信息,见“钩子脚本”一节。)

钩子机制需求是从文件系统代码的其它部分中抽象出单独的版本库库的一个原因,libsvn_repos的API提供了许多其他有用的工具,它们可以做到:

  1. 在Subversion版本库和版本库包括的文件系统的上创建、打开、销毁和执行恢复步骤。

  2. 描述两个文件系统树的区别。

  3. 关于所有(或者部分)修订版本中的文件系统中的一组文件的提交日志信息的查询

  4. 产生可读的文件系统“导出”,一个文件系统修订版本的完整展现。

  5. 解析导出格式,加载导出的版本到一个不同的Subversion版本库。

伴随着Subversion的发展,版本库库会随着文件系统提供更多的功能和配置选项而不断成长。

如果说Subversion版本库层是在“这条线的另一端”,那版本库访问层就是这条线。负责在客户端库和版本库之间编码数据,这一层包括libsvn_ra模块加载模块,RA模块本身(现在包括了libsvn_ra_dav、libsvn_ra_local和libsvn_ra_svn),和所有一个或多个RA模块需要的附加库,例如与Apache模块mod_dav_svn通讯的libsvn_ra_dav或者是libsvn_ra_svn的服务器,svnserve

因为Subversion使用URL来识别版本库资源,URL模式的协议部分(通常是file:http:https:svn:)用来监测那个RA模块用来处理通讯。每个模块注册一组它们知道如何“说话”的协议,所以RA加载器可以在运行中监测在手边的任务中使用哪个模块。通过运行svn --version,你可以监测Subversion命令行客户端所支持的RA模块和它们声明支持的协议:

$ svn --version
svn, version 1.2.3 (r15833)
   compiled Sep 13 2005, 22:45:22

Copyright (C) 2000-2005 CollabNet.
Subversion is open source software, see http://subversion.tigris.org/
This product includes software developed by CollabNet (http://www.Collab.Net/).

The following repository access (RA) modules are available:

* ra_dav : Module for accessing a repository via WebDAV (DeltaV) protocol.
  - handles 'http' scheme
  - handles 'https' scheme
* ra_svn : Module for accessing a repository using the svn network protocol.
  - handles 'svn' scheme
* ra_local : Module for accessing a repository on local disk.
  - handles 'file' scheme

libsvn_ra_dav库是给在不同机器使用http:https:协议访问服务器的用户设计的,为了理解这个模块的工作,我们首先要知道这种版本库访问层中的特定配置的关键组成部分—强大的Apache HTTP服务器,和Neon HTTP/WebDAV客户端库。

Subversion的主要网络服务器是Apache HTTP服务器,Apache是久经考验的用来认真使用的开源服务器,它可以支撑很大的网络压力并且可以运行在多种平台。Apache服务器支持多种认证协议,而且可以通过模块扩展使用其它协议。它也支持流水线和缓存之类的网络优化。通过将Apache作为服务器,Subversion轻易得到这些特性。而且因为许多防火墙已经允许HTTP通过,系统管理员通常不需要改变防火墙设置来允许Subversion工作。

Subversion使用HTTP和WebDAV(和DeltaV)来与Apache服务器通讯,你可以在本章的WebDAV读到更多信息,但简而言之,WebDAV和DeltaV是标准HTTP 1.1协议的扩展,允许在web上对文件进行分享和版本操作。Apache的2.0及以后的版本都包含一个mod_dav,一个Apache理解HTTP DAV扩展的模块,Subversion本身提供了mod_dav_svn,尽管,这是另一个Apache模块,它与mod_dav结合(实际上mod_dav_svn是作为后端支持)使用来提供Subversion对WebDAV和DeltaV的实现。

当与版本库通过HTTP通讯时,RA加载器库选择libsvn_ra_dav作为正确的访问模块,Subversion客户端调用原始的RA接口,libsvn_ra_dav把这些调用(包含了大量Subversion操作)影射为一系列HTTP/WebDAV请求。使用Neon库,libsvn_ra_dav把这些请求传递到Apache服务器,Apache接受到这些请求(就像平时web服务器常做的那样处理原始的HTTP请求),注意到这些请求的URL已经配置为DAV的位置(使用httpd.conf<Location>指示),并且会使用自己的mod_dav模块来处理。当正确的配置了mod_dav使之知道了使用mod_dav_svn来处理所有文件系统相关的要求,而不是使用默认的Apache自带的原始mod_dav_fs来处理。所以最终客户端是与mod_dav_svn通讯,直接与Subversion版本库层绑定。

有一个实际交换发生的简单描述,举个例子,Subversion版本库可以使用Apache的授权指示进行保护。这会导致初始的与版本库的通讯会被Apache的授权基础拒绝,在此刻,libsvn_ra_dav将提供不足鉴定的通知返回,并且回调客户端层来得到一些更新的认证数据。如果数据是正确提供,而且用户有访问的权限,会赋予libsvn_ra_dav的下一个对原操作的自动尝试权限,并且一切会很好。如果足够的认证信息不能提供,请求会最后失败,客户端也会报告给用户失败信息。

通过使用Neon和Apache,Subversion在许多其它领域的轻易得到复杂的功能。举个例子,如果Neon找到OpenSSL库,它允许Subversion客户端尝试与Apache服务器(它自己的mod_ssl“可以说这个语言”)使用SSL加密的通讯。Neon本身和Apache的mod_deflate都可以理解“deflate”算法(PKZIP和gzip共同使用的程序),所以请求可以压缩块方式传输。其它Subversion今后希望支持的复杂特性包括,自动处理服务器重定向(举个例子,当版本库转移到一个新的规范URL)和利用HTTP流水线的能力。

在客户端这一面,Subversion工作拷贝是所有动作发生的地方。大多数客户端库实现的功能是为了管理工作拷贝的目的实现的—满是文件子目录的目录是一个或多个版本库位置的可编辑的本地“影射”—从版本库访问层来回传递修改。

Subversion的工作拷贝库,libsvn_wc直接负责管理工作拷贝的数据,为了完成这一点,库会在工作拷贝的每个目录的特殊子目录中保存关于工作拷贝的管理性信息。这个子目录叫做.svn,出现在所有工作拷贝目录里,保存了各种记录了状态和用来在私有工作区工作的文件和目录。对那些熟悉CVS的用户,.svn子目录与CVS工作拷贝管理目录的作用类似,关于.svn管理区域的更多信息,见本章的“进入工作拷贝的管理区”一节

Subversion客户端库libsvn_client具备最广泛的职责;它的工作是结合工作拷贝库和版本库访问库的功能,然后为希望普通版本控制的应用提供最高级的API。举个例子,svn_client_checkout()方法是用一个URL作为参数,传递这个URL到RA层然后在特定版本库打开一个会话。然后向版本库要求一个特定的目录树,然后把目录树发送给工作拷贝库,然后把完全的工作拷贝写到磁盘(.svn目录和一切)。

客户端库是为任何程序使用设计的,尽管Subversion的源代码包括了一个标准的命令行客户端,用客户端库编写GUI客户端也是很简单,Subversion新的GUI(或者任何新的客户端,真的)不需要紧密围绕包含的命令行客户端—他们对具有相同功能、数据和回调机制的libsvn_client的API有完全的访问权利。



[42] 译者:这里的“库(library)”指函数库,与文中大量出现的“版本库(Repository)”不同,一般情况下,作为独词出现的“库”应属于前者。

[43] 我们理解这一定会给科幻小说迷带来一个震撼,他们认为时间是第四维的,我们要为提出这样一个不同理论的断言而伤害了他们的作出道歉。