这份文档是为了描述 Subversion 1.6.x 系列而编写的。如果您运行的是不同版本的 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——Subversion 仓库管理”部分,了解所有子命令及其功能。

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
$
[Note] 注意

请记住,您只能浏览未提交的事务。大多数仓库没有这样的事务,因为事务通常要么已提交(在这种情况下,您应该使用 --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——Subversion 仓库检查”部分,了解 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 仓库只读镜像所需的所有功能。该程序只有一个工作——将一个仓库的版本化历史记录传输到另一个仓库中。虽然有很多方法可以做到这一点,但它的主要优势在于它可以远程操作——接收[35] 仓库可以与 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
   info
   help (?, h)
$

我们将在本章后面(“仓库复制”部分)详细介绍使用 svnsync 复制仓库。

fsfs-reshard.py

虽然它不是 Subversion 工具链的正式成员,但 fsfs-reshard.py 脚本(位于 Subversion 源代码分发版的 tools/server-side 目录中)是 FSFS 支持的 Subversion 仓库管理员的实用性能调整工具。如侧边栏 修订版本文件和分片 中所述,FSFS 仓库使用单个文件来存储有关每个修订版本的信息。有时这些文件都位于同一个目录中;有时它们被分片到多个目录中。但巧妙的是,用于存储这些文件使用的目录数量是可配置的。这就是 fsfs-reshard.py 的用武之地。

fsfs-reshard.py 将仓库的文件结构重新排列成新的安排,该安排反映了请求的分片子目录数量,并更新仓库配置以保留此更改。当与 svnadmin upgrade 命令结合使用时,这对于将 1.5 之前的 Subversion(未分片)仓库升级到最新的文件系统格式并对数据文件进行分片(Subversion 不会自动为您执行此操作)特别有用。此脚本也可以用于微调已经分片的仓库。

Berkeley DB 实用程序

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

对于日常的 Subversion 使用,这些工具是多余的。svnadmin 工具已经复制了 Subversion 仓库通常需要的功能。例如,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 可以完成某些工作,并且速度更快。如果您需要出于某种原因对 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 (pset, ps))远程 修复 其日志消息。但是,由于可能永远丢失信息,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-revprop-changepost-revprop-change 钩子仍然会触发,因此必须设置它们以接受此类更改。但管理员可以通过将 --bypass-hooks 选项传递给 svnadmin setlog 命令来绕过这些保护措施。

[Warning] 警告

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

管理磁盘空间

虽然存储成本在过去几年中大幅下降,但对于希望对大量数据进行版本控制的管理员来说,磁盘使用仍然是一个合理的关注点。存储在活动存储库中的所有版本历史信息都需要备份到其他地方,可能作为旋转备份计划的一部分进行多次备份。了解哪些 Subversion 存储库数据需要保留在活动站点上、哪些需要备份以及哪些可以安全删除非常有用。

Subversion 如何节省磁盘空间

为了使存储库保持较小,Subversion 在存储库本身内使用 增量存储(或基于增量的存储)。增量存储涉及将数据块的表示编码为相对于其他数据块的差异集合。如果两个数据片段非常相似,则此增量存储将为增量存储的片段节省存储空间 - 它不会占用与原始数据大小相同的空间,而只占用足够的空间来说明 我看起来就像这里另一个数据片段,除了以下几个更改。" 结果是,存储库中大多数倾向于占用大量空间的数据 - 即版本化文件的内容 - 的存储大小远小于该数据原始全文本表示的大小。

虽然增量存储从一开始就是 Subversion 设计的一部分,但多年来已经进行了其他改进。使用 Subversion 1.4 或更高版本创建的 Subversion 存储库受益于文件内容全文本表示的压缩。使用 Subversion 1.6 或更高版本创建的存储库进一步享受了 表示共享带来的磁盘空间节省,此功能允许具有相同文件内容的多个文件或文件修订版引用该数据的单个共享实例,而不是每个文件都拥有自己的不同副本。

[Note] 注意

由于 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!
[Warning] 警告

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

打包 FSFS 文件系统

