本手册描述的是 Subversion 1.6.x 系列。如果您使用的是其他版本的 Subversion,强烈建议您访问 https://svnbook.subversion.org.cn/,并参阅适合您 Subversion 版本的手册。

Subversion 版本控制方式

我们之前已经提到,Subversion 是一款现代化的、网络感知的版本控制系统。正如我们在 “版本控制基础”部分(我们对版本控制的概述)中描述的那样,仓库充当 Subversion 版本化数据的核心存储机制,用户和他们的软件程序通过工作副本与这些数据进行交互。在本节中,我们将开始介绍 Subversion 实现版本控制的具体方式。

Subversion 仓库

Subversion 实现版本控制仓库的概念与任何其他现代版本控制系统类似。与工作副本不同,Subversion 仓库是一个抽象实体,几乎只能由 Subversion 自己的库和工具操作。由于大多数用户的 Subversion 交互都涉及使用 Subversion 客户端,并且发生在工作副本的上下文中,因此本书的大部分内容都讨论了 Subversion 工作副本以及如何操作它。但是,有关仓库的更详细内容,请查看 第 5 章,仓库管理

修订版本

Subversion 客户端将任意数量的文件和目录作为单个原子事务提交(即,将所做的更改传送到仓库)。原子事务的含义很简单:要么所有更改都被接受到仓库中,要么没有任何更改被接受。Subversion 试图在程序崩溃、系统崩溃、网络问题和其他用户的操作面前保持这种原子性。

每次仓库接受提交时,都会创建一个新的文件系统树状态,称为 修订版本。每个修订版本都被分配一个唯一的自然数,该数字比分配给先前修订版本的数字大 1。新创建的仓库的初始修订版本编号为 0,只包含一个空的根目录。

图 1.6,“树随时间变化” 说明了一种很好的仓库可视化方法。想象一个从左到右延伸的修订版本号数组,从 0 开始。每个修订版本号下面都挂着一个文件系统树,每个树都是仓库在提交后状态的 快照

图 1.6 树随时间变化

Tree changes over time

仓库地址

Subversion 客户端程序使用 URL 来标识 Subversion 仓库中的版本化文件和目录。在大多数情况下,这些 URL 使用标准语法,允许在 URL 中指定服务器名称和端口号。

  • http://svn.example.com/svn/project
  • http://svn.example.com:9834/repos

Subversion 仓库 URL 不限于 http:// 形式。由于 Subversion 为其客户端提供了几种与服务器通信的方式,因此用于访问仓库的 URL 会因所采用的仓库访问机制而略有不同。 表 1.1,“仓库访问 URL” 描述了不同的 URL 方案如何映射到可用的仓库访问方法。有关 Subversion 服务器选项的更多详细信息,请查看 第 6 章,服务器配置

表 1.1 仓库访问 URL

模式 访问方法
file:/// 直接仓库访问(在本地磁盘上)
http:// 通过 WebDAV 协议访问支持 Subversion 的 Apache 服务器
https:// http:// 相同,但使用 SSL 封装(加密和身份验证)
svn:// 通过自定义协议访问 svnserve 服务器
svn+ssh:// svn:// 相同,但通过 SSH 隧道

Subversion 对 URL 的处理有一些明显的细微差别。例如,包含 file:// 访问方法(用于本地仓库)的 URL 必须根据惯例,要么具有 localhost 的服务器名称,要么根本没有服务器名称。

  • file:///var/svn/repos
  • file://127.0.0.1/var/svn/repos

此外,Windows 平台上的 file:// 方案用户需要使用非官方的 标准 语法来访问与客户端当前工作驱动器不同的驱动器上的仓库。以下两种 URL 路径语法都可以使用,其中 X 是仓库所在的驱动器。

  • file:///X:/var/svn/repos
  • file:///X|/var/svn/repos

请注意,即使 Windows 上路径的本地(非 URL)形式使用反斜杠,URL 也使用正斜杠。还要注意,在命令行中使用 file:///X|/ 形式时,您需要对 URL 进行引用(将其用引号括起来),这样竖线字符就不会被解释为管道符。

[Note] 注意

您不能像使用典型的 file:// URL 那样在普通 Web 浏览器中使用 Subversion 的 file:// URL。当您尝试在普通 Web 浏览器中查看 file:// URL 时,它会通过直接检查文件系统来读取和显示该位置的文件内容。但是,Subversion 的资源存在于虚拟文件系统中(参见 “仓库层”部分),您的浏览器将无法理解如何与该文件系统交互。

Subversion 客户端会像 Web 浏览器一样自动对 URL 进行编码。例如,URL http://host/path with space/project/españa(包含空格和上 ASCII 字符)将被 Subversion 自动解释为 http://host/path%20with%20space/project/espa%C3%B1a。如果 URL 包含空格,请确保在命令行中将其用引号括起来,这样您的 shell 会将整个 URL 视为程序的单个参数。

Subversion 对 URL 的处理有一个明显的例外,它也适用于许多上下文中对本地路径的处理。如果 URL 或本地路径的最终路径组件包含一个“@”符号 (@),您需要使用一种特殊语法(在 “桩修订版本和操作修订版本”部分 中描述),以便使 Subversion 正确地访问该资源。

在 Subversion 1.6 中,引入了一个新的插入符号 (^) 符号,作为 仓库根目录的 URL 的简写形式。例如,您可以使用 ^/tags/bigsandwich/ 来引用仓库根目录中 /tags/bigsandwich 目录的 URL。请注意,这种 URL 语法仅在当前工作目录是工作副本时有效——命令行客户端通过查看工作副本的元数据来了解仓库的根 URL。还要注意,当您希望精确地引用仓库的根目录时,必须使用 ^/(带尾部斜杠字符),而不能仅仅使用 ^

Subversion 工作副本

Subversion 工作副本是您本地系统上的一个普通目录树,包含一组文件。您可以根据需要编辑这些文件,如果它们是源代码文件,您可以按照通常的方式从这些文件编译您的程序。您的工作副本是您自己的私有工作区:在您明确告知它这样做之前,Subversion 永远不会合并其他人的更改,也不会将您自己的更改提供给他人。您甚至可以拥有同一个项目的多个工作副本。

在您对工作副本中的文件进行了一些更改并验证了它们正常工作后,Subversion 为您提供了命令来 发布 您的更改,以便与您一起处理项目的人员(通过写入仓库)。如果其他人发布了自己的更改,Subversion 为您提供了命令将这些更改合并到您的工作副本中(通过从仓库中读取)。

工作副本还包含一些由 Subversion 创建和维护的额外文件,以帮助它执行这些命令。特别是,工作副本中的每个目录都包含一个名为 .svn 的子目录,也称为工作副本的 管理目录。每个管理目录中的文件帮助 Subversion 识别哪些文件包含未发布的更改,以及哪些文件与其他人的工作相比已过时。

[Tip] 提示

虽然 .svn 是 Subversion 管理目录的实际名称,但 Windows 用户可能会遇到 ASP.NET Web 应用程序框架不允许访问以点 (.) 开头的目录的问题。为了特殊考虑这种情况下的用户,如果 Subversion 在其运行环境中找到一个名为 SVN_ASP_DOT_NET_HACK 的变量,它将改为使用 _svn 作为管理目录名称。在本手册中,您遇到的任何对 .svn 的引用也适用于使用这种 ASP.NET 技巧 时的情况。

工作副本的工作原理

