本文档旨在描述 Subversion 1.5。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的书籍版本。

仓库维护

维护 Subversion 仓库可能很艰巨,主要是因为具有数据库后端的系统固有的复杂性。做好这项工作关键在于了解工具——它们是什么,何时使用以及如何使用。本节将介绍 Subversion 提供的仓库管理工具,并讨论如何使用它们来完成诸如仓库数据迁移、升级、备份和清理等任务。

管理员工具包

Subversion 提供了一些实用程序,这些实用程序可用于创建、检查、修改和修复您的仓库。让我们更仔细地看一下每个工具。之后,我们将简要介绍 Berkeley DB 发行版中包含的一些实用程序,这些实用程序提供了特定于您的仓库数据库后端的功能,而这些功能不是 Subversion 自身工具提供的。

svnadmin

svnadmin 程序是仓库管理员的最佳帮手。除了提供创建 Subversion 仓库的能力外,此程序还允许您对这些仓库执行多种维护操作。 svnadmin 的语法类似于其他 Subversion 命令行程序的语法

$ svnadmin help
general usage: svnadmin SUBCOMMAND REPOS_PATH  [ARGS & OPTIONS ...]
Type 'svnadmin help <subcommand>' for help on a specific subcommand.
Type 'svnadmin --version' to see the program version and FS modules.

Available subcommands:
   crashtest
   create
   deltify
…

在本节的前面(在 名为“创建仓库”的部分中),我们介绍了 svnadmin create 子命令。我们将在这章的后面介绍大多数其他 svnadmin 子命令。您还可以查阅 名为“svnadmin”的部分,以了解子命令的完整说明以及每个子命令提供的功能。

svnlook

svnlook 是 Subversion 提供的一个工具,用于检查仓库中的各种修订版和 事务(正在进行的修订版)。该程序的任何部分都不会尝试更改仓库。 svnlook 通常由仓库钩子用于报告即将提交的更改(在 pre-commit 钩子的情况下)或刚刚提交的更改(在 post-commit 钩子的情况下)到仓库。仓库管理员可以使用此工具进行诊断。

svnlook 具有简单的语法

$ svnlook help
general usage: svnlook SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...]
Note: any subcommand which takes the '--revision' and '--transaction'
      options will, if invoked without one of those options, act on
      the repository's youngest revision.
Type 'svnlook help <subcommand>' for help on a specific subcommand.
Type 'svnlook --version' to see the program version and FS modules.
…

大多数 svnlook 子命令可以在修订版或事务树上操作,打印有关树本身的信息,或打印它与仓库上一个修订版的区别。您可以使用 --revision (-r) 和 --transaction (-t) 选项分别指定要检查的修订版或事务。在没有 --revision (-r) 和 --transaction (-t) 选项的情况下, svnlook 将检查仓库中最年轻的(或 HEAD)修订版。因此,以下两个命令在 /var/svn/repos 中的仓库中最年轻的修订版是 19 时执行相同的操作

$ svnlook info /var/svn/repos
$ svnlook info /var/svn/repos -r 19

这些关于子命令规则的一个例外是 svnlook youngest 子命令,它不接受任何选项,只是简单地打印出仓库中最年轻的修订版号

$ svnlook youngest /var/svn/repos
19
$

注意

请记住,您只能浏览未提交的事务。大多数仓库都没有此类事务,因为事务通常要么已提交(在这种情况下,您应该使用 --revision (-r) 选项将其作为修订版访问),要么已中止并删除。

来自 svnlook 的输出旨在同时为人机可读。以 svnlook info 子命令的输出为例

$ svnlook info /var/svn/repos
sally
2002-11-04 09:29:13 -0600 (Mon, 04 Nov 2002)
27
Added the usual
Greek tree.
$

svnlook info 的输出按以下顺序组成

  1. 作者,后面跟着一个换行符

  2. 日期,后面跟着一个换行符

  3. 日志消息中的字符数,后面跟着一个换行符

  4. 日志消息本身,后面跟着一个换行符

此输出是人可读的,这意味着诸如日期戳之类的项目使用文本表示形式显示,而不是更模糊的东西(例如自 Tastee Freez 的家伙经过以来的纳秒数)。但输出也是机器可读的——因为日志消息可以包含多行并且长度不受限制, svnlook 在消息本身之前提供了该消息的长度。这允许脚本和其他围绕此命令的包装器对日志消息做出明智的决定,例如为消息分配多少内存,或者至少在该输出不是流中的最后一点数据时跳过多少字节。

svnlook 可以执行各种其他查询:显示我们之前提到的部分信息,递归地列出版本化的目录树,报告在给定修订版或事务中修改了哪些路径,显示对文件和目录所做的文本和属性差异,等等。有关 svnlook 功能的完整参考,请参见 名为“svnlook”的部分

svndumpfilter

虽然它不是管理员常用的工具,但 svndumpfilter 提供了一种非常特殊的实用功能——通过充当基于路径的过滤器来快速轻松地修改 Subversion 仓库历史记录数据流。

svndumpfilter 的语法如下

$ svndumpfilter help
general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...]
Type "svndumpfilter help <subcommand>" for help on a specific subcommand.
Type 'svndumpfilter --version' to see the program version.
  
Available subcommands:
   exclude
   include
   help (?, h)

只有两个有趣的子命令: svndumpfilter excludesvndumpfilter include。它们允许您在流中隐式或显式包含路径之间进行选择。您可以在本章的后面,在 名为“过滤仓库历史记录”的部分中了解有关这些子命令和 svndumpfilter 的独特目的的更多信息。

svnsync

svnsync 程序是在 Subversion 1.4 版本中新添加的,它提供了维护 Subversion 仓库只读镜像所需的所有功能。该程序实际上只有一个任务——将一个仓库的版本化历史记录传输到另一个仓库。虽然有几种方法可以做到这一点,但它的主要优势在于它可以远程操作——“”和“接收[32] 仓库可能在彼此不同的计算机上,也可能与 svnsync 本身位于不同的计算机上。

正如您所料, svnsync 的语法与我们在本章中提到的其他每个程序非常相似

$ svnsync help
general usage: svnsync SUBCOMMAND DEST_URL  [ARGS & OPTIONS ...]
Type 'svnsync help <subcommand>' for help on a specific subcommand.
Type 'svnsync --version' to see the program version and RA modules.

Available subcommands:
   initialize (init)
   synchronize (sync)
   copy-revprops
   help (?, h)
$

我们将在本章的后面更多地讨论使用 svnsync 复制仓库(请参见 名为“仓库复制”的部分)。

fsfs-reshard.py

虽然不是 Subversion 工具链的正式成员,但 fsfs-reshard.py 脚本(位于 Subversion 源代码发行版中的 tools/server-side 目录中)是 FSFS 支持的 Subversion 仓库管理员的有用性能调整工具。FSFS 仓库包含描述单个修订版中所做更改的文件,以及包含与单个修订版关联的修订版属性的文件。在 Subversion 1.5 版本之前创建的仓库将这些文件保存在两个目录中——每个目录类型一个。随着新的修订版提交到仓库,Subversion 将更多文件放入这两个目录中——随着时间的推移,每个目录中这些文件的数量可能会变得非常大。这在某些基于网络的文件系统上会导致性能问题。

Subversion 1.5 使用稍微修改的布局创建 FSFS 支持的仓库,其中这两个目录的内容被 分片或散布在几个子目录中。这可以大大减少系统定位任何一个文件所需的时间,因此在读取仓库时提高了 Subversion 的整体性能。用于容纳这些文件的子目录的数量是可配置的,这就是 fsfs-reshard.py 的用武之地。该脚本将仓库的文件结构重新整理到一个新的安排中,该安排反映了请求的分片子目录数量。这对于将旧的 Subversion 仓库转换为新的 Subversion 1.5 分片布局(Subversion 不会自动为您执行此操作)或对已分片的仓库进行微调特别有用。