如边栏 修订文件和分片 中所述,FSFS 支持的 Subversion 存储库默认情况下会为添加到存储库的每个修订版创建一个新的磁盘文件。在您的 Subversion 服务器上存在数千个这样的文件 - 即使它们位于单独的分片目录中 - 也会导致低效。

第一个问题是操作系统必须在短时间内引用许多不同的文件。这会导致磁盘缓存使用效率低下,因此会导致更多时间花在跨大磁盘进行查找上。因此,Subversion 在访问您的版本化数据时会受到性能影响。

第二个问题比较微妙。由于大多数文件系统分配磁盘空间的方式,每个文件在磁盘上所占的空间都比实际使用的大小更多。容纳单个文件所需的额外空间平均可以达到 2 到 16 千字节 每个文件,具体取决于使用的底层文件系统。对于 FSFS 支持的存储库,这会直接转化为每个修订版的磁盘使用量惩罚。在具有许多较小修订版的存储库中,这种影响最为明显,因为存储修订文件所涉及的开销很快就会超过所存储的实际数据大小。

为了解决这些问题,Subversion 1.6 引入了 svnadmin pack 命令。通过将完成的分片的所有文件连接到单个 pack 文件中,然后删除原始的每个修订版文件,svnadmin pack 将给定分片中的文件数量减少到单个文件。这样做有助于文件系统缓存,并将存储文件开销惩罚的次数(减少到 1 次)。

Subversion 可以打包已升级到 1.6 文件系统格式(参见 svnadmin upgrade)的现有分片存储库。为此,只需在存储库上运行 svnadmin pack

$ svnadmin pack /var/svn/repos
Packing shard 0...done.
Packing shard 1...done.
Packing shard 2...done.
…
Packing shard 34...done.
Packing shard 35...done.
Packing shard 36...done.
$

由于打包过程在执行工作之前会获得所需的锁,因此您可以在活动存储库上运行它,甚至可以在提交后钩子中运行它。重新打包已打包的分片是合法的,但不会影响存储库的磁盘使用量。

svnadmin pack 对 BDB 支持的 Subversion 存储库没有影响。

Berkeley DB 恢复

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

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

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

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

相反,请使用以下方法尝试解除卡住 您的存储库

  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 子命令。

[Warning] 警告

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

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

[Note] 注意

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 分别为每个加载的修订版执行预提交和后提交挂钩程序。例如,您可以使用它们来确保加载的修订版通过与常规提交相同的验证步骤。当然,您应该谨慎使用这些选项——如果您的后提交挂钩程序向邮件列表发送每个新提交的电子邮件,您可能不想在该列表上快速连续地发送数百或数千封提交电子邮件!您可以在名为“实现存储库挂钩”的部分 中阅读有关挂钩脚本使用的更多信息。

请注意,由于 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 实用程序(参见 名为“Converting a Repository from CVS to Subversion”的部分)使用转储格式来表示 CVS 存储库的内容,以便可以将这些内容复制到 Subversion 存储库中。

过滤存储库历史记录

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

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

这就是 svndumpfilter 派上用场的地方。此程序充当存储库转储流的基于路径的过滤器。只需提供您要保留的路径列表或您不希望保留的路径列表,然后将存储库转储数据通过此过滤器传递。结果将是一个修改后的转储数据流,其中仅包含您(显式或隐式)请求的版本化路径。

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

我们假设的存储库包含三个项目: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
  
[Warning] 警告

