本文档尚在编写中,内容可能会发生重大变化,可能无法准确描述 Apache™ Subversion® 软件的任何已发布版本。将此页面加入书签或以其他方式将他人引导至此页面可能不是一个明智的选择。请访问 http://svnbooks.subversion.org.cn/ 获取该手册的稳定版本。

仓库维护

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

管理员工具包

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 独特用途的更多信息,请参阅 名为“筛选仓库历史记录”的部分

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 仓库只读镜像所需的所有功能。该程序实际上只有一个工作 - 将一个仓库的版本化历史记录传输到另一个仓库。虽然有几种方法可以做到这一点,但它的主要优势是它可以在远程运行 - 接收[53] 仓库可能位于彼此和 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 版本会将所有修订版本文件存储在单个目录中,该目录随着仓库的生命周期不断增长(每个修订版本一个文件)。这在对给定目录中允许的文件数量有硬限制的系统上造成了问题,即使在没有此类限制或限制设置得足够高的情况下,它也是性能负担。

从版本 1.5 开始,Subversion 使用略微修改的布局来创建 FSFS 支持的仓库,其中修订版本文件目录(以及其他始终在增长的目录)的内容被 分片,或者散布在多个子目录中。这可以极大地减少系统查找任何一个文件所需的时间,从而提高 Subversion 从仓库读取时的整体性能。

允许存在于给定子目录中的文件数量是可配置的(尽管默认值对于大多数已知平台来说是合理的),但如果在仓库使用一段时间后更改此配置,可能会导致 Subversion 无法找到它正在寻找的文件。这就是 fsfs-reshard.py 的作用。

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

提交日志消息更正