Berkeley DB 实用程序

如果您使用的是 Berkeley DB 仓库,则您的所有版本化文件系统结构和数据都保存在仓库的 db/ 子目录中的一组数据库表中。此子目录是一个常规的 Berkeley DB 环境目录,因此可以与任何 Berkeley 数据库工具一起使用,这些工具通常作为 Berkeley DB 发行版的一部分提供。

对于日常的 Subversion 使用,这些工具是多余的。Subversion 仓库通常需要的功能大多数已在 svnadmin 工具中复制。例如, svnadmin list-unused-dblogssvnadmin list-dblogs 执行了 Berkeley db_archive 实用程序提供的功能的一个子集,而 svnadmin recover 反映了 db_recover 实用程序的常见用例。

但是,仍然有一些 Berkeley DB 实用程序可能对您有所帮助。 db_dumpdb_load 程序分别写入和读取自定义文件格式,该格式描述了 Berkeley DB 数据库中的键和值。由于 Berkeley 数据库在机器体系结构之间不可移植,因此这种格式是在机器之间传输这些数据库的一种有用方法,无论体系结构或操作系统如何。正如我们将在本章的后面介绍的那样,您还可以使用 svnadmin dumpsvnadmin load 来完成类似的目的,但 db_dumpdb_load 可以同样好地完成某些工作,而且速度要快得多。如果经验丰富的 Berkeley DB 黑客需要出于某种原因对 BDB 支持的仓库中的数据进行就地调整,它们也可能很有用,而 Subversion 的实用程序不允许这样做。此外, db_stat 实用程序可以提供有关 Berkeley DB 环境状态的有用信息,包括有关锁定和存储子系统的详细统计信息。

有关 Berkeley DB 工具链的更多信息,请访问 Oracle 网站的 Berkeley DB 部分的文档部分,网址为 http://www.oracle.com/technology/documentation/berkeley-db/db/

提交日志消息更正

有时,用户会在她的日志消息中出现错误(可能是拼写错误或一些错误信息)。如果存储库配置(使用 pre-revprop-change 钩子;参见 名为“实现存储库钩子”的部分)以在提交完成之后接受对该日志消息的更改,则用户可以使用 svn propset 远程地“修复”她的日志消息(参见 svn propset)。但是,由于可能永远丢失信息,因此 Subversion 存储库默认情况下未配置为允许更改未版本化属性,除非由管理员更改。

如果管理员需要更改日志消息,则可以使用 svnadmin setlog 完成此操作。此命令更改存储库给定修订版本的日志消息(svn:log 属性),从提供的文件中读取新值。

$ echo "Here is the new, correct log message" > newlog.txt
$ svnadmin setlog myrepos newlog.txt -r 388

默认情况下,svnadmin setlog 命令仍然受制于与远程客户端相同的防止修改未版本化属性的保护措施,即仍然会触发 pre-post-revprop-change 钩子,因此必须将其设置为接受此类更改。但是,管理员可以通过将 --bypass-hooks 选项传递给 svnadmin setlog 命令来绕过这些保护措施。

警告

但是请记住,通过绕过钩子,您可能会避免诸如属性更改的电子邮件通知、跟踪未版本化属性更改的备份系统等。换句话说,请务必谨慎操作要更改的内容以及更改方式。

管理磁盘空间

尽管存储成本在过去几年中大幅下降,但对于寻求对大量数据进行版本控制的管理员而言,磁盘使用量仍然是一个有效的担忧。存储在实时存储库中的每一条版本历史信息都需要备份到其他地方,也许还需要根据轮换备份计划进行多次备份。了解哪些 Subversion 存储库数据需要保留在实时站点上,哪些需要备份以及哪些可以安全删除非常有用。

Subversion 如何节省磁盘空间

为了使存储库保持较小,Subversion 在存储库本身中使用 增量化(或增量存储)。增量化涉及将数据块的表示编码为相对于其他数据块的差异集合。如果这两部分数据非常相似,则增量化将导致增量数据块的存储节省,它不会占用与原始数据大小相同的空间,而只会占用足够的空间来说明:“我看起来与这里另一部分数据完全一样,除了以下几处更改。”。结果是,存储库中大部分容易占用大量空间的数据,即版本化文件的目录,存储的大小远小于这些数据原始的完整文本表示形式。对于使用 Subversion 1.4 或更高版本创建的存储库,节省的空间更大,因为这些文件的完整文本表示形式本身现在已进行了压缩。

注意

由于 BDB 支持的存储库中所有可能进行增量化的数据都存储在单个 Berkeley DB 数据库文件中,因此减小存储的值的大小不会立即减小数据库文件本身的大小。但是,Berkeley DB 会保留数据库文件中未使用的区域的内部记录,并在扩展数据库文件大小之前首先使用这些区域。因此,虽然增量化不会产生立即的存储空间节省,但它可以大大减缓数据库的未来增长。

删除无效事务

虽然这种情况很少见,但在某些情况下,Subversion 提交过程可能会失败,从而在存储库中留下未完成的修订版的残留部分,即未提交的事务以及与之关联的所有文件和目录更改。这可能有多种原因:可能是用户粗鲁地终止了客户端操作,或者在操作过程中出现了网络故障。无论原因如何,无效事务都可能发生。它们不会造成任何实际损害,除了占用磁盘空间。尽管如此,一丝不苟的管理员可能希望将其删除。

您可以使用 svnadmin lstxns 命令列出当前未完成事务的名称。

$ svnadmin lstxns myrepos
19
3a1
a45
$

然后,可以使用 svnlook(以及其 --transaction (-t) 选项)对结果输出中的每个项目进行处理,以确定谁创建了事务、创建的时间以及事务中进行了哪些类型的更改(这些信息有助于确定事务是否可以安全删除!)。如果您确实要删除事务,则可以将其名称传递给 svnadmin rmtxns,该命令将执行事务的清理。实际上,svnadmin rmtxns 可以直接从 svnadmin lstxns 的输出中获取输入!

