本文档旨在描述 Apache™ Subversion® 的 1.7.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 仓库管理”的部分,该部分位于 第 9 章,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)修订版。因此,当 19 是位于 /var/svn/repos 的存储库中最新的修订版时,以下两个命令执行完全相同的事情

$ 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 存储库检查”的部分第 9 章,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 独特用途的更多信息,在 名为“过滤存储库历史记录”的部分 中。

svnrdump

简而言之,svnrdump 程序本质上只是 svnadmin dumpsvnadmin load 子命令的网络感知版本,它们被整合到一个单独的程序中。

$ svnrdump help
general usage: svnrdump SUBCOMMAND URL [-r LOWER[:UPPER]]
Type 'svnrdump help <subcommand>' for help on a specific subcommand.
Type 'svnrdump --version' to see the program version and RA modules.

Available subcommands:
   dump
   load
   help (?, h)

$

我们将在本章后面讨论 svnrdump 和上述 svnadmin 命令的使用(请参阅 名为“将存储库数据迁移到其他位置”的部分)。

svnsync

svnsync 程序提供了维护 Subversion 存储库只读镜像所需的所有功能。该程序实际上只有一项工作——将一个存储库的版本化历史记录传输到另一个存储库。虽然有几种方法可以做到这一点,但它的主要优势在于它可以远程操作——接收[43] 存储库可能位于彼此不同的计算机上,也可能位于与 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 使用,这些工具是不必要的。大多数通常用于 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 (pset, ps)第 9 章,Subversion 完整参考 中)。但是,由于可能永远丢失信息,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-change 和 post-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 版本开始,数据库环境能够自动删除自身未使用的日志文件。使用 svnadmin 创建的任何存储库,只要是在编译时针对 Berkeley DB 4.2 或更高版本进行的,都会配置为自动删除日志文件。如果您不想启用此功能,只需将 --bdb-log-keep 选项传递给 svnadmin create 命令即可。如果您忘记这样做,或者之后改变主意,只需编辑存储库 db 目录中的 DB_CONFIG 文件,注释掉包含 set_flags DB_LOG_AUTOREMOVE 指令的行,然后在您的存储库上运行 svnadmin recover 命令,以强制配置更改生效。有关数据库配置的更多信息,请参见 名为“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 将给定碎片中的文件数量减少到一个文件。这样做有助于文件系统缓存,并将文件存储开销惩罚的次数减少到一次。

Subversion 可以打包已升级到 1.6 文件系统格式或更高版本的现有分片存储库(参见 svnadmin upgrade)在 第 9 章,Subversion 完整参考 中。为此,只需在存储库上运行 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 文件系统利用数据库事务、检查点和预写日志记录来确保只有最灾难性的事件[44] 才能永久破坏数据库环境。一个足够谨慎的存储库管理员会以某种方式对存储库数据进行异地备份,但现在不要急着去磁带备份存储室。

相反,请使用以下方法尝试解卡您的仓库

  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 子命令,以及 svnrdump 程序。