有时用户会在她的日志消息中出现错误(可能是拼写错误或一些错误信息)。如果仓库配置为(使用 pre-revprop-change 钩子;请参阅 名为“实现仓库钩子”的部分)在提交完成后接受对该日志消息的更改,则用户可以使用 svn propset 远程 修复 她的日志消息(请参阅 svn propset (pset, ps) 中的 svn 参考 - 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 或更高版本创建的存储库进一步享受了 表示共享带来的磁盘空间节省,此功能允许具有相同文件内容的多个文件或文件修订版引用该数据的单个共享实例,而不是每个文件都拥有各自的独立副本。

删除死事务

虽然这种情况并不常见,但某些情况下,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.3,“txn-info.sh(报告未完成的事务)” 包含一些 shell 脚本,可以快速生成有关存储库中每个未完成事务的信息。

示例 5.3. 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 修订版历史记录等。当然,管理员通常可以简单地与看似死掉的事务的所有者(例如通过电子邮件)进行沟通,以验证事务确实处于僵尸状态。

打包 FSFS 文件系统

FSFS 存储库包含描述单个修订版中所做的更改的文件,以及包含与单个修订版关联的修订版属性的文件。在 Subversion 1.5 之前的版本中创建的存储库将这些文件保留在两个目录中——每个类型一个目录。随着新的修订版提交到存储库,Subversion 会将更多文件放入这两个目录中——随着时间的推移,每个目录中这些文件的数量可能会变得非常多。据观察,这会在某些基于网络的文件系统上造成性能问题。

第一个问题是,操作系统必须在短时间内引用许多不同的文件。这会导致对磁盘缓存的低效使用,并因此导致更多时间花费在大型磁盘上的寻道上。因此,Subversion 在访问您的版本化数据时会付出性能代价。

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

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

Subversion 可以打包现有的已升级到 1.6 文件系统格式或更高版本(参见 svnadmin upgrade)的碎片存储库,在 svnadmin 参考资料——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.
$

由于打包过程在执行操作之前会获取所需的锁,因此您可以在活动存储库上运行它,甚至可以作为 post-commit 钩子的一部分运行它。重新打包已打包的碎片是合法的,但不会影响存储库的磁盘使用量。

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

请注意,由于 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 或更高版本。它目前仅生成与您将 --deltas 选项传递给 svnadmin dump 时创建的转储流相同类型的转储流。这在典型用例中并不有趣,但可能会影响您可能希望对生成的转储流应用的特定类型的自定义转换。

[Note] 注意

由于它在提交新的修订版本后会修改修订版本属性,因此 svnrdump load 要求目标仓库通过预提交修订版本更改钩子启用修订版本属性更改。有关详细信息,请参阅 pre-revprop-change,该部分位于 Subversion 仓库钩子参考 中。

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

[Tip] 提示

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

过滤仓库历史记录

由于 Subversion 使用二进制差异算法和数据压缩(可选地使用完全不透明的数据库系统)来存储您的版本化历史记录,因此尝试手动调整是不明智的,即使不是非常困难,也强烈建议不要这样做。而且,一旦数据存储在您的版本库中,Subversion 通常不提供简单的方法来删除该数据。[54] 但不可避免地,您会希望操作版本库的历史记录。您可能需要删除所有意外添加到版本库中的文件实例(无论出于何种原因,这些文件都不应该存在)。[55] 或者,您可能有多个项目共享一个版本库,并且您决定将它们拆分成各自的版本库。为了完成这些任务,管理员需要一个更易于管理和修改的版本库数据表示形式——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 的路径中包含前导斜杠(如果路径没有前导斜杠,则不应包含前导斜杠)。此外,如果您的转储文件由于某种原因对前导斜杠的使用不一致,[56] 您可能应该规范化这些路径,以便它们都具有或都缺少前导斜杠。

此外,复制的路径可能会给您带来一些麻烦。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 无法 要求 目标版本库为只读,[57] 允许目标版本库中的版本历史记录通过任何其他机制进行更改,而不是镜像过程,这会导致灾难。

[Warning] 警告

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

目标存储库的另一个要求是,允许svnsync进程修改修订版本属性。因为svnsync在该存储库的钩子系统框架内工作,所以存储库的默认状态(即不允许修改修订版本属性;请参见pre-revprop-change in 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.4,“镜像存储库的 pre-revprop-change 钩子脚本”中,它基本上验证尝试修改属性的用户是否是我们的 syncuser 用户。如果是,则允许修改;否则,则拒绝修改。

示例 5.4。镜像存储库的 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.5,“镜像存储库的 start-commit 钩子脚本”中的脚本。

示例 5.5。镜像存储库的 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将所有尚未镜像的修订版本从源存储库复制到目标存储库。[58] 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,并从中提取版本化数据。

但是,该过程中确实存在一些不优雅之处。由于 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 hook 实现的一部分。这将使镜像能够尽可能接近实时地保持最新状态。

使用 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.
$

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

复制总结

我们讨论了几种将修订版历史记录从一个代码库复制到另一个代码库的方法。因此,现在让我们看一下这些操作的用户端。复制和各种需要复制的情况如何影响 Subversion 客户端?

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

一旦两个代码库具有相同的 UUID,您就可以使用 svn relocate 将您的工作副本指向您想要操作的任何代码库,此过程在 svn relocate 中进行了描述,该方法位于 svn 参考——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 代码库,如果发生可怕的事情,可以作为您的实时代码库的替代品。

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

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

从 Subversion 1.8 开始,svnadmin hotcopy 接受 --incremental 选项并为 FSFS 仓库支持增量热备份模式。在增量热备份模式下,已经从源仓库复制到目标仓库的修订版数据将不再被复制。当使用 --incremental 选项与 svnadmin hotcopy 一起使用时,Subversion 将只复制新的修订版,以及自上次热备份操作以来大小发生变化或修改时间戳发生变化的修订版。此外,与 svnsyncsvnadmin dump --incremental 不同,svnadmin hotcopy --incremental 的性能仅受磁盘 I/O 的限制。因此,在备份大型仓库时,增量热备份可以节省大量时间。

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

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

在任何备份情况下,仓库管理员都需要了解对非版本化修订版属性的修改如何影响他们的备份。由于这些更改本身不会生成新的修订版,因此它们不会触发 post-commit 钩子,甚至可能不会触发 pre-revprop-change 和 post-revprop-change 钩子。[59] 由于您可以不考虑时间顺序地更改修订版属性——您可以在任何时候更改任何修订版的属性——最近几个修订版的增量备份可能无法捕获对包含在以前备份中的修订版的属性修改。

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

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

管理仓库 UUID

Subversion 仓库具有与之关联的通用唯一标识符 (UUID)。Subversion 客户端使用它来验证仓库的身份,当其他形式的验证不够好时(例如检查仓库 URL,该 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 明确设置为该值。



[53] 或者,是 同步吗?

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

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

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

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

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

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

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

TortoiseSVN 官方中文版 1.14.7 发布