这份文档是用来描述 Subversion 1.2 的。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的书籍。

Subversion 实战

现在是时候从抽象过渡到具体了。在本节中,我们将展示 Subversion 被实际使用的例子。

工作副本

您已经读过关于工作副本的内容;现在我们将演示 Subversion 客户端如何创建和使用它们。

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

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

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

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

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

图 2.6. 存储库的文件系统

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  integer.c  button.c  .svn/

字母 A 的列表表示 Subversion 将许多项目添加到您的工作副本中。您现在拥有存储库中 /calc 目录的个人副本,其中有一个额外的条目 - .svn - 它包含 Subversion 需要用到的额外信息,如前所述。

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

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

$ svn commit button.c
Sending        button.c
Transmitting file data .
Committed revision 57.

现在您对 button.c 的更改已经提交到存储库中;如果另一个用户检出一个 /calc 的工作副本,他们将看到您在文件最新版本中的更改。

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

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

$ pwd
/home/sally/calc

$ ls -A 
.svn/ Makefile integer.c button.c

$ svn update
U    button.c
Updated to revision 57.

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

版本

一个 svn commit 操作可以将对任意数量的文件和目录的更改作为单个原子事务发布。在您的工作副本中,您可以更改文件的內容、创建、删除、重命名和复制文件和目录,然后将完整的更改集作为一个单元提交。

在存储库中,每次提交都被视为一个原子事务:要么提交的所有更改都执行,要么都不执行。Subversion 试图在程序崩溃、系统崩溃、网络问题和其他用户的操作面前保留这种原子性。

每次存储库接受提交时,都会创建一个新的文件系统树状态,称为 版本。每个版本都被分配一个唯一的自然数,比前一个版本的数字大一。新创建的存储库的初始版本编号为零,并且仅包含一个空的根目录。

图 2.7, “存储库” 演示了一种可视化存储库的好方法。想象一个从 0 开始的版本号数组,从左到右延伸。每个版本号下都挂着一个文件系统树,每个树都是存储库在提交后状态的“快照”。

图 2.7. 存储库

The repository

需要重点说明的是,工作副本并不总是对应于存储库中的任何单个版本;它们可能包含来自多个不同版本的文档。例如,假设您从存储库中检出一个工作副本,该存储库的最新版本为 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 将标记您工作副本中的 Makefile 为版本 6,以指示它仍然是当前的。因此,在您对工作副本的顶层进行干净更新之后,它通常会完全对应于存储库中的一个版本。

工作副本如何跟踪存储库

对于工作目录中的每个文件,Subversion 在 .svn/ 管理区域中记录两条重要信息

  • 您的工作文件基于哪个版本(这被称为文件的 工作版本),以及

  • 一个时间戳,记录本地副本上次被存储库更新的时间。

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

未更改且当前

文件在工作目录中未更改,并且自其工作版本以来,没有对该文件的更改提交到存储库。对文件的 svn commit 不会执行任何操作,对文件的 svn update 也不会执行任何操作。

本地更改且当前

文件已在工作目录中更改,并且自其基版本以来,没有对该文件的更改提交到存储库。存在尚未提交到存储库的本地更改,因此对文件的 svn commit 将成功发布您的更改,而对文件的 svn update 不会执行任何操作。

未更改且过时

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

本地修改,并且过时

该文件在工作目录和仓库中都已更改。对该文件的 svn commit 操作将失败,并出现“过时”错误。应首先更新文件;svn update 命令将尝试将公共更改与本地更改合并。如果 Subversion 无法以合理的方式自动完成合并,则它会将冲突留给用户解决。

这听起来可能需要跟踪很多内容,但 svn status 命令将显示工作副本中任何项目的狀態。有关该命令的更多信息,请参阅 名为“svn status”的部分

混合修订版本工作副本

作为一般原则,Subversion 尝试尽可能灵活。一种特殊的灵活性是能够拥有包含具有不同工作修订版本的文件和目录的工作副本。不幸的是,这种灵活性往往会让许多新用户感到困惑。如果前面的混合修订版本示例让您感到困惑,这里有一篇关于该功能存在的原因以及如何使用它的入门教程。

更新和提交是分开的

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 命令(有关更多信息,请参阅 名为“svn status”的部分。)

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

混合修订版本很有用

如果您的项目足够复杂,您会发现有时将工作副本的部分内容强行“回溯”到更早的修订版本会很方便;您将在第 3 章中学习如何做到这一点。也许您想测试包含在子目录中的子模块的更早版本,或者也许您想找出特定文件中出现错误的时间。这就是版本控制系统的“时间机器”方面——允许您将工作副本的任何部分在历史记录中向前和向后移动的功能。

混合修订版本有一些限制

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

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

其次,您无法提交目录的元数据更改,除非它已完全更新。您将在第 6 章中学习如何将“属性”附加到项目。目录的工作修订版本定义了一组特定的条目和属性,因此将属性更改提交到过时的目录可能会破坏尚未查看的属性。