This text is a work in progress—highly subject to change—and may not accurately describe any released version of the Apache™ Subversion® software. Bookmarking or otherwise referring others to this page is probably not such a smart idea. Please visit http://www.svnbook.com/ for stable versions of this book.

Subversion 的版本控制方法

我们已经提到 Subversion 是一个现代的, 支持网络的版本控制系统. 在 “版本控制基础”一节 说过 (从较高的层次 看待版本控制), 仓库是存放 Subversion 的版本控制数据的中央位置, 用户及 其软件通过工作副本与仓库交互. 本节介绍 Subversion 的版本控制实现方法.

Subversion 的仓库

Subversion 实现仓库的方式与其他版本控制系统非常类似. 与工作副本 不同, 一个 Subversion 仓库是一个抽象的实体, 可以被 Subversion 的库函数 和工具进行独占性地操作. 因为大多数用户是在工作副本中通过客户端工具 与 Subversion 交互, 所以本书主要讨论工作副本以及如何操作它, 关于 仓库的细节请参考 第 5 章 仓库管理.

[警告] 警告

在 Subversion 中, 每一个版本控制系统用户都有的客户端对象 —一个目录, 目录中除了存放被版本控制的文件外, 还有用于跟踪 文件和与服务器通信的元数据—叫做 工作副本 (working copy). 虽然有些版本控制系统使用 仓库 表示 存放在客户端的对象, 但这种说法并不正确, 而且在 Subversion 中会让 用户感到困惑.

“Subversion 的工作副本”一节 介绍工作副本.

版本号

Subversion 客户端将任意多的文件和目录的修改作为一个原子事务提交 给仓库. 原子事务的意思是要么所有的修改都被仓库接受, 要么一个也没有. Subversion 尽量保证即使是在程序崩溃, 操作系统崩溃, 网络断开和有其他 用户干扰的情况下, 也能维持住原子性.

仓库每接受一次提交都会为文件系统树创建一个新状态, 叫作一个 版本号 (revision). 每一个版本号都与一个独一无二的自然数相关联, 后一个版本号都比前一个 大一. 新创建的仓库的初始版本号是 0, 除了一个空的根目录外, 什么也没 有.

图 1.6 “文件系统树在时间上的变化” 以可视化的方式 展示了仓库的版本号在时间上的变化. 想像有一个由版本号号码组成的队列, 从 0 开始, 从左向右伸展. 每一个版本号下面都挂着一个文件系统树, 每一 个文件系统树都是本次提交后仓库的 快照 (snapshot).

图 1.6. 文件系统树在时间上的变化

文件系统树在时间上的变化

仓库寻址

Subversion 客户端工具使用 URL 识别仓库中的文件与目录. 在大部分情况下, 这些 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://localhost/var/svn/repos

如果工作副本和仓库不在同一个驱动器上, 那么 Windows 用户在使用 file:// 模式时需要用到一种非官方的 标准 语法. 下面是两个例子, 其中 X 表示仓库所在的驱动器盘符:

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

注意, 虽然 Windows 的路径使用反斜杠, 但是 URL 仍然需要使用 正斜杠. 另外还要注意的是在命令行上输入 file://X|/ 形式的字符串时, 你需要用双引号把它包裹起来, 这样的话竖线符就不会 被翻译成管道.

[注意] 注意

你不能在网页浏览器上输入 Subversion 的 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 或本地路径的最后一个分量含有符号 @, 为了让 Subversion 能够正确地对资源进行寻址, 你需要使用一种特殊的语法—具体内容将在 “限定版本号与实施版本号”一节 介绍.

Subversion 1.6 引入了一个新记号—脱字符 (^) —用来表示仓库根目录的 URL. 比如说用户可以用 ^/tags/bigsandwich/ 表示项目根目录中的 /tags/bigsandwich 的 URL, 这种 URL 称为 仓库的相对 URL (repository-relative URL). 这种语法只能在 工作副本中使用—客户端需要从工作副本的元数据中获取仓库根目录的 URL. 另外, 使用仓库的相对 URL 访问仓库的根目录时需要写成 ^/ (末尾要有一个斜杠), 而不是 ^. Windows 用户不要忘了在他们的操作系统中, 脱字符是一个转义字符, 因此为了表示一个脱字符, 需要写成 ^^.

Subversion 的工作副本

一个 Subversion 工作副本是用户本地系统中的一个普通目录, 用户可以按照 自己的要求对存放在目录中的文件进行编辑, 如果是源代码文件, 用户也可以 按照通常的方式对它们进行编译. 工作副本是用户的私有工作空间: 除非用户 明确地要求 Subversion, 否则它不会让工作副本合并其他人的修改, 也不会把 用户的修改暴露给其他人. 用户可以为同一个项目创建多个工作副本.