$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos`
$

如果您像这样使用这两个子命令,则应考虑暂时使存储库对客户端不可访问。这样一来,在您开始清理之前,没有人可以开始合法的事务。 示例 5.1,“txn-info.sh(报告未完成事务)” 包含一些 shell 脚本,可以快速生成有关存储库中每个未完成事务的信息。

示例 5.1. txn-info.sh(报告未完成事务)

#!/bin/sh

### Generate informational output for all outstanding transactions in
### a Subversion repository.

REPOS="${1}"
if [ "x$REPOS" = x ] ; then
  echo "usage: $0 REPOS_PATH"
  exit
fi

for TXN in `svnadmin lstxns ${REPOS}`; do 
  echo "---[ Transaction ${TXN} ]-------------------------------------------"
  svnlook info "${REPOS}" -t "${TXN}"
done

脚本的输出基本上是几个 svnlook info 输出块的串联(参见 名为“svnlook”的部分),看起来像这样

$ txn-info.sh myrepos
---[ Transaction 19 ]-------------------------------------------
sally
2001-09-04 11:57:19 -0500 (Tue, 04 Sep 2001)
0
---[ Transaction 3a1 ]-------------------------------------------
harry
2001-09-10 16:50:30 -0500 (Mon, 10 Sep 2001)
39
Trying to commit over a faulty network.
---[ Transaction a45 ]-------------------------------------------
sally
2001-09-12 11:09:28 -0500 (Wed, 12 Sep 2001)
0
$

长期搁置的事务通常表示某种失败或中断的提交。事务的时间戳可以提供有趣的信息,例如,九个月前开始的操作仍然活跃的可能性有多大?

简而言之,事务清理决策无需不明智地做出。可以利用各种信息源(包括 Apache 的错误和访问日志、Subversion 的操作日志、Subversion 修订历史记录等)来进行决策过程。当然,管理员通常可以简单地与似乎处于死机状态的事务所有者进行沟通(例如通过电子邮件),以验证事务实际上是否处于僵尸状态。

清除未使用的 Berkeley DB 日志文件

直到最近,对于 BDB 支持的 Subversion 存储库而言,磁盘空间使用量最大的罪魁祸首是日志文件,Berkeley DB 在修改实际数据库文件之前会将这些文件用于预写操作。这些文件捕获了从一种状态更改到另一种状态的数据库更改过程中的所有操作,而数据库文件在任何给定时间都反映了特定状态,而日志文件包含所有状态之间的众多更改 之间 的状态。因此,它们会迅速增长并累积。

幸运的是,从 Berkeley DB 4.2 版本开始,数据库环境能够自动删除其自身未使用的日志文件。使用针对 Berkeley DB 4.2 或更高版本编译的 svnadmin 创建的所有存储库都将为此自动日志文件删除进行配置。如果您不想启用此功能,只需将 --bdb-log-keep 选项传递给 svnadmin create 命令。如果您忘记执行此操作或稍后改变主意,只需编辑存储库的 db 目录中找到的 DB_CONFIG 文件,注释掉包含 set_flags DB_LOG_AUTOREMOVE 指令的行,然后对存储库运行 svnadmin recover 以强制配置更改生效。有关数据库配置的更多信息,请参见 名为“Berkeley DB 配置”的部分

如果没有某种形式的自动日志文件删除,日志文件会在您使用存储库时累积。这实际上是数据库系统的一个功能,您应该能够仅使用日志文件来重建整个数据库,因此这些文件对于灾难性数据库恢复很有用。但是,通常情况下,您需要存档不再被 Berkeley DB 使用的日志文件,然后将其从磁盘中删除以节省空间。使用 svnadmin list-unused-dblogs 命令列出未使用的日志文件。

$ svnadmin list-unused-dblogs /var/svn/repos
/var/svn/repos/log.0000000031
/var/svn/repos/log.0000000032
/var/svn/repos/log.0000000033
…
$ rm `svnadmin list-unused-dblogs /var/svn/repos`
## disk space reclaimed!

警告

将日志文件用作备份或灾难恢复计划一部分的 BDB 支持的存储库 不应 使用日志文件自动删除功能。仅当 所有 日志文件都可用时,才能从日志文件中重建存储库的数据。如果备份系统有机会将某些日志文件复制到其他位置之前,它们就被从磁盘中删除,那么备份的日志文件集不完整,基本上毫无用处。

Berkeley DB 恢复

名为“Berkeley DB”的部分 中所述,如果 Berkeley DB 存储库没有正确关闭,有时会处于冻结状态。发生这种情况时,管理员需要将数据库回溯到一致状态。这对于 BDB 支持的存储库来说是独一无二的,如果您使用的是 FSFS 支持的存储库,则这并不适用于您。对于那些使用 Subversion 1.4 及更高版本的 Berkeley DB 4.4 的用户来说,您应该会发现 Subversion 在此类情况下变得更加健壮。尽管如此,卡住的 Berkeley DB 存储库确实会发生,管理员需要知道如何安全地处理这种情况。

为了保护存储库中的数据,Berkeley DB 使用锁定机制。这种机制可以确保数据库的各个部分不会被多个数据库访问器同时修改,并且每个进程在从数据库中读取数据时都会看到数据处于正确状态。当进程需要更改数据库中的内容时,它首先检查目标数据上是否存在锁定。如果数据未被锁定,则进程会锁定数据,进行所需的更改,然后解锁数据。其他进程必须等到锁定被移除后才能继续访问数据库的该部分。(这与您作为用户可以对存储库中的版本化文件施加的锁定无关;我们试图在边栏 “锁”的三种含义 中消除由这种术语冲突造成的混淆。)

在使用 Subversion 存储库的过程中,致命错误或中断可能会阻止进程有机会删除它在数据库中设置的锁定。结果是后端数据库系统“卡住。”。发生这种情况时,任何尝试访问存储库的操作都会无限期地挂起(因为每个新的访问器都在等待锁定消失,而这种情况不会发生)。

如果您的代码库出现这种情况,请不要惊慌。Berkeley DB 文件系统利用数据库事务、检查点和预写日志,以确保只有最灾难性的事件 [33] 才能永久破坏数据库环境。一个足够谨慎的代码库管理员会以某种方式对代码库数据进行异地备份,但现在不要急着去磁带备份存储柜。

相反,请使用以下方法尝试“解除卡死”您的代码库

  1. 确保没有进程访问(或尝试访问)代码库。对于联网代码库,这也意味着关闭 Apache HTTP Server 或 svnserve 守护进程。

  2. 以拥有和管理代码库的用户身份登录。这一点很重要,因为以错误的用户身份恢复代码库可能会调整代码库文件权限,从而导致即使在代码库“解除卡死”后,代码库仍然无法访问。

  3. 运行命令 svnadmin recover /var/svn/repos。您应该看到类似以下的输出

    Repository lock acquired.
    Please wait; recovering the repository may take some time...
    
    Recovery completed.
    The latest repos revision is 19.
    

    此命令可能需要几分钟才能完成。

  4. 重新启动服务器进程。

此过程可以修复几乎所有代码库卡死的情况。请确保您以拥有和管理数据库的用户身份运行此命令,而不仅仅是以 root 身份运行。恢复过程的一部分可能涉及从头开始重新创建各种数据库文件(共享内存区域,例如)。以 root 身份恢复将创建这些文件,使其由 root 拥有,这意味着即使您恢复了对代码库的连接,普通用户也无法访问它。

如果由于某些原因,之前的过程无法成功解除代码库卡死,您应该执行以下两项操作。首先,将损坏的代码库目录移到一边(例如,将其重命名为 repos.BROKEN),然后恢复其最新的备份。然后,向 Subversion 用户邮件列表(地址为 )发送一封电子邮件,详细描述您的问题。数据完整性对 Subversion 开发人员来说是一个非常高的优先级。

将代码库数据迁移到其他位置

Subversion 文件系统将其数据分散在代码库中的各个文件中,一般只有 Subversion 开发人员自己才能理解和感兴趣。但是,可能会出现需要将所有或部分数据复制或移动到另一个代码库的情况。

Subversion 通过 代码库转储流 提供这种功能。代码库转储流(通常在存储为磁盘上的文件时称为“转储文件”)是一种可移植的扁平文件格式,它描述了代码库中的各个修订版——发生了什么变化、由谁更改、何时更改以及其他信息。此转储流是用于在代码库之间(全部或部分,有或无修改)编组版本化历史记录的主要机制。Subversion 提供了创建和加载这些转储流所需的工具:分别为 svnadmin dumpsvnadmin load 子命令。

警告

虽然 Subversion 代码库转储格式包含人类可读部分和熟悉的结构(它类似于 RFC 822 格式,与大多数电子邮件使用的格式相同),但它 不是 纯文本文件格式。它是一种二进制文件格式,对干预高度敏感。例如,许多文本编辑器会通过自动转换行尾来破坏文件。

转储和加载 Subversion 代码库数据有很多原因。在 Subversion 诞生的早期,最常见的原因是 Subversion 本身的演变。随着 Subversion 的成熟,有时对后端数据库模式所做的更改会导致与早期版本的代码库不兼容,因此用户必须使用早期版本的 Subversion 转储他们的代码库数据,并将其加载到使用新版本 Subversion 新创建的代码库中。现在,此类模式更改自 Subversion 1.0 版本发布以来就没有发生过,Subversion 开发人员承诺不会在升级 Subversion 的次要版本(例如从 1.3 到 1.4)时强迫用户转储和加载他们的代码库。但仍然有其他理由需要转储和加载,包括在新的操作系统或 CPU 架构上重新部署 Berkeley DB 代码库、在 Berkeley DB 和 FSFS 后端之间切换或(正如我们将在本章后面的 名为“过滤代码库历史记录”的部分 中介绍的那样)从代码库历史记录中清除版本化数据。

注意

Subversion 代码库转储格式仅描述版本化代码库更改。它不会包含任何关于未提交的事务、用户对文件系统路径的锁定、代码库或服务器配置自定义(包括钩子脚本)的信息等。

无论您迁移代码库历史记录的原因是什么,使用 svnadmin dumpsvnadmin load 子命令都非常简单。 svnadmin dump 将输出一系列使用 Subversion 自定义文件系统转储格式格式化的代码库修订版。转储格式被打印到标准输出流,而信息性消息被打印到标准错误流。这允许您将输出流重定向到文件,同时在终端窗口中查看状态输出。例如

$ svnlook youngest myrepos
26
$ svnadmin dump myrepos > dumpfile
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
…
* Dumped revision 25.
* Dumped revision 26.

在进程结束时,您将获得一个包含代码库中存储的所有数据(在请求的修订版范围内)的单个文件(在前面的示例中为 dumpfile)。请注意, svnadmin dump 正如任何其他“读取器”进程(例如, svn checkout)那样从代码库中读取修订版树,因此可以在任何时间运行此命令。

这对子命令中的另一个子命令 svnadmin load 将标准输入流解析为 Subversion 代码库转储文件,并有效地将这些转储的修订版重放回该操作的目标代码库中。它还会提供信息性反馈,这次使用标准输出流

$ svnadmin load newrepos < dumpfile
<<< Started new txn, based on original revision 1
     * adding path : A ... done.
     * adding path : A/B ... done.
     …
------- Committed new rev 1 (loaded from original rev 1) >>>

<<< Started new txn, based on original revision 2
     * editing path : A/mu ... done.
     * editing path : A/D/G/rho ... done.

------- Committed new rev 2 (loaded from original rev 2) >>>

…

<<< Started new txn, based on original revision 25
     * editing path : A/D/gamma ... done.

------- Committed new rev 25 (loaded from original rev 25) >>>

<<< Started new txn, based on original revision 26
     * adding path : A/Z/zeta ... done.
     * editing path : A/mu ... done.

------- Committed new rev 26 (loaded from original rev 26) >>>

加载的结果是向代码库添加了新的修订版——与通过常规 Subversion 客户端对该代码库进行提交所获得的结果相同。就像在提交中一样,您可以使用钩子程序在加载过程中每次提交之前和之后执行操作。通过将 --use-pre-commit-hook--use-post-commit-hook 选项传递给 svnadmin load,您可以指示 Subversion 分别为每个加载的修订版执行 pre-commit 和 post-commit 钩子程序。例如,您可以使用这些程序来确保加载的修订版通过与常规提交相同的验证步骤。当然,您应该谨慎使用这些选项——如果您的 post-commit 钩子程序将电子邮件发送到邮件列表以进行每次新的提交,您可能不希望在邮件列表中快速连续地喷出数百或数千封提交电子邮件!您可以阅读更多关于使用钩子脚本的信息,请参见 名为“实现代码库钩子”的部分

请注意,由于 svnadmin 使用标准输入和输出流来进行代码库转储和加载过程,因此那些感觉特别大胆的人可以尝试以下操作(甚至可以在管道两侧使用不同版本的 svnadmin

$ svnadmin create newrepos
$ svnadmin dump oldrepos | svnadmin load newrepos

默认情况下,转储文件会非常大——远大于代码库本身。这是因为默认情况下,每个文件的每个版本都以转储文件中的完整文本形式表示。这是最快捷、最简单的行为,如果您将转储数据直接管道到其他进程(例如压缩程序、过滤程序或加载程序),这将很有用。但是,如果您要创建用于长期存储的转储文件,您可能希望通过使用 --deltas 选项来节省磁盘空间。使用此选项,文件的后续修订版将以压缩的二进制差异的形式输出——就像文件修订版存储在代码库中一样。此选项速度较慢,但会导致转储文件的大小更接近于原始代码库。

我们之前提到过 svnadmin dump 输出一系列修订版。使用 --revision (-r) 选项指定要转储的单个修订版或一系列修订版。如果您省略此选项,将转储所有现有的代码库修订版。

$ svnadmin dump myrepos -r 23 > rev-23.dumpfile
$ svnadmin dump myrepos -r 100:200 > revs-100-200.dumpfile

当 Subversion 转储每个新的修订版时,它只输出足够的信息,以便将来加载程序能够根据上一个修订版重新创建该修订版。换句话说,对于转储文件中的任何给定修订版,只有该修订版中更改的项目才会出现在转储中。此规则的唯一例外是使用当前 svnadmin dump 命令转储的第一个修订版。

默认情况下,Subversion 不会将第一个转储的修订版表示为仅仅是应用于上一个修订版的差异。一方面,转储文件中没有上一个修订版!另一方面,Subversion 无法知道将转储数据加载到的代码库的状态(如果它确实被加载)。为了确保每次执行 svnadmin dump 的输出都是自包含的,默认情况下,第一个转储的修订版是该代码库修订版中每个目录、文件和属性的完整表示。

但是,您可以更改此默认行为。如果您在转储代码库时添加 --incremental 选项, svnadmin 将将第一个转储的修订版与代码库中的上一个修订版进行比较——就像它处理要转储的每个其他修订版一样。然后,它将以与转储范围内其他修订版完全相同的方式输出第一个修订版——只提及该修订版中发生的更改。这样做的好处是,您可以创建多个小的转储文件,这些文件可以连续加载,而不是一个大的文件,就像这样

$ svnadmin dump myrepos -r 0:1000 > dumpfile1
$ svnadmin dump myrepos -r 1001:2000 --incremental > dumpfile2
$ svnadmin dump myrepos -r 2001:3000 --incremental > dumpfile3

这些转储文件可以使用以下命令序列加载到新的代码库中

$ svnadmin load newrepos < dumpfile1
$ svnadmin load newrepos < dumpfile2
$ svnadmin load newrepos < dumpfile3

您可以使用 --incremental 选项执行的另一个巧妙技巧涉及将新的一系列转储的修订版追加到现有的转储文件中。例如,您可能有一个 post-commit 钩子,它只将触发该钩子的单个修订版的代码库转储追加到其中。或者您可能有一个脚本,它在夜间运行以将自上次脚本运行以来添加到代码库的所有修订版的转储文件数据追加到其中。这样使用, svnadmin dump 可以成为一种备份代码库更改随时间推移的方式,以防系统崩溃或其他灾难性事件发生。

转储格式也可以用于将多个不同代码库的内容合并到一个代码库中。通过使用 svnadmin load--parent-dir 选项,您可以为加载过程指定新的虚拟根目录。这意味着如果您有三个代码库的转储文件——例如 calc-dumpfilecal-dumpfiless-dumpfile——您可以先创建一个新的代码库来容纳它们

$ svnadmin create /var/svn/projects
$

然后,在代码库中创建新的目录,这些目录将封装以前三个代码库中的内容

$ svn mkdir -m "Initial project roots" \
      file:///var/svn/projects/calc \
      file:///var/svn/projects/calendar \
      file:///var/svn/projects/spreadsheet
Committed revision 1.
$ 

最后,将各个转储文件加载到新代码库中各自的位置

$ svnadmin load /var/svn/projects --parent-dir calc < calc-dumpfile
…
$ svnadmin load /var/svn/projects --parent-dir calendar < cal-dumpfile
…
$ svnadmin load /var/svn/projects --parent-dir spreadsheet < ss-dumpfile
…
$

我们将介绍使用 Subversion 仓库转储格式的最后一种方式——从不同的存储机制或版本控制系统进行转换。由于转储文件格式在很大程度上是人类可读的,因此使用此文件格式描述一组通用的更改(每个更改都应视为一个新修订版)应该相对容易。事实上,cvs2svn 实用程序(参见 名为“将仓库从 CVS 转换为 Subversion”的部分)使用转储格式来表示 CVS 仓库的内容,以便可以将这些内容复制到 Subversion 仓库中。

过滤仓库历史记录

由于 Subversion 使用至少二进制差异算法和数据压缩(可选地在完全不透明的数据库系统中)来存储您的版本化历史记录,因此尝试手动调整是不明智的,即使不是非常困难,也强烈建议不要这样做。并且一旦数据存储在您的仓库中,Subversion 通常不会提供一种简单的方法来删除该数据。[34] 但不可避免地,您有时会想要操作仓库的历史记录。您可能需要删除所有意外添加到仓库中的文件的实例(无论出于何种原因,这些文件都不应该存在)。[35] 或者,您可能有多个项目共享一个仓库,并且您决定将它们拆分到各自的仓库中。为了完成此类任务,管理员需要一种更易于管理和灵活的仓库中数据的表示形式——Subversion 仓库转储格式。

正如我们在前面章节 名为“将仓库数据迁移到其他地方”的部分中所述,Subversion 仓库转储格式是您对版本化数据随时间推移所做的更改的人类可读表示形式。使用 svnadmin dump 命令生成转储数据,并使用 svnadmin load 将其填充到新仓库中。转储格式的人类可读性方面最棒的一点是,如果您不粗心大意,您可以手动检查和修改它。当然,缺点是,如果您有三年时间的仓库活动封装在一个很可能非常大的转储文件中,那么您可能需要花费很长时间来手动检查和修改它。

这就是 svndumpfilter 发挥作用的地方。此程序充当仓库转储流的基于路径的过滤器。只需提供您想要保留的路径列表或您不想保留的路径列表,然后将仓库转储数据通过此过滤器。结果将是一个修改后的转储数据流,其中只包含您(明确或隐式)请求的版本化路径。

让我们看一个关于如何使用此程序的实际示例。在本节的前面部分(参见 名为“规划仓库组织”的部分),我们讨论了决定如何为仓库中的数据选择布局的过程——每个项目使用一个仓库或将它们组合起来、在仓库中安排内容,等等。但是,有时在新的修订版开始传入后,您会重新考虑您的布局,并希望进行一些更改。一个常见的更改是决定将多个共享单个仓库的项目移动到每个项目的单独仓库中。

我们假设的仓库包含三个项目:calccalendarspreadsheet。它们在这样的布局中并肩存在

/
   calc/
      trunk/
      branches/
      tags/
   calendar/
      trunk/
      branches/
      tags/
   spreadsheet/
      trunk/
      branches/
      tags/

要将这三个项目放入自己的仓库中,我们首先转储整个仓库

$ svnadmin dump /var/svn/repos > repos-dumpfile
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
* Dumped revision 3.
…
$

接下来,将该转储文件通过过滤器运行,每次只包含一个顶级目录。这将产生三个新的转储文件

$ svndumpfilter include calc < repos-dumpfile > calc-dumpfile
…
$ svndumpfilter include calendar < repos-dumpfile > cal-dumpfile
…
$ svndumpfilter include spreadsheet < repos-dumpfile > ss-dumpfile
…
$

此时,您需要做出决定。您的每个转储文件都将创建一个有效的仓库,但会保留与原始仓库中完全相同的路径。这意味着,即使您有一个仅用于 calc 项目的仓库,该仓库仍然会有一个名为 calc 的顶级目录。如果您希望 trunktagsbranches 目录位于仓库的根目录中,您可能希望编辑转储文件,调整 Node-pathNode-copyfrom-path 标题,以便它们不再具有第一个 calc/ 路径组件。此外,您还需要删除创建 calc 目录的转储数据部分。它看起来类似于以下内容

Node-path: calc
Node-action: add
Node-kind: dir
Content-length: 0
  

警告

如果您确实计划手动编辑转储文件以删除顶级目录,请确保您的编辑器未设置为自动将行尾字符转换为本机格式(例如,\r\n\n),因为内容将与元数据不符。这将使转储文件无用。

现在剩下的就是创建三个新仓库,并将每个转储文件加载到正确的仓库中,忽略转储流中的 UUID

$ svnadmin create calc
$ svnadmin load --ignore-uuid calc < calc-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : button.c ... done.
…
$ svnadmin create calendar
$ svnadmin load --ignore-uuid calendar < cal-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : cal.c ... done.
…
$ svnadmin create spreadsheet
$ svnadmin load --ignore-uuid spreadsheet < ss-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : ss.c ... done.
…
$

两个 svndumpfilter 的子命令都接受用于决定如何处理“”修订版的选项。如果给定的修订版只包含对已过滤掉的路径的更改,那么现在空的修订版可能被认为是不重要的,甚至是不必要的。因此,为了让用户控制如何处理这些修订版,svndumpfilter 提供了以下命令行选项

--drop-empty-revs

根本不生成空修订版——直接忽略它们。

--renumber-revs

如果丢弃空修订版(使用 --drop-empty-revs 选项),则更改剩余修订版的修订版号,以便在数字序列中没有间隙。

--preserve-revprops

如果未丢弃空修订版,则保留这些空修订版的修订版属性(日志消息、作者、日期、自定义属性等)。否则,空修订版将只包含原始日期戳,以及一个生成的日志消息,指示此修订版已由 svndumpfilter 清空。

虽然 svndumpfilter 非常有用且可以节省大量时间,但不幸的是,存在一些需要注意的地方。首先,此实用程序对路径语义过于敏感。请注意转储文件中的路径是使用前导斜杠还是没有前导斜杠。您需要查看 Node-pathNode-copyfrom-path 标题。

…
Node-path: spreadsheet/Makefile
…

如果路径具有前导斜杠,则应在传递给 svndumpfilter includesvndumpfilter exclude 的路径中包含前导斜杠(如果路径没有前导斜杠,则不应包含)。此外,如果您的转储文件由于某种原因对前导斜杠的使用不一致,[36] 您可能需要规范化这些路径,以便它们都具有或都缺少前导斜杠。

此外,复制的路径可能会给您带来一些麻烦。Subversion 支持仓库中的复制操作,其中通过复制某个已存在的路径来创建一个新路径。有可能在仓库的生命周期中,您可能已从 svndumpfilter 排除的某个位置复制了一个文件或目录,到它包含的位置。为了使转储数据自给自足,svndumpfilter 仍然需要显示新路径的添加(包括通过复制创建的任何文件的內容),而不将该添加表示为来自转储数据流中不存在的源的复制。但是,由于 Subversion 仓库转储格式只显示每个修订版中更改的内容,因此复制源的内容可能无法轻易获得。如果您怀疑仓库中存在任何此类复制操作,您可能需要重新考虑您的包含/排除路径集,也许也包括用作有问题的复制操作源的路径。

最后,svndumpfilter 对路径过滤非常严格。如果您尝试复制以 trunk/my-project 为根目录的项目的历史记录并将其移动到自己的仓库中,您当然会使用 svndumpfilter include 命令来保留 trunk/my-project 中和下方的所有更改。但是,生成的转储文件不会对要将这些数据加载到的仓库做出任何假设。具体来说,转储数据可能以添加 trunk/my-project 目录的修订版开始,但它 不会 包含创建 trunk 目录本身的指令(因为 trunk 不匹配包含过滤器)。您需要确保在新转储流期望存在的任何目录在尝试将流加载到该仓库之前实际存在于目标仓库中。

仓库复制

在以下几种情况下,拥有一个版本历史记录与其他仓库完全相同的 Subversion 仓库非常方便。也许最明显的情况是维护一个简单的备份仓库,当主仓库由于硬件故障、网络中断或其他此类问题而变得不可访问时使用。其他场景包括部署镜像仓库以将沉重的 Subversion 负载分散到多个服务器上,用作软升级机制,等等。

从 1.4 版开始,Subversion 提供了一个用于管理此类场景的程序——svnsync。这是通过本质上要求 Subversion 服务器“重播”修订版(一次一个)来实现的。然后,它使用该修订版信息来模拟对另一个仓库的相同提交。两个仓库都不需要本地可访问运行 svnsync 的机器——它的参数是仓库 URL,它通过 Subversion 的仓库访问 (RA) 接口完成所有工作。它只要求对源仓库有读访问权限,对目标仓库有读/写访问权限。

注意

当对远程源仓库使用 svnsync 时,该仓库的 Subversion 服务器必须运行 Subversion 1.4 版或更高版本。

假设您已经拥有想要镜像的源代码库,那么接下来您需要的是一个空的作为镜像的目标代码库。这个目标代码库可以使用任何可用的文件系统数据存储后端(参见 名为“选择数据存储”的部分),但它不能包含任何版本历史记录。 svnsync 用于通信修订信息使用的协议对源代码库和目标代码库中包含的版本历史记录之间的不匹配非常敏感。出于这个原因,虽然 svnsync 无法 要求 目标代码库为只读,[37] 允许目标代码库中的版本历史记录通过任何机制(镜像过程以外)进行更改,会导致灾难性的后果。

警告

不要以导致其版本历史记录偏离其镜像的代码库的方式修改镜像代码库。该镜像代码库上发生的唯一提交和修订属性修改应该是 svnsync 工具执行的修改。

目标代码库的另一个要求是允许 svnsync 进程修改修订属性。因为 svnsync 在该代码库的钩子系统框架内工作,所以代码库的默认状态(不允许修订属性更改;参见 pre-revprop-change)是不够的。您需要显式实现 pre-revprop-change 钩子,并且您的脚本必须允许 svnsync 设置和更改修订属性。有了这些规定,您就可以开始镜像代码库修订版了。

提示

实施授权措施是一个好主意,这些措施允许您的代码库复制过程执行其任务,同时阻止其他用户修改镜像代码库的内容。

让我们在一个典型的镜像场景中逐步介绍 svnsync 的使用。我们将用一些实际建议来补充这个讨论,如果您不需要或不适合您的环境,您可以随意忽略这些建议。

作为对我们最喜欢的版本控制系统优秀开发人员的帮助,我们将镜像公共 Subversion 源代码库,并在互联网上公开展示该镜像,该镜像托管在与原始 Subversion 源代码库所在的机器不同的机器上。这个远程主机有一个全局配置,允许匿名用户读取主机上代码库的内容,但要求用户进行身份验证才能修改这些代码库。(请原谅我们暂时忽略了 Subversion 服务器配置的细节——这些内容将在 第 6 章,服务器配置 中详细介绍。)并且,为了让示例更有趣,我们将从第三台机器(我们目前使用的机器)来驱动复制过程。

首先,我们将创建将成为我们镜像的代码库。这以及接下来的几个步骤确实需要对镜像代码库所在的机器进行 shell 访问。但是,一旦代码库配置完成,我们就无需再直接访问它。

$ ssh [email protected] \
      "svnadmin create /var/svn/svn-mirror"
[email protected]'s password: ********
$

此时,我们有了代码库,并且由于服务器的配置,该代码库现在在互联网上“在线”。现在,因为我们不希望除复制过程以外的任何东西修改代码库,我们需要一种方法来区分该过程和其他潜在的提交者。为此,我们为我们的过程使用一个专用用户名。只有由特殊用户名 syncuser 执行的提交和修订属性修改将被允许。

我们将使用代码库的钩子系统来允许复制过程执行其所需的操作,并强制只有该过程才能执行这些操作。我们通过实现两个代码库事件钩子来实现这一点——pre-revprop-change 和 start-commit。我们的 pre-revprop-change 钩子脚本位于 示例 5.2,“镜像代码库的 pre-revprop-change 钩子脚本” 中,基本上验证了尝试更改属性的用户是否为我们的 syncuser 用户。如果是,则允许更改;否则,拒绝更改。

示例 5.2。镜像代码库的 pre-revprop-change 钩子脚本

#!/bin/sh 

USER="$3"

if [ "$USER" = "syncuser" ]; then exit 0; fi

echo "Only the syncuser user may change revision properties" >&2
exit 1

这涵盖了修订属性更改。现在我们需要确保只有 syncuser 用户被允许提交新修订版到代码库。我们通过使用 start-commit 钩子脚本来实现这一点,例如 示例 5.3,“镜像代码库的 start-commit 钩子脚本” 中的脚本。

示例 5.3。镜像代码库的 start-commit 钩子脚本

#!/bin/sh 

USER="$2"

if [ "$USER" = "syncuser" ]; then exit 0; fi

echo "Only the syncuser user may commit new revisions" >&2
exit 1

安装我们的钩子脚本并确保它们可以被 Subversion 服务器执行后,我们就完成了镜像代码库的设置。现在,我们开始实际进行镜像。

我们使用 svnsync 需要做的第一件事是在目标代码库中注册它将成为源代码库的镜像这一事实。我们使用 svnsync initialize 子命令来实现。我们提供的 URL 分别指向目标代码库和源代码库的根目录。在 Subversion 1.4 中,这是必需的——只允许代码库的完整镜像。但在 Subversion 1.5 中,您也可以使用 svnsync 来镜像代码库的某个子树。

$ svnsync help init
initialize (init): usage: svnsync initialize DEST_URL SOURCE_URL

Initialize a destination repository for synchronization from
another repository.
…
$ svnsync initialize http://svn.example.com/svn-mirror \
                     http://svn.collab.net/repos/svn \
                     --sync-username syncuser --sync-password syncpass
Copied properties for revision 0.
$

我们的目标代码库现在将记住它是一个公共 Subversion 源代码库的镜像。请注意,我们在 svnsync 中提供了一个用户名和密码作为参数——这是镜像代码库上的 pre-revprop-change 钩子所要求的。

注意

在 Subversion 1.4 中,传递给 svnsync--username--password 命令行选项的值用于针对源代码库和目标代码库进行身份验证。当用户在两个代码库的凭据不完全相同的情况下,尤其是在非交互模式(使用 --non-interactive 选项)下运行时,会导致问题。

这个问题在 Subversion 1.5 中通过引入两对新的选项得到了解决。使用 --source-username--source-password 为源代码库提供身份验证凭据;使用 --sync-username--sync-password 为目标代码库提供凭据。(旧的 --username--password 选项仍然存在是为了兼容性,但我们建议不要使用它们。)

现在进入有趣的部分。使用单个子命令,我们可以告诉 svnsync 将所有尚未镜像的修订版从源代码库复制到目标代码库。[38] svnsync synchronize 子命令将查看之前存储在目标代码库上的特殊修订属性,并确定它要镜像的代码库以及最近镜像的修订版是修订版 0。然后它将查询源代码库并确定该代码库中的最新修订版是什么。最后,它要求源代码库的服务器开始重播 0 到该最新修订版之间的所有修订版。当 svnsync 从源代码库的服务器获得结果响应时,它开始将这些修订版作为新的提交转发到目标代码库的服务器。

$ svnsync help synchronize
synchronize (sync): usage: svnsync synchronize DEST_URL

Transfer all pending revisions to the destination from the source
with which it was initialized.
…
$ svnsync synchronize http://svn.example.com/svn-mirror
Transmitting file data ........................................
Committed revision 1.
Copied properties for revision 1.
Transmitting file data ..
Committed revision 2.
Copied properties for revision 2.
Transmitting file data .....
Committed revision 3.
Copied properties for revision 3.
…
Transmitting file data ..
Committed revision 23406.
Copied properties for revision 23406.
Transmitting file data .
Committed revision 23407.
Copied properties for revision 23407.
Transmitting file data ....
Committed revision 23408.
Copied properties for revision 23408.
$

这里特别值得注意的是,对于每个镜像的修订版,首先是对该修订版进行提交到目标代码库,然后是属性更改。这是因为初始提交是由(并归因于)用户 syncuser 执行的,并且它使用该修订版的创建时间的日期戳进行标记。此外,Subversion 的底层代码库访问接口不提供在提交过程中设置任意修订属性的机制。因此,svnsync 会立即进行一系列属性修改,将源代码库中找到的该修订版的全部修订属性复制到目标代码库。这也具有将修订版的作者和日期戳修正为与源代码库匹配的效果。

同样值得注意的是,svnsync 执行了细致的簿记工作,允许它在不破坏镜像数据完整性的情况下安全地中断和重新启动。如果在镜像代码库时出现网络故障,只需重复 svnsync synchronize 命令,它将愉快地从中断的地方继续执行。事实上,当源代码库中出现新修订版时,这正是您为保持镜像更新而需要执行的操作。

然而,这个过程中存在一个不优雅的地方。由于 Subversion 修订版属性可以在代码库的生命周期中随时更改,并且因为它们不会留下表明何时更改的审计跟踪,所以复制过程必须特别注意它们。如果你已经镜像了代码库的前 15 个修订版,然后有人更改了修订版 12 上的修订版属性,svnsync 将不知道要返回并修补它对修订版 12 的副本。你需要使用(或使用一些围绕它的额外工具)svnsync copy-revprops 子命令来手动告诉它这样做,该命令只是重新复制特定修订版或修订版范围内的所有修订版属性。

$ svnsync help copy-revprops
copy-revprops: usage: svnsync copy-revprops DEST_URL [REV[:REV2]]

Copy the revision properties in a given range of revisions to the
destination from the source with which it was initialized.
…
$ svnsync copy-revprops http://svn.example.com/svn-mirror 12
Copied properties for revision 12.
$

这就是代码库复制的概括。你可能需要围绕此过程进行一些自动化。例如,虽然我们的示例是拉取和推送设置,但你可能希望你的主代码库将更改推送到一个或多个已批准的镜像,作为其 post-commit 和 post-revprop-change 挂钩实现的一部分。这将使镜像尽可能接近实时地保持最新。

此外,虽然这样做并不常见,但 svnsync 确实可以优雅地镜像代码库,其中它进行身份验证的用户只具有部分读取权限。它只会复制它被允许查看的代码库部分。显然,这样的镜像不适合用作备份解决方案。

在 Subversion 1.5 中,svnsync 增加了镜像代码库子集而不是整个代码库的功能。设置和维护此类镜像的过程与镜像整个代码库的过程完全相同,只是在运行 svnsync init 时,你指定的是源代码库中某个子目录的 URL,而不是源代码库的根 URL。与该镜像的同步现在将只复制该源代码库子目录下发生更改的部分。不过,此支持有一些限制。首先,你不能将源代码库的多个不相交子目录镜像到单个镜像代码库中——你应该改而镜像这两个子目录的共同父目录。其次,过滤逻辑完全基于路径,因此,如果你正在镜像的子目录过去曾被重命名,则你的镜像将只包含该目录出现在你指定的 URL 之后的修订版。同样地,如果源子目录在将来被重命名,则你的同步进程将在你指定的源 URL 无效时停止镜像数据。

就用户与代码库和镜像的交互而言,确实 可以使用与两者交互的单个工作副本,但你需要做一些操作才能实现。首先,你需要确保主代码库和镜像代码库具有相同的代码库 UUID(默认情况下,它们不相同)。有关详细信息,请参阅本章后面关于“管理代码库 UUID”部分的内容

一旦两个代码库具有相同的 UUID,你就可以使用 svn switch--relocate 选项来将你的工作副本指向你希望针对其进行操作的任何一个代码库,此过程在svn switch 中进行了描述。不过,这里存在一个潜在的危险,如果主代码库和镜像代码库没有紧密同步,则指向主代码库并与之保持最新状态的工作副本,如果重新定位为指向过时的镜像,将会对它完全期望存在的修订版突然消失感到困惑,并且会抛出相关错误。如果发生这种情况,你可以将你的工作副本重新定位回主代码库,然后要么等待镜像代码库更新,要么将你的工作副本回滚到你知道存在于同步代码库中的修订版,然后重试重新定位。

最后,请注意,svnsync 提供的基于修订版的复制只是修订版的复制。只有 Subversion 代码库转储文件格式携带的信息可用作复制。因此,svnsync 具有代码库转储流的相同限制,并且不包括挂钩实现、代码库或服务器配置数据、未提交的事务或有关用户对代码库路径的锁的信息。

代码库备份

尽管自现代计算机诞生以来技术取得了长足进步,但有一件事不幸的是仍然非常清楚——有时候事情会非常非常糟糕。电源故障、网络连接中断、内存损坏和硬盘崩溃只是命运准备对即使是最尽职的管理员释放的邪恶的一小部分。因此,我们来到了一个非常重要的主题——如何创建代码库数据的备份副本。

Subversion 代码库管理员可以使用两种类型的备份方法——完整备份和增量备份。代码库的完整备份涉及在一次大规模操作中保存所有重建代码库所需的信息(以防发生灾难)。通常,这意味着,从字面上讲,复制整个代码库目录(其中包含 Berkeley DB 或 FSFS 环境)。增量备份是较小的备份:仅备份自上次备份以来发生更改的代码库数据部分。

就完整备份而言,幼稚的方法似乎是一种明智的做法,但除非你暂时禁用对代码库的所有其他访问,否则简单的递归目录复制会产生错误备份的风险。对于 Berkeley DB,文档描述了可以以特定顺序复制数据库文件,以确保备份副本的有效性。FSFS 数据也存在类似的排序。但你不必自己实现这些算法,因为 Subversion 开发团队已经这样做了。svnadmin hotcopy 命令负责处理创建代码库热备份所涉及的细微差别。它的调用与 Unix cp 或 Windows copy 操作一样简单

$ svnadmin hotcopy /var/svn/repos /var/svn/repos-backup

生成的备份是一个功能齐全的 Subversion 代码库,如果出现严重问题,可以将其作为你的活动代码库的替换。

当复制 Berkeley DB 代码库时,你甚至可以指示 svnadmin hotcopy 在完成复制后从原始代码库中清除任何未使用的 Berkeley DB 日志文件(参见关于“清除未使用的 Berkeley DB 日志文件”部分的内容)。只需在命令行中提供 --clean-logs 选项即可。

$ svnadmin hotcopy --clean-logs /var/svn/bdb-repos /var/svn/bdb-repos-backup

还有其他围绕此命令的工具。Subversion 源代码分发包的 tools/backup/ 目录包含 hot-backup.py 脚本。此脚本在 svnadmin hotcopy 之上添加了一些备份管理功能,使你能够只保留每个代码库的最新配置备份数量。它将自动管理备份代码库目录的名称以避免与之前的备份冲突,并且将“轮换掉”旧的备份,删除它们,以便只保留最新的备份。即使你也有增量备份,你可能也希望定期运行此程序。例如,你可能考虑从程序调度程序(例如 Unix 系统上的 cron)运行 hot-backup.py,这会导致它在夜间(或在你认为安全的任何时间粒度)运行。

一些管理员使用围绕生成和存储代码库转储数据构建的另一种备份机制。我们在关于“将代码库数据迁移到其他位置”部分的内容 中描述了如何使用 svnadmin dump--incremental 选项执行给定修订版或修订版范围的增量备份。当然,你可以通过省略该命令的 --incremental 选项来实现此方法的完整备份变体。这些方法具有一定的价值,因为你备份的信息格式很灵活——它不依赖于特定的平台、版本控制文件系统类型或 Subversion 或 Berkeley DB 的版本。但这种灵活性是有代价的,即恢复这些数据可能需要很长时间——随着你的代码库中提交的每个新修订版,时间会更长。此外,与许多不同的备份方法一样,对已备份修订版进行的修订版属性更改不会被不重叠的增量转储生成捕获。出于这些原因,我们建议不要完全依赖基于转储的备份方法。

如你所见,各种备份类型和方法各有优缺点。最简单的是完整的热备份,它将始终产生你的代码库的完美工作副本。如果你的活动代码库发生故障,你可以使用简单的递归目录复制从备份中恢复。不幸的是,如果你维护了代码库的多个备份,这些完整副本将每个占用与你的活动代码库一样多的磁盘空间。相比之下,增量备份的生成速度更快,存储空间更小。但是恢复过程可能很痛苦,通常需要应用多个增量备份。其他方法也有其自身的特点。管理员需要在创建备份的成本和恢复备份的成本之间找到平衡点。

svnsync 程序(参见关于“代码库复制”部分的内容)实际上提供了一种相当方便的折衷方法。如果你定期将只读镜像与你的主代码库同步,那么,在紧急情况下,你的只读镜像可能是替换该主代码库的不错选择(如果它出现故障)。这种方法的主要缺点是,只有版本控制的代码库数据被同步——代码库配置文件、用户指定的代码库路径锁以及其他可能存在于物理代码库目录中但不在代码库的虚拟版本控制文件系统内部 的项目不被 svnsync 处理。

在任何备份场景中,仓库管理员都需要了解对未版本化的修订属性的修改如何影响他们的备份。由于这些更改本身不会生成新的修订,因此它们不会触发提交后钩子,甚至可能不会触发 pre-revprop-change 和 post-revprop-change 钩子。[39] 并且由于您可以更改修订属性而不必考虑时间顺序——您可以随时更改任何修订的属性——因此对最近几个修订的增量备份可能无法捕获对作为先前备份的一部分包含的修订的属性修改。

一般来说,只有真正偏执的人才需要备份他们的整个仓库,比如,每次提交都备份一次。但是,假设给定仓库有其他冗余机制,并且具有相对精细的粒度(例如每个提交的电子邮件或增量转储),那么仓库管理员可能会希望将数据库的热备份作为系统级夜间备份的一部分。这是你的数据——尽可能地保护它。

通常,对仓库备份的最佳方法是多元化的方法,利用此处描述的方法的组合。例如,Subversion 开发人员使用 hot-backup.py 和这些完整备份的 off-site rsync 每晚备份 Subversion 源代码仓库;保留所有提交和属性更改通知电子邮件的多个存档;并让各种志愿者使用 svnsync 维护仓库镜像。您的解决方案可能类似,但应根据您的需求以及便利性和偏执之间的微妙平衡进行定制。无论您做什么,都要定期验证您的备份——破损的备胎有什么用?虽然所有这些可能无法将您的硬件从命运的铁拳中拯救出来,[40] 但它肯定能帮助您从那些艰难时期中恢复过来。

管理仓库 UUID

Subversion 仓库有一个与之关联的通用唯一标识符 (UUID)。Subversion 客户端使用它来验证仓库的身份,当其他形式的验证不足时(例如检查仓库 URL,它可能会随着时间的推移而改变)。大多数 Subversion 仓库管理员很少(如果有的话)需要将仓库 UUID 视为 Subversion 的微不足道的实现细节。但是,有时会有关注此细节的理由。

一般来说,您希望您的活动仓库的 UUID 是唯一的。毕竟,这就是拥有 UUID 的目的。但是,有时您希望两个仓库的仓库 UUID 完全相同。例如,如果您复制一个仓库以进行备份,您希望备份成为原始仓库的完美副本,以便在您必须恢复该备份并替换活动仓库的情况下,用户不会突然看到看起来不同的仓库。当转储和加载仓库历史记录时(如前所述在 名为“将仓库数据迁移到其他位置”的部分),您可以决定是否将数据转储流中封装的 UUID 应用于您正在加载数据的仓库。具体情况将决定正确的行为。

有几种方法可以设置(或重置)仓库的 UUID,如果您需要这样做。从 Subversion 1.5 开始,这与使用 svnadmin setuuid 命令一样简单。如果您为该子命令提供显式 UUID,它将验证 UUID 是否格式正确,然后将仓库 UUID 设置为该值。如果您省略 UUID,将为您的仓库生成一个全新的 UUID。

$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$ svnadmin setuuid /var/svn/repos   # generate a new UUID
$ svnlook uuid /var/svn/repos
3c3c38fe-acc0-11dc-acbc-1b37ff1c8e7c
$ svnadmin setuuid /var/svn/repos \
           cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec  # restore the old UUID
$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$

对于使用低于 1.5 版本的 Subversion 的用户,这些任务稍微复杂一些。您可以通过将包含新的 UUID 规范的仓库转储文件存根通过 svnadmin load --force-uuid REPOS-PATH 传递来显式设置仓库的 UUID。

$ svnadmin load --force-uuid /var/svn/repos <<EOF
SVN-fs-dump-format-version: 2

UUID: cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
EOF
$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$

让较旧版本的 Subversion 生成一个全新的 UUID 并不那么容易。您最好的选择是在此处找到其他方法来生成 UUID,然后将仓库的 UUID 显式设置为该值。



[32] 还是那个“同步”?

[33] 例如,硬盘 + 超大电磁铁 = 灾难。

[34] 这就是您使用版本控制的原因,对吧?

[35] 有意识地、谨慎地删除某些版本化数据实际上是真实用例支持的。这就是为什么“清除”功能一直是 Subversion 最受请求的功能之一,也是 Subversion 开发人员希望尽快提供的功能。

[36] 虽然 svnadmin dump 具有一致的前导斜杠策略(不包含它们),但其他生成转储数据的程序可能不那么一致。

[37] 事实上,它不能真正是只读的,否则 svnsync 本身将很难将修订历史记录复制到其中。

[38] 请注意,虽然普通读者只需几秒钟即可解析本段及其后的示例输出,但完成此类镜像操作所需的实际时间是,怎么说呢,要长得多。

[39] svnadmin setlog 可以以完全绕过钩子接口的方式调用。

[40] 你知道——所有“反复无常的手指”的总称。

TortoiseSVN 官方中文版 1.14.7 发布