本文档内容正在不断完善,可能会发生很大变化,可能无法准确描述任何已发布的 Apache™ Subversion® 软件版本。将此页面添加为书签或将其推荐给其他人可能不是一个好主意。请访问 https://svnbook.subversion.org.cn/ 获取此书的稳定版本。
我们已经提到 Subversion 是一个现代的、网络感知的版本控制系统。正如我们在 名为“版本控制基础”的部分(我们对版本控制的高级概述)中描述的那样,存储库充当 Subversion 版本化数据的核心存储机制,并且用户及其软件程序通过工作副本与该数据进行交互。在本节中,我们将开始介绍 Subversion 实现版本控制的具体方式。
Subversion 实现版本控制存储库的概念,与任何其他现代版本控制系统一样。与工作副本不同,Subversion 存储库是一个抽象实体,几乎只能由 Subversion 自己的库和工具进行操作。由于大多数用户的 Subversion 交互都涉及使用 Subversion 客户端并在工作副本的上下文中进行,因此我们在本书的大部分内容中讨论了 Subversion 工作副本以及如何操作它。但是,有关存储库的更详细的信息,请查看 第 5 章,存储库管理。
警告 | |
---|---|
在 Subversion 中,系统每个用户都拥有的客户端对象 - 版本化文件的目录以及使系统能够跟踪它们并与服务器通信的元数据 - 被称为 工作副本。虽然其他版本控制系统使用术语 “存储库” 来指代客户端对象,但在 Subversion 的上下文中使用该术语来指代它是不正确的,并且是一个常见的混淆来源。 工作副本将在后面的 名为“Subversion 工作副本”的部分 中进行描述。 |
Subversion 客户端将任何数量的文件和目录作为单个原子事务提交(即,将所做的更改传递到)。原子事务的含义很简单:要么所有更改都被接受到存储库中,要么所有更改都不被接受。Subversion 试图在程序崩溃、系统崩溃、网络问题以及其他用户操作的情况下保留这种原子性。
每次存储库接受提交时,都会创建一个新的文件系统树状态,称为 修订版。每个修订版都分配一个唯一的自然数,比分配给前一个修订版的数字大 1。新创建存储库的初始修订版编号为 0,仅包含一个空根目录。
图 1.6,“树随时间变化” 说明了可视化存储库的一种不错的方式。想象一个从左到右延伸的修订版号数组,从 0 开始。每个修订版号下都挂着一个文件系统树,每个树都是存储库在提交后状态的 “快照”。
Subversion 客户端程序使用 URL 来标识 Subversion 存储库中的版本化文件和目录。在大多数情况下,这些 URL 使用标准语法,允许在 URL 中指定服务器名称和端口号。
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
的服务器名称,要么根本没有服务器名称
此外,Windows 平台上的 file://
方案用户将需要使用非正式的 “标准” 语法来访问位于同一台机器上但与客户端当前工作驱动器不同的驱动器上的存储库。以下两种 URL 路径语法中的任何一种都有效,其中 X
是存储库所在的驱动器
请注意,即使 Windows 上的本机(非 URL)路径形式使用反斜杠,URL 也使用正斜杠。还要注意,在命令行中使用 file:///
形式时,需要将 URL 括在引号中,以便 shell 将整个内容视为程序的单个参数。X
|/
注意 | |
---|---|
您不能像使用典型的 |
Subversion 客户端将自动对 URL 进行编码,就像 Web 浏览器一样。例如,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 或本地路径的最终路径组件包含一个 at 符号 (@
),您需要使用一种特殊语法(在 名为“挂钩修订版和操作修订版”的部分 中进行了描述)才能使 Subversion 正确地寻址该资源。
在 Subversion 1.6 中,引入了一种新的插入符号 (^
) 表示法,作为 “存储库根目录的 URL” 的简写。例如,您可以使用 ^/tags/bigsandwich/
来引用存储库根目录中 /tags/bigsandwich
目录的 URL。这样的 URL 被称为 存储库相对 URL。请注意,此 URL 语法仅在当前工作目录是工作副本时才有效 - 命令行客户端通过查看工作副本的元数据来了解存储库的根 URL。还要注意,当您希望精确地引用存储库的根目录时,您必须使用 ^/
(带有尾部斜杠字符),而不能仅仅使用 ^
。Windows 用户不要忘记,插入符号是他们平台上的转义字符。因此,如果您在 Windows 机器上运行 Subversion 客户端,请使用双插入符号 ^^
。
Subversion 工作副本是您本地系统上的一个普通目录树,其中包含文件集合。您可以随意编辑这些文件,如果它们是源代码文件,您就可以以通常的方式从这些文件编译程序。您的工作副本是您自己的私人工作区:在您明确告诉它这样做之前,Subversion 永远不会合并其他人的更改,也不会让其他人看到您自己的更改。您甚至可以拥有同一个项目的多个工作副本。
在您对工作副本中的文件进行了一些更改并验证它们正常工作后,Subversion 提供了命令供您 “发布” 您的更改(通过写入存储库),从而使其他与您一起在项目上工作的人员可以使用这些更改。如果其他人发布了他们自己的更改,Subversion 会提供命令供您将这些更改合并到您自己的工作副本中(通过从存储库读取)。请注意,中央存储库是 Subversion 中每个人更改的经纪人 - 更改不会以典型的流程从工作副本直接传递到工作副本。
工作副本还包含一些由 Subversion 创建和维护的额外文件,以帮助它执行这些命令。特别是,每个工作副本都包含一个名为 .svn
的子目录,也称为工作副本的 管理目录。管理目录中的文件帮助 Subversion 识别哪些版本化文件包含未发布的更改,以及哪些文件与其他人的工作相比已过时。
注意 | |
---|---|
在 1.7 版本之前,Subversion 在工作副本的每个版本控制目录中都维护着 |
对于工作目录中的每个文件,Subversion 会记录(除其他外)两条基本信息:
有了这些信息,Subversion 通过与存储库通信,可以判断工作文件处于以下四种状态中的哪一种:
文件在工作目录中未更改,并且自其工作版本以来,该文件在存储库中没有提交任何更改。对该文件执行svn commit将不会执行任何操作,对该文件执行svn update也不会执行任何操作。
文件在工作目录中已更改,并且自您上次更新以来,该文件在存储库中没有提交任何更改。存在尚未提交到存储库的本地更改;因此,对该文件执行svn commit将成功发布您的更改,对该文件执行svn update将不会执行任何操作。
文件在工作目录中未更改,但它在存储库中已更改。应最终更新该文件,以使其与最新的公共版本保持一致。对该文件执行svn commit将不会执行任何操作,对该文件执行svn update将把最新的更改合并到您的工作副本中。
文件在工作目录和存储库中都已更改。对该文件执行svn commit将失败,并显示“已过期”错误。应先更新该文件;svn update命令将尝试将公共更改与本地更改合并。如果 Subversion 无法以合理的方式自动完成合并,则会将其留给用户解决冲突。
典型的 Subversion 存储库通常包含多个项目的 files(或源代码);通常,每个项目都是存储库文件系统树中的一个子目录。在这种安排中,用户的工 作副本通常对应于存储库的特定子树。
例如,假设您有一个存储库,其中包含两个软件项目,paint
和 calc
。每个项目都位于其自己的顶级子目录中,如图 1.7, “存储库的文件系统”所示。
要获取工作副本,您必须检出存储库的某个子树。(术语检出听起来似乎与锁定或预订资源有关,但事实并非如此;它只是为您创建项目的 工作副本。)例如,如果您检出/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 的工作副本,而其他人一直提交他们的更改,因此存储库中的最新版本现在是版本 14。您编辑了文件 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 开始,默认情况下您不能使用混合版本的工作副本作为合并操作的目标。(引入此新要求是为了防止因这样做而导致的常见问题。)