如果用户修改了工作副本中的文件, 并且确认了修改是正确的, 此时 可以使用 Subversion 提供的命令来 发布 修改 (通过 把修改保存到仓库中), 于是项目中的其他人就可以看到你的修改. 如果其他人也发布了他们的修改, Subversion 也提供了命令把他们的修改 合并到你的工作副本中 (通过读取仓库). 可以看到, 仓库是每个用户发布的 修改的中间人—修改并非从一个工作副本直接传递到另一个工作副本.

工作副本还会包含一些额外的文件, 这些文件由 Subversion 创建并维护, 用于命令的正常运行. 每一个工作副本中都有一个名为 .svn 的子目录, 它是工作副本的 管理目录 (administrative directory ). 管理目录中的文件可以帮助 Subversion 识别哪些文件含有 未发布的修改, 哪些文件是过时的.

[注意] 注意

在 1.7 版以前, Subversion 在工作副本的每一个子目录内都维护了 一个 .svn 目录. Subversion 1.7 在存放和 维护工作副本元数据上提出了一种全新的方法, 从外面看最显著的变化 是每个工作副本只创建了一个 .svn 目录, 存 放在工作副本的根目录下.

工作副本的工作原理

Subversion 为工作副本中的每一个文件记录两项信息:

  • 文件的版本号 (这被称为文件的 工作版本号 (working revision))

  • 一个时间戳, 记录了本地文件最近一次被仓库更新是在什么时候

有了这些信息后, 通过与仓库通信, Subversion 就可以判断出 工作副本中的每一个文件处于以下 4 种状态中的哪一种:

当前未修改的

文件在工作副本中未被修改, 并且在工作版本号之后还没有 人提交过该文件的修改. 对文件执行 svn commitsvn update 都不会产生任何效果.

当前已修改的

文件在工作副本中已被修改, 并且在一次更新以来还没有人 向仓库提交过该文件的修改. 本地有未提交的修改, 于是执行 svn commit 将会成功地把修改提交到仓库中, 而 svn update 不会产生任何效果.

过时未修改的

文件在工作副本中未被修改, 但是在上一次更新之后有人往 仓库提交了该文件的修改. 为了让文件和最新版本保持同步, 应 该执行更新操作. 对文件执行 svn commit 不会产生任何效果, 执行 svn update 将 把仓库中的最新修改合并到文件中.

过时且已修改的

文件在本地工作副本和仓库都被修改了. 对文件执行 svn commit 会由于文件已过时而失败. 首先应该更新文件, 命令 svn update 尝试 把仓库的修改合并到本地. 如果 Subversion 不能自动地以一种 合理的方式完成合并, 就会把冲突交由用户来解决.

工作副本的基本操作

一个典型的 Subversion 仓库经常存放着若干个项目的文件, 一般来说, 每一个项目都是仓库文件系统树的一个子目录. 在这种目录 布局下, 用户的一个工作副本就对应着仓库中一个特定的子目录.

举例来说, 假设你有一个包含了两个软件项目的仓库, 这两个项目是 paintcalc, 每一个项目 都有一个属于自己的目录, 如 图 1.7 “仓库的文件系统” 所示.

图 1.7. 仓库的文件系统

仓库的文件系统

为了得到一个工作副本, 你必须 检出 (checkout) 仓库的某些子树 (术语 检出 听起来好像会涉及到加锁和资源的预留, 但实际 上并没有, 它仅仅是为用户创建一份仓库的工作副本). 举例来说, 如果检出 /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 正 在往工作副本中添加项目 (item). 现在你就有了仓库 /calc 的一份私有副本, 外加一项额外的目录— .svn—目录里存放了 Subversion 需要的 额外信息, 我们在前面已经介绍过了.

假设用户修改了文件 button.c, 因为 .svn 记录了文件原来的修改日期和内容, 所以 Subversion 可以检测到用户修改了文件, 但是 Subversion 不会自动地 发布修改, 除非用户显式地告诉它要这么做. 发布修改 这个操作更常见的说法是向仓库 提交 (committing) 或 检入 (checking in) 修改.

用户为了发布修改, 需要使用 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 的工作副本, 他就会在最新版本 button.c 中看到用户新提交的修改.

假设你有一个同事 Sally, 在你修改 button.c 的同时, 他也检出了一个 /calc 的工作副本. 当 你把 button.c 的修改提交到仓库后, Sally 的 工作副本并不会自动地把修改同步到本地—只有在用户的显式要求下, Subversion 才会更新工作副本.

为了把工作副本更新到最新的状态, Sally 可以要求 Subversion 更新 (update) 他的工作 副本, 用到的命令是 svn update. 如果在 Sally 检 出工作副本之后, 有人向仓库提交了修改, 命令就会把这些修改都合并到他 的工作副本中.

