本手册旨在描述 Apache™ Subversion® 的 1.7.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 一样,在普通网页浏览器中使用 Subversion 的 file:// URL。当您尝试在普通网页浏览器中查看 file:// URL 时,它会通过直接检查文件系统来读取并显示该位置文件的內容。但是,Subversion 的资源存在于虚拟文件系统中(参见 名为“仓库层”的部分),您的浏览器将无法理解如何与该文件系统交互。

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

Subversion 处理 URL 的方式有一个值得注意的例外,这也适用于它在许多情况下处理本地路径的方式。如果您的 URL 或本地路径的最后一个路径组件包含一个“@”符号 (@),您需要使用一种特殊语法(在 名为“Peg and Operative Revisions”的部分 中描述)才能让 Subversion 正确地访问该资源。

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

Subversion 工作副本

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

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

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

[Note] 注意

在 1.7 版本之前,Subversion 在工作副本的每个版本控制目录中维护.svn 管理子目录。Subversion 1.7 提供了一种全新的工作副本元数据存储和维护方式,其中最明显的改变是每个工作副本现在只有一个.svn 子目录,它是该工作副本根目录的直接子目录。

[Tip] 提示

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

工作副本的工作原理

对于工作目录中的每个文件,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
Updating '.':
U    button.c
Updated to revision 57.
$

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

混合版本的工作副本

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

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

混合版本很有用

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

混合版本有局限性

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

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

其次,您不能提交对目录的元数据更改,除非它已完全更新。您将在 第 3 章,高级主题 中了解如何将 属性 附加到项目。目录的当前版本定义了一组特定的条目和属性,因此将属性更改提交到过时的目录可能会破坏您尚未看到的属性。

最后,从 Subversion 1.7 开始,默认情况下您不能将混合版本的工作副本用作合并操作的目标。(引入此新要求是为了防止由此产生的常见问题。)

TortoiseSVN 官方中文版 1.14.7 发布