如果您确实打算手动编辑转储文件以删除顶级目录,请确保您的编辑器未设置为自动将行尾字符转换为本机格式(例如,将 \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 的路径中包含前导斜杠(如果没有,您不应该包含)。此外,如果您的转储文件出于某种原因对前导斜杠的使用不一致, [39] 您可能应该标准化这些路径,以便它们都具有或都缺少前导斜杠。

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

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

仓库复制

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

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

[Note] 注意

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

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

[Warning] 警告

不要以任何方式修改镜像仓库,使其版本历史偏离其镜像的仓库。在该镜像仓库上发生的唯一提交和修订版本属性修改应该由 svnsync 工具执行。

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

[Tip] 提示

最好实现授权措施,允许你的仓库复制进程执行其任务,同时防止其他用户修改镜像仓库的内容。

让我们在一个相当典型的镜像场景中逐步了解 svnsync 的用法。我们将在这个讨论中加入一些实用的建议,如果你在自己的环境中不需要或不适合这些建议,可以随意忽略它们。

我们将镜像公开的 Subversion 仓库,该仓库包含本书的源代码,并将该镜像公开发布在互联网上,托管在与原始 Subversion 源代码仓库所在的机器不同的机器上。这个远程主机有一个全局配置,允许匿名用户读取主机上的仓库的内容,但要求用户进行身份验证才能修改这些仓库。(请原谅我们目前忽略了 Subversion 服务器配置的细节——这些细节在 第 6 章,服务器配置 中有详细介绍。)为了让例子更有趣,我们将从第三台机器(我们目前使用的机器)驱动复制进程。

首先,我们将创建作为镜像的仓库。这一步和接下来的几步确实需要对镜像仓库所在的机器进行 shell 访问。但是,一旦仓库配置完毕,我们就不需要再直接访问它了。

$ ssh admin@svn.example.com "svnadmin create /var/svn/svn-mirror"
admin@svn.example.com'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 \
                     https://svn.code.sf.net/p/svnbook/source \
                     --sync-username syncuser --sync-password syncpass
Copied properties for revision 0 (svn:sync-* properties skipped).
NOTE: Normalized svn:* properties to LF line endings (1 rev-props, 0 node-props).
$

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

[Note] 注意

在 Subversion 1.4 中,提供给 svnsync--username--password 命令行选项的值用于对源仓库和目标仓库进行身份验证。当用户的凭据对这两个仓库不完全相同,尤其是在非交互模式(使用 --non-interactive 选项)下运行时,就会出现问题。这在 Subversion 1.5 中通过引入两对新选项得到了解决。使用 --source-username--source-password 提供源仓库的身份验证凭据;使用 --sync-username--sync-password 提供目标仓库的凭据。(旧的 --username--password 选项仍然存在,以保持兼容性,但我们建议不要使用它们。)

现在是乐趣开始的时候了。使用一个子命令,我们可以告诉 svnsync 将所有尚未镜像的修订版本从源仓库复制到目标仓库。 [41] 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
Committed revision 1.
Copied properties for revision 1.
Committed revision 2.
Copied properties for revision 2.
Transmitting file data .
Committed revision 3.
Copied properties for revision 3.
…
Transmitting file data .
Committed revision 4063.
Copied properties for revision 4063.
Transmitting file data .
Committed revision 4064.
Copied properties for revision 4064.
Transmitting file data ....
Committed revision 4065.
Copied properties for revision 4065.
$

这里特别值得注意的是,对于每个镜像的修订版本,首先会向目标仓库提交该修订版本,然后是属性更改。这种两阶段复制是必需的,因为初始提交是由(并归属于)用户 syncuser 执行的,并且使用该修订版本创建时的日期时间进行时间戳标记。 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 (sw) 中进行描述。但是,这里存在一个潜在的危险,即如果主存储库和镜像存储库没有紧密同步,则如果一个工作副本与主存储库保持最新并且指向主存储库,然后重新定位到指向过时的镜像,该工作副本将对预期存在的版本的突然丢失感到困惑,并且会抛出相应的错误。如果发生这种情况,您可以将工作副本重新定位回主存储库,然后等待镜像存储库更新,或将工作副本回退到您知道存在于同步存储库中的版本,然后重试重新定位。

最后,请注意,由 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 钩子。[42] 而且由于您可以不考虑时间顺序地更改修订属性——您可以随时更改任何修订的属性——对最近几个修订的增量备份可能无法捕获对作为先前备份的一部分包含的修订的属性修改。

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

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

管理仓库 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 显式地设置为该值。



[35] 或者,是“同步”?

[36] 例如,硬盘 + 巨型电磁铁 = 灾难。

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

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

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

[40] 事实上,它不能真正是只读的,否则 svnsync 本身在将修订历史复制到其中时会遇到困难。

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

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

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