$ 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 不需要 指定应该更新哪些文件, 根据 .svn 和仓库中的 信息, Subversion 可以自动判断出哪些文件需要更新.

版本号混合的工作副本

尽量保持灵活性是 Subversion 的总体原则, 其中一项灵活性是 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, Subversion 就会把工作副本更新到最新版, 变成:


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

Sally 对 integer.c 的修改出现在了你的工作 副本中, 你的修改依然保留在 button.c 中. 在这 个例子里, Makefile 在版本号 4, 5 和 6 中都 保持不变. 于是, 在工作副本的根目录执行了 svn update 后, 工作副本才精确地对应到了仓库的 同一个版本号.

更新和提交是分开的

Subversion 的一条基本规则是一次 推送 (push) 操作不会产生一次 抓取 (pull) 操作, 反之依然成立. 理由是用户准备好向仓库提交修改并不表示他已经准备好接收其他人提交 的修改, 另外, 如果用户的修改还未完全完成, 命令 svn update 应该把仓库的修改合并到本地, 但不 应该强迫用户提交本地未完成的修改.

这条规则主要的副作用是工作副本必须记录额外的信息来跟踪混合 的版本号, 同时还要能够处理版本号混合的情况. 目录也是版本库的一部 分, 这使得情况变得更加复杂.

举例来说, 假设你有一个版本号是 10 的工作副本, 检出后有人往 仓库提交了修改, 仓库最新的版本号是 14. 你修改了文件 foo.html, 然后向仓库提交了修改, 创建了新版本 号 15, 提交完成后, 许多 Subversion 新手可能会认为工作副本的版本号 会自动更新到 16, 但事实并非如此. 在版本号 10 和 15 之间, 仓库可 能发生了任意次数的修改, 但是客户端对此一无所知, 因为你并没有执行 svn update, 而 svn commit 并不会自动从仓库抓取更新. 如果让 svn commit 自动下载更新, 它就会把工作副本整体的版本号更新到 15—但是这 样做就违背了基本规则 推送和抓取是分开. 于是, 客户 端唯一能做的安全操作是只把文件 foo.html 的 版本号更新到 15, 工作副本中的其他文件与目录的版本号依然停留在 10. 只有在执行 svn update 后, 仓库的最新修改才会 被下载到本地, 并把工作副本的版本号更新到 15.

版本号混合是正常情况

事实上, 每次 执行 svn commit 都会产生新的版本号混合的情况, 刚被 提交的文件或目录的版本号是工作副本中的最大值. 再经历过几次提交 后 (提交之间没有执行更新操作), 工作副本的版本号就已经处于一种非常 混乱的情况, 即使在此期间只有一个人在往仓库提交修改, 这种情况也会 发生. 为了查看版本号的混乱情况, 带上选项 --verbose (-v) 执行 svn status (参考 “查看修改的整体概述”一节).

新用户常常没有意识到他们的工作副本包含了混合的版本号, 有时候 可能会让他们感到很困惑, 因为很多客户端命令对文件或目录的版本号 很敏感. 例如, 命令 svn log 会列出文件或目录 的修改历史 (见 “生成历史修改列表”一节), 当用户对 某个文件执行 svn log 时, 他很可能想看到文件的 全部修改历史, 但是如果文件在工作副本中的版本号太老了 (常常是因 为太久没有执行过 svn update), 那么较新的修改 历史就不会显示出来.

版本号混合是有益的

如果项目足够复杂, 你就会发现只把工作副本的某一部分 回退 (backdate) 到一个较旧的版本会很方便—我们将会在 第 2 章 基本用法 介绍如何完成这种操作. 也许是你想要测试存放在某个子目录中的子模块 早期版本, 又或许是你想要查出某个文件的问题是在什么时候第一次出现. 这是版本控制系统的 时间机器 特性, 该特性允许用户把 工作副本的任意一部分在时间上向前或向后移动.

版本号混合的限制

不过, 在使用工作副本的版本号混合特性时会有一些限制条件.

首先, 如果你删除了过时的文件或目录, 则不能提交删除. 因为如果 仓库中有更新的版本, 该限制就可以避免用户在没有看到新版本的情况下 做出错误的决定.

然后, 除非目录是最新的, 否则不能提交该目录的元数据修改 (我们 将在 第 3 章 高级主题 介绍如何为项目 (item) 添加 属性). 目录的工作版本号定义了一个条目和属性的特定集合, 提交过时 的目录的属性修改可能会销毁用户还没有看到的属性.

最后, 从 Subversion 1.7 开始, 含有版本号混合情况的工作副本 不能作为合并操作的目标 (引入这个限制的原因和前面两条类似).