对于工作目录中的每个文件,Subversion 会记录(除其他信息外)两条基本信息:

  • 您的工作文件基于哪个修订版本(这称为文件的 工作修订版本

  • 记录本地副本上次由仓库更新的时间戳

有了这些信息,Subversion 通过与仓库对话,可以判断工作文件处于以下四种状态中的哪一种:

未改变且最新

该文件在工作目录中没有改变,并且自其工作修订版本以来,该文件在仓库中没有进行过任何更改。对该文件执行 svn commit 不会执行任何操作,对该文件执行 svn update 也不会执行任何操作。

本地已更改且最新

该文件已在工作目录中更改,并且自您上次更新以来,该文件在仓库中没有进行过任何更改。有一些本地更改尚未提交到仓库;因此,对该文件执行 svn commit 将成功发布您的更改,而对该文件执行 svn update 不会执行任何操作。

未更改,且已过期

该文件在工作目录中未更改,但在存储库中已更改。应及时更新文件,使其与最新的公开版本保持一致。对该文件执行 svn commit 命令将不会有任何作用,而执行 svn update 命令将把最新的更改合并到您的工作副本中。

本地已更改,且已过期

该文件在工作目录和存储库中均已更改。对该文件执行 svn commit 命令将失败并出现 已过期 错误。应先更新该文件;svn update 命令将尝试将公开更改与本地更改合并。如果 Subversion 无法以合理的方式自动完成合并,它将留待用户解决冲突。

基本工作副本交互

一个典型的 Subversion 存储库通常保存多个项目的(源代码)文件;通常情况下,每个项目都是存储库文件系统树中的一个子目录。在这种安排下,用户的工作副本通常对应于存储库的特定子树。

例如,假设您有一个包含两个软件项目 paintcalc 的存储库。每个项目都位于其自己的顶级子目录中,如 图 1.7,“存储库的文件系统” 所示。

图 1.7. 存储库的文件系统

The repository's filesystem

要获取工作副本,您必须 检出 存储库的某个子树。(术语 检出 听起来可能与锁定或预留资源有关,但实际上并非如此;它只是为您创建项目的副本。)例如,如果您检出 /calc,您将获得如下所示的工作副本

$ svn checkout http://svn.example.com/repos/calc
A    calc/Makefile
A    calc/integer.c
A    calc/button.c
Checked out revision 56.
$ ls -A calc
Makefile  button.c integer.c .svn/
$

左边空白处字母 A 的列表表示 Subversion 正在向您的工作副本添加一些项。您现在拥有存储库 /calc 目录的个人副本,其中有一个额外的条目 - .svn - 用于存储 Subversion 所需的额外信息,如前所述。

假设您对 button.c 做出了一些更改。由于 .svn 目录记住了文件的原始修改日期和内容,因此 Subversion 可以判断出您已更改该文件。但是,Subversion 不会将您的更改公开,除非您明确告知它这样做。发布您的更改的行为通常被称为 提交(或 检入)更改到存储库。

要将您的更改发布给其他人,您可以使用 Subversion 的 svn commit 命令

$ svn commit button.c -m "Fixed a typo in button.c."
Sending        button.c
Transmitting file data .
Committed revision 57.
$

现在,您对 button.c 的更改已提交到存储库,并带有描述您的更改的注释(即,您修复了一个拼写错误)。如果另一个用户检出 /calc 的工作副本,她将看到您在该文件的最新版本中的更改。

假设您有一个合作者 Sally,她在您检出 /calc 的工作副本时也检出了工作副本。当您将更改提交到 button.c 时,Sally 的工作副本将保持不变;Subversion 仅在用户要求时才会修改工作副本。

要更新她的项目,Sally 可以要求 Subversion 更新 她的工作副本,方法是使用 svn update 命令。这将把您的更改以及自她检出以来提交的任何其他更改都合并到她的工作副本中。

$ pwd
/home/sally/calc
$ ls -A
Makefile button.c integer.c .svn/
$ svn update
U    button.c
Updated to revision 57.
$

来自 svn update 命令的输出表明 Subversion 更新了 button.c 的内容。请注意,Sally 不需要指定要更新的文件;Subversion 使用 .svn 目录中的信息以及存储库中的其他信息来决定哪些文件需要更新。

混合修订版工作副本

作为一项 general 原则,Subversion 尽力做到尽可能灵活。一种特殊的灵活性是可以拥有包含具有不同工作修订版号的文件和目录的工作副本。Subversion 工作副本并不总是对应于存储库中的任何单个修订版;它们可能包含来自多个不同修订版的文件。例如,假设您检出来自存储库的工作副本,该存储库的最新修订版是 4


calc/
   Makefile:4
   integer.c:4
   button.c:4

目前,此工作目录完全对应于存储库中的修订版 4。但是,假设您对 button.c 做出了一些更改,并提交了该更改。假设没有其他提交发生,您的提交将创建存储库的修订版 5,而您的工作副本将如下所示


calc/
   Makefile:4
   integer.c:4
   button.c:5

假设此时,Sally 提交了对 integer.c 的更改,创建了修订版 6。如果您使用 svn update 更新您的工作副本,它将如下所示


calc/
   Makefile:6
   integer.c:6
   button.c:6

Sally 对 integer.c 的更改将显示在您的工作副本中,而您对 button.c 的更改仍然存在。在此示例中,Makefile 的文本在修订版 4、5 和 6 中是相同的,但 Subversion 将使用修订版 6 标记您的工作副本的 Makefile,以表明它仍然是最新的。因此,在您对工作副本的顶部进行干净更新之后,它通常会完全对应于存储库中的一个修订版。

更新和提交是独立的

Subversion 的基本规则之一是 推送 操作不会导致 拉取,反之亦然。仅仅因为您已准备好将新更改提交到存储库,并不意味着您已准备好接收来自其他人的更改。如果您还有正在进行的新更改,svn update 应该能够将存储库更改优雅地合并到您的更改中,而不是强迫您发布它们。

此规则的主要副作用是,它意味着工作副本必须执行额外的簿记工作来跟踪混合修订版,并能够容忍混合。由于目录本身也是版本化的,因此事情变得更加复杂。

例如,假设您的工作副本完全处于修订版 10。您编辑文件 foo.html,然后执行 svn commit,这将在存储库中创建修订版 15。提交成功后,许多新用户会期望工作副本完全处于修订版 15,但事实并非如此!在修订版 10 和 15 之间,存储库中可能发生了任何数量的更改。客户端对存储库中的这些更改一无所知,因为您还没有运行 svn update,而 svn commit 不会下载新更改。另一方面,如果 svn commit 要自动下载最新的更改,则可以将整个工作副本设置为修订版 15 - 但随后我们将违反 推送拉取 仍然是独立操作的基本规则。因此,Subversion 客户端可以做的唯一安全的事情是将一个文件 - foo.html - 标记为处于修订版 15。工作副本的其余部分保持在修订版 10。只有运行 svn update 才能下载最新的更改,并将整个工作副本标记为修订版 15。

混合修订版很正常

事实是,每次 您运行 svn commit 时,您的工作副本都会最终包含一些修订版的混合。您刚刚提交的内容将被标记为具有比其他所有内容更大的工作修订版。在进行多次提交(两次提交之间没有进行更新)后,您的工作副本将包含整个修订版的混合。即使您是唯一使用存储库的人,您仍然会看到这种现象。要检查您的工作修订版混合,请使用 svn status 命令,并使用 --verbose (-v) 选项(有关更多信息,请参阅 名为“查看更改概述”的部分)。

通常,新用户完全不知道他们的工作副本包含混合修订版。这可能会令人困惑,因为许多客户端命令对它们正在检查的项目的 working revision 很敏感。例如,svn log 命令用于显示文件或目录的更改历史记录(有关更多信息,请参阅 名为“生成历史更改列表”的部分)。当用户在工作副本对象上调用此命令时,他希望看到该对象的整个历史记录。但是,如果该对象的 working revision 很旧(通常是因为 svn update 很长时间没有运行),则会显示 版本对象的 history。

混合修订版很有用

如果您的项目足够复杂,您会发现有时需要强制 回溯(或更新到您已有的修订版之前的修订版)工作副本的某些部分;您将在 第 2 章,“基本用法” 中学习如何做到这一点。也许您想测试包含在子目录中的子模块的早期版本,或者也许您想弄清楚某个特定文件中的错误何时首次出现。这就是版本控制系统中的 时间机器 功能 - 允许您将工作副本的任何部分在历史记录中向前和向后移动的功能。

混合修订版存在限制

无论您在工作副本中如何使用混合修订版,这种灵活性都存在限制。

首先,您无法提交尚未完全更新的文件或目录的删除操作。如果存储库中存在该项的较新版本,则您的删除尝试将被拒绝,以防止您意外删除尚未看到的更改。

其次,您无法提交对尚未完全更新的目录的元数据更改。您将在 第 3 章,“高级主题” 中了解如何将 属性 附加到项。目录的 working revision 定义了一组特定的条目和属性,因此将属性更改提交到过时的目录可能会删除尚未看到的属性。