[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 仓库转储格式仅描述版本化的仓库更改。它不会携带有关未提交的事务、文件系统路径上的用户锁定、仓库或服务器配置自定义(包括挂钩脚本)等的任何信息。

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

目前,我们只关注 Subversion 仓库之间的数据迁移,将在接下来的部分详细介绍。

使用 svnadmin 进行仓库数据迁移

无论您迁移仓库历史记录的原因是什么,使用 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 选项,您还可以执行另一个巧妙的操作,即向现有的转储文件追加新的转储修订版本范围。例如,您可能有一个提交后钩子,它只追加触发钩子的单个修订版本的存储库转储。或者,您可能有一个脚本,它每天晚上运行以追加自上次脚本运行以来添加到存储库的所有修订版本的转储文件数据。像这样使用,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
…
$

使用 svnrdump 进行存储库数据迁移

在 Subversion 1.7 中,svnrdump 加入了一组标准的 Subversion 工具。它提供了相当专业的功能,本质上是 svnadmin dumpsvnadmin load 命令的网络感知版本,我们在 名为“使用 svnadmin 进行存储库数据迁移”的部分 中对其进行了深入讨论。 svnrdump dump 将从远程存储库生成转储流,并将其输出到标准输出;svnrdump load 将从标准输入读取转储流并将其加载到远程存储库中。使用 svnrdump,您可以生成增量转储,就像使用 svnadmin dump 一样。您甚至可以转储存储库的子树——这是 svnadmin dump 无法做到的。

主要区别在于,svnrdump 不需要直接访问仓库,而是远程操作,使用与 Subversion 客户端相同的仓库访问 (RA) 协议。因此,您可能需要提供身份验证凭据。此外,您的远程交互会受到 Subversion 服务器上配置的任何授权限制的影响。

[Note] 注意

svnrdump dump 要求远程服务器运行 Subversion 1.4 或更高版本。它目前只生成当您向 svnadmin dump 传递 --deltas 选项时创建的转储流。这在典型用例中并不有趣,但可能会影响您可能希望对生成的转储流应用的特定类型的自定义转换。

[Note] 注意

由于它在提交新修订版后修改修订版属性,svnrdump load 要求目标仓库通过 pre-revprop-change 钩子启用修订版属性更改。有关详细信息,请参阅 pre-revprop-change,位于 第 9 章,Subversion 完整参考 中。

正如您所料,您可以将 svnadminsvnrdump 结合使用。例如,您可以使用 svnrdump dump 从远程仓库生成转储流,并将结果通过管道传递给 svnadmin load,将所有仓库历史记录复制到本地仓库。或者,您可以反过来,将历史记录从本地仓库复制到远程仓库。

[Tip] 提示

通过使用 file:// URL,svnrdump 也可以访问本地仓库,但它将通过 Subversion 的仓库访问 (RA) 抽象层进行访问——在这种情况下,您将从 svnadmin 获得更好的性能。

过滤仓库历史记录

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

正如我们在名为“将仓库数据迁移到其他地方”的部分中所述,Subversion 仓库转储格式是您对版本化数据所做的更改随时间推移的人类可读表示。使用 svnadmin dumpsvnrdump dump 命令生成转储数据,并使用 svnadmin loadsvnrdump 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
  
[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 的路径中包含前导斜杠(如果路径不包含前导斜杠,则不应包含)。此外,如果您的转储文件由于某种原因对前导斜杠的使用不一致,[47] 您可能需要规范化这些路径,以便它们都包含或都不包含前导斜杠。

此外,复制的路径可能会给你带来一些麻烦。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 进行复制

假设您已经拥有一个要镜像的源存储库,那么您需要的下一件事是一个目标存储库,它将充当该镜像。此目标存储库可以使用两种可用的文件系统数据存储后端(请参阅 名为“选择数据存储”的部分)——Subversion 的抽象层确保这些细节无关紧要。但默认情况下,它不应在其中包含任何版本历史记录。(我们将在本节后面讨论对此的例外情况。)

svnsync 用于通信修订版信息的协议对源存储库和目标存储库中包含的版本化历史记录之间的不匹配非常敏感。因此,虽然 svnsync 无法要求 目标存储库为只读,[48] 允许目标存储库中的修订版历史记录通过除镜像过程之外的任何机制更改,这将导致灾难。

[Warning] 警告

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

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

[Tip] 提示

建议实施授权措施,允许您的仓库复制过程执行其任务,同时阻止其他用户修改镜像仓库的内容。

让我们逐步了解 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 \
                     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 将所有尚未镜像的修订版本从源代码库复制到目标代码库。[49] svnsync synchronize 子命令将查看之前存储在目标代码库中的特殊修订版本属性,并确定源代码库中已镜像的部分 - 在这种情况下,最近镜像的修订版本是 r0。然后,它将查询源代码库并确定该代码库中的最新修订版本。最后,它要求源代码库的服务器开始重放 0 到最新修订版本之间的所有修订版本。当 svnsync 从源代码库的服务器获取结果响应时,它会开始将这些修订版本转发到目标代码库的服务器作为新的提交。

$ svnsync help synchronize
synchronize (sync): usage: svnsync synchronize DEST_URL [SOURCE_URL]

Transfer all pending revisions to the destination from the source
with which it was initialized.
…
$ svnsync synchronize http://svn.example.com/svn-mirror \
                      https://svn.code.sf.net/p/svnbook/source
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 命令,它将愉快地从中断的地方继续执行。实际上,当源代码库中出现新的修订版本时,这正是您用来保持镜像更新的操作。

[Warning] 警告

作为其簿记的一部分,svnsync 在镜像代码库中记录了用于初始化镜像的 URL。因此,在初始化步骤之后调用的 svnsync需要您再次在命令行中提供源 URL。但是,出于安全目的,我们建议您继续这样做。根据其部署方式,svnsync 可能无法安全地信任从镜像代码库中检索到的源 URL,以及从该 URL 中提取版本化数据。

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

$ svnsync help copy-revprops
copy-revprops: usage:

    1. svnsync copy-revprops DEST_URL [SOURCE_URL]
    2. svnsync copy-revprops DEST_URL REV[:REV2]

…
$ svnsync copy-revprops http://svn.example.com/svn-mirror 12
Copied properties for revision 12.
$

这就是通过 svnsync 进行的代码库复制的概况。您可能希望围绕此类过程进行一些自动化。例如,虽然我们的示例是拉取和推送设置,但您可能希望让您的主代码库将更改推送到一个或多个经过验证的镜像,作为其 post-commit 和 post-revprop-change 钩子实现的一部分。这将使镜像尽可能接近实时地保持最新状态。

使用 svnsync 进行部分复制

svnsync 不仅限于对代码库中所有内容的完整复制。它也可以处理各种部分复制的阴影。例如,虽然这样做并不常见,但 svnsync 确实可以优雅地镜像用户以其身份进行身份验证的用户只有部分读取权限的代码库。它只复制它被允许查看的代码库部分。显然,这样的镜像对于备份解决方案来说是无用的。

从 Subversion 1.5 开始,svnsync 也可以镜像仓库的一部分,而不是整个仓库。设置和维护这种镜像的过程与镜像整个仓库完全相同,只是在运行 svnsync init 时,您不是指定源仓库的根 URL,而是指定该仓库中某个子目录的 URL。现在,同步到该镜像将只复制该源仓库子目录下发生更改的部分。不过,这种支持有一些限制。首先,您不能将源仓库的多个不相交的子目录镜像到单个镜像仓库中——您需要镜像这两个子目录共有的某个父目录。其次,过滤逻辑完全基于路径,因此,如果您镜像的子目录在过去某个时间点被重命名,您的镜像将只包含该目录出现在您指定的 URL 之后的所有修订版本。同样,如果源子目录在将来被重命名,您的同步进程将在您指定的源 URL 无效时停止镜像数据。

镜像创建的快速技巧

我们之前提到过设置现有仓库的初始镜像的成本。对于许多人来说,通过 svnsync 将数千甚至数百万个历史修订版本传输到新的镜像仓库的成本是不可接受的。幸运的是,Subversion 1.7 提供了一种解决方法,即为 svnsync initialize 提供了一个新的 --allow-non-empty 选项。此选项允许您将一个仓库初始化为另一个仓库的镜像,同时绕过对要初始化的镜像中不存在版本历史记录的验证。根据我们之前关于此整个复制过程敏感性的警告,您应该正确地判断,此选项只应谨慎使用。但是,当您对源仓库具有管理访问权限时,它非常有用,您可以简单地制作仓库的物理副本,然后将该副本初始化为新的镜像。

$ svnadmin hotcopy /path/to/repos /path/to/mirror-repos
$ ### create /path/to/mirror-repos/hooks/pre-revprop-change
$ svnsync initialize file:///path/to/mirror-repos \
                     file:///path/to/repos
svnsync: E000022: Destination repository already contains revision history; co
nsider using --allow-non-empty if the repository's revisions are known to mirr
or their respective revisions in the source repository
$ svnsync initialize --allow-non-empty file:///path/to/mirror-repos \
                                       file:///path/to/repos
Copied properties for revision 32042.
$

运行 Subversion 1.7 之前版本的管理员(因此无法访问 svnsync initialize--allow-non-empty 功能)可以通过对即将成为原始仓库镜像的仓库副本的 r0 修订版本属性进行 仔细 操作来有效地完成该功能。使用 svnadmin setrevprop 创建 svnsync 在那里创建的相同簿记属性。

复制总结

我们已经讨论了几种将修订历史从一个仓库复制到另一个仓库的方法。现在让我们看看这些操作的用户端。复制以及各种需要复制的情况如何影响 Subversion 客户端?

就用户与仓库和镜像的交互而言,可以拥有一个与两者交互的单个工作副本,但您需要进行一些操作才能实现。首先,您需要确保主仓库和镜像仓库具有相同的仓库 UUID(默认情况下并非如此)。有关更多信息,请参阅本章后面的名为“管理仓库 UUID”的部分

一旦两个仓库具有相同的 UUID,您就可以使用svn relocate将您的工作副本指向您希望对其进行操作的任何仓库,此过程在svn relocate(位于第 9 章,Subversion 完整参考中)中进行了描述。但是,这里存在一个潜在的危险,即如果主仓库和镜像仓库没有紧密同步,那么一个与主仓库同步并指向主仓库的工作副本,如果重新定位到指向过时的镜像,将会对它完全期望存在的修订版本突然消失感到困惑,并会抛出相应的错误。如果发生这种情况,您可以将您的工作副本重新定位回主仓库,然后等待镜像仓库更新,或者将您的工作副本回溯到您知道存在于同步仓库中的修订版本,然后重试重新定位。

最后,请注意,svnsync 提供的基于修订版本的复制仅仅是修订版本的复制。只有 Subversion 仓库转储文件格式所携带的信息类型可用于复制。因此,诸如svnsync(以及我们在名为“使用 svnrdump 进行仓库数据迁移”的部分中讨论的svnrdump)之类的工具在功能上与仓库转储流类似。它们不会在其复制的信息中包含诸如钩子实现、仓库或服务器配置数据、未提交的事务或有关用户对仓库路径的锁定的信息。

仓库备份

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

对于 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 之上添加了一些备份管理功能,允许您只保留每个仓库最近配置数量的备份。它会自动管理备份仓库目录的名称,以避免与之前的备份冲突,并会 轮换掉 较旧的备份,删除它们,以便只保留最新的备份。即使您也有增量备份,您可能也希望定期运行此程序。例如,您可以考虑使用 hot-backup.py 从程序调度器(例如 Unix 系统上的 cron)运行,这可以使它在夜间(或您认为安全的任何时间粒度)运行。

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

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

svnsync 程序(参见名为“仓库复制”的部分)实际上提供了一种非常方便的折衷方案。如果您定期将只读镜像与主仓库同步,那么在紧急情况下,您的只读镜像可能是主仓库发生故障时替换主仓库的理想选择。此方法的主要缺点是只有版本化仓库数据会被同步——仓库配置文件、用户指定的仓库路径锁以及可能存在于物理仓库目录中但不在仓库虚拟版本化文件系统内部的其他项目不会被svnsync处理。

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

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

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

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



[43] 或者,是 同步 吗?

[44] 例如,硬盘 + 强力电磁体 = 灾难。

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

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

[47] 虽然 svnadmin dump 具有始终如一的领先斜杠策略(不包含它们),但其他生成转储数据的程序可能并不那么一致。

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

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

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

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

TortoiseSVN 官方中文版 1.14.7 发布