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 的算法通常只能处理文本文件, 在面对非文本文件 时, 现实就残酷多了. 如果用户无法找到支持非文本文件合并的工具, 复制-修改-合并 模型就不再适用.

介绍一个现实生活中可能会遇到的例子. Harry 和 Sally 是同一个项目的 图片设计师, 为汽车保险部门设计一款海报. 海报的中心是一辆汽车, 海报的格式 是 PNG. 海报的布局已经基本确定, Harry 和 Sally 将一辆 1967 年淡蓝色 Ford Mustang 照片放在海报中央, 车的左前侧保险杠有一点凹陷.

项目计划有所改动, 导致车身的颜色需要修改, 于是 Sally 把工作副本更 新到 HEAD, 打开图片编辑软件, 将车身的颜色改成樱桃红. 同时, Harry 觉得车的毁坏程度应该更严重一些, 这样效果更好, 于是他也把自己 的工作副本更新到 HEAD, 在车挡风玻璃上增加了一些裂痕. 就在 Harry 提交修改后, Sally 也提交了自己的修改, 显然, Subversion 会拒绝 Sally 的提交.

现在麻烦来了. 如果 Harry 和 Sally 编辑的是文本文件, 此时 Sally 只 要更新工作副本, 然后就可以再次尝试提交, 最差的情况不过是两人都修改了文件 的同一区域, 而 Sally 必须手工地解决冲突. 但海报不是文本文件, 它是二进制 的图片, 没有哪一款软件可以聪明到能够把两张图片合并成一张, 得到一辆 樱桃红的, 挡风玻璃上有裂痕的汽车.

如果 Harry 和 Sally 是串行地修改图片, 那事情就会顺利很多—比如 Sally 修改车身颜色并提交后, Harry 再去添加裂痕, 或者是 Sally 等到 Harry 添加裂 痕后再去修改车身颜色. “复制-修改-合并 解决方案”一节 已经说过, 如果 Harry 和 Sally 之间进行了充分的沟通, 这种问题大部分都可 以迎刃而解. 但是版本控制系统也是一种沟通的形式, 由软件来保证工作的串行 化并不是一件坏事, 反而效果更好, 效率更高. 正是基于这点考虑, Subversion 实现了 加锁-修改-解锁 模型. Subversion 的 锁定 特性和其他版本控制系统的 保留检出 比较类似.

Subversion 的锁定特性是为了最大程度地减少时间和精力的浪费. 允许用户 独占地修改仓库中的文件, 保证了用户在不支持合并的修改上所花费的精力不会 被浪费—他的修改总能提交成功. 并且, Subversion 把对象正在被锁定的 事实告诉给了其他用户, 其他用户就可以知道该对象正在被修改, 也就不会把时 间浪费在无法成功提交与合并的修改上.

当我们谈到 Subversion 的锁定特性时, 实际上说的是多种不同行为的集合, 包括锁定文件的的能力 [27] (获得独占修改文件的权利), 解锁一个文件 (放弃独占修改 文件的权利), 查看哪些文件被哪些用户锁定, 为锁定的文件添加注释 (强烈建议) 等, 所有的这些都会在本节进行详细介绍.

创建锁

在 Subversion 仓库里, 一个 (lock ) 就是一段元数据, 它赋予了一个用户独占修改文件的权利, 该 用户被认为是 锁的持有者 (lock owner). 仓库 负责管理锁, 具体来说就是锁的创建, 实施和删除. 如果有一个提交试图修改或 删除被锁定了的文件 (或删除文件的某个父目录), 仓库就会要求客户端提供 2 项信息—一是执行提交操作的客户端已被授权为锁的所有者, 二是提供了 锁令牌, 表明客户端知道它用的是哪一个锁.

为了演示锁的创建, 我们再以海报设计作为例子. Harry 决定修改一个 JPEG 图片, 为了防止其他用户在他完成修改之前向该文件提交修改, 他使用命令 svn lock 锁定了仓库中的文件:

$ svn lock banana.jpg -m "Editing file for tomorrow's release."
'banana.jpg' locked by user 'harry'.
$

上面的例子展示了一些新东西. 首先, Harry 向命令 svn lock 传递了选项 --message (-m), 和命令 svn commit 类似, svn lock 支持注释—借助选项 --message (-m) 或 --file (-F)—注释描述了锁定文件的原因. 然而, 和 svn commit 不同的是 svn lock 不 会通过启动文本编辑器来要求用户输入注释, 注释是可选的, 但是为了更好地 与其他用户沟通, 建议输入注释.

第二, 尝试加锁成功了, 这就是说文件之前未被锁定, 而且 Harry 工作副本 里的文件是最新的. 如果 Harry 工作副本里的文件是过时了的, 仓库将会拒绝 加锁请求, 要求 Harry 执行 svn update 并重新执行加 锁命令. 如果文件已经处于加锁状态, 加锁命令也会失败.

如果加锁成功, svn lock 会输出确认信息, 从现在开始, 文件已经被锁定的事实会体现在 svn statussvn info 的输出信息里.

$ svn status
     K  banana.jpg

$ svn info banana.jpg
Path: banana.jpg
Name: banana.jpg
Working Copy Root Path: /home/harry/project
URL: http://svn.example.com/repos/project/banana.jpg
Relative URL: ^/banana.jpg
Repository Root: http://svn.example.com/repos/project
Repository UUID: edb2f264-5ef2-0310-a47a-87b0ce17a8ec
Revision: 2198
Node Kind: file
Schedule: normal
Last Changed Author: frank
Last Changed Rev: 1950
Last Changed Date: 2006-03-15 12:43:04 -0600 (Wed, 15 Mar 2006)
Text Last Updated: 2006-06-08 19:23:07 -0500 (Thu, 08 Jun 2006)
Properties Last Updated: 2006-06-08 19:23:07 -0500 (Thu, 08 Jun 2006)
Checksum: 3b110d3b10638f5d1f4fe0f436a5a2a5
Lock Token: opaquelocktoken:0c0f600b-88f9-0310-9e48-355b44d4a58e
Lock Owner: harry
Lock Created: 2006-06-14 17:20:31 -0500 (Wed, 14 Jun 2006)
Lock Comment (1 line):
Editing file for tomorrow's release.

$

svn info 在执行时不会与仓库通信, 但是它仍然可 以显示锁令牌, 说明了一个很重要的信息: 它们被缓存在工作副本里. 锁令牌 的存在非常重要, 它向工作副本提供了使用锁的授权. 并且, svn status 在文件名的旁边显示一个 K (locKed 的缩写), 表示该文件存在锁令牌.

因为 Harry 已经锁定了文件 banana.jpg, 所以 Sally 不能提交和 banana.jpg 有关的修改:

$ svn delete banana.jpg
D         banana.jpg
$ svn commit -m "Delete useless file."
Deleting       banana.jpg
svn: E175002: Commit failed (details follow):
svn: E175002: Server sent unexpected return value (423 Locked) in response to 
DELETE request for '/repos/project/!svn/wrk/64bad3a9-96f9-0310-818a-df4224ddc
35d/banana.jpg'
$

修改完香蕉的黄色阴影后, Harry 可以向仓库提交修改, 这是因为他被授 权为锁的拥有者, 而且工作副本包含了正确的锁令牌:

$ svn status
M    K  banana.jpg
$ svn commit -m "Make banana more yellow"
Sending        banana.jpg
Transmitting file data .
Committed revision 2201.
$ svn status
$

注意提交完成后, svn status 显示锁令牌不再出现 在工作副本里, 这是 svn commit 的标准行为— 它搜索工作副本 (如果提供了目标列表, 则搜索该列表) 的本地修改, 并将所 有遇到的锁令牌作为提交事务的一部分发送给服务器, 如果提交成功, 仓库中 所有涉及到的锁都会被释放—即使是未被提交的文件上的锁 也会被释放. 这是为了防止粗心的用户持锁时间过长. 如果 Harry 随意地把目录 images 下的 30 个文件都锁定了 (因为他 不确定哪些文件需要修改), 而他只修改了其中 4 个文件, 当他执行完 svn commit images 后, 所有 30 个文件的锁都会被 释放.

svn commit 添加选项 --no-unlock 就不会在提交成功后自动释放锁, 适用选项的场景是用户需要多次 提交修改. 你可以通过运行时配置选项 no-unlock (见 “运行时配置区域”一节) 把不自动释放锁设置成默认行为.

当然, 锁定文件后并不要求一定要向该文件提交修改才能释放锁, 用户可以在 任何时候用命令 svn unlock 释放文件上的锁:

$ svn unlock banana.c
'banana.c' unlocked.

发现锁

如果由于其他用户锁定了文件而导致提交失败, 获取有关锁的信息非常方便, 最简单的方式是执行 svn status -u:

$ svn status -u
M               23   bar.c
M    O          32   raisin.jpg
        *       72   foo.h
Status against revision:     105
$

在这个例子里, Sally 不仅可以看到 foo.h 是过时了的, 而且他打算提交的两个文件中, 有一个在仓库中是被锁定了的. 字符 O 表示 其他 (Other), 意思是说 文件被其他用户锁定了, 如果 Sally 试图提交, raisin.jpg 上的锁会阻止提交成功. Sally 想知道是 谁, 在什么时候, 因为什么原因锁定了文件, svn info 可以 回答他的问题:

$ svn info ^/raisin.jpg
Path: raisin.jpg
Name: raisin.jpg
URL: http://svn.example.com/repos/project/raisin.jpg
Relative URL: ^/raisin.jpg
Repository Root: http://svn.example.com/repos/project
Repository UUID: edb2f264-5ef2-0310-a47a-87b0ce17a8ec
Revision: 105
Node Kind: file
Last Changed Author: sally
Last Changed Rev: 32
Last Changed Date: 2006-01-25 12:43:04 -0600 (Sun, 25 Jan 2006)
Lock Token: opaquelocktoken:fc2b4dee-98f9-0310-abf3-653ff3226e6b
Lock Owner: harry
Lock Created: 2006-02-16 13:29:18 -0500 (Thu, 16 Feb 2006)
Lock Comment (1 line):
Need to make a quick tweak to this image.
$

svn info 除了可以检查工作副本里的项目外, 也可以 检查仓库里的项目. 如果传递给 svn info 的参数是一个 工作副本路径, 那么缓存在工作副本里的所有信息都会显示出来; 只要显示的 信息中提到了锁, 那就说明工作副本持有一个锁令牌 (如果文件是被其他用户 或者是在另一个工作副本里锁定的, 那么在一个工作副本路径上执行 svn info 将不会显示关于锁的任何信息). 如果传递给 svn info 的是一个 URL, 输出的信息反映了仓库中的 项目的最新版, 信息中提到关于锁的任何信息都是在描述项目的当前加锁情况.

在我们的例子里, Sally 可以看到 Harry 在 2 月 16 日锁定了文件 raisin.jpg, 原因是 Need to make a quick tweak to this image. 现在已经 6 月了, Sally 怀疑 Harry 忘记给文件解锁, 她可能会打电话给 Harry, 向他抱怨, 让他马上释放锁. 如 果联系不到 Harry, 她可能会强行地破坏锁, 或者让管理员来帮她解决.

破坏与窃取锁

锁并非是不可侵犯的—在 Subversion 的默认配置状态下, 除了创建锁的 用户可以释放锁之外, 任意一个用户也可以释放锁. 如果释放锁的用户不是锁 的创建者, 我们把这种行为叫作 破坏锁 (breaking the lock).

对于管理员来说, 破坏锁非常简单. 命令 svnlooksvnadmin 可以直接从仓库中显示与移除锁 (关于 svnlooksvnadmin 的更多信息, 见 “管理员工具箱”一节).

$ svnadmin lslocks /var/svn/repos
Path: /project2/images/banana.jpg
UUID Token: opaquelocktoken:c32b4d88-e8fb-2310-abb3-153ff1236923
Owner: frank
Created: 2006-06-15 13:29:18 -0500 (Thu, 15 Jun 2006)
Expires: 
Comment (1 line):
Still improving the yellow color.

Path: /project/raisin.jpg
UUID Token: opaquelocktoken:fc2b4dee-98f9-0310-abf3-653ff3226e6b
Owner: harry
Created: 2006-02-16 13:29:18 -0500 (Thu, 16 Feb 2006)
Expires: 
Comment (1 line):
Need to make a quick tweak to this image.

$ svnadmin rmlocks /var/svn/repos /project/raisin.jpg
Removed lock on '/project/raisin.jpg'.
$

Subversion 还允许用户通过网络破坏其他用户的锁, 为了破坏 Harry 设置 在 raisin.jpg 上的锁, Sally 要给 svn unlock 加上选项 --force:

$ svn status -u
M               23   bar.c
M    O          32   raisin.jpg
        *       72   foo.h
Status against revision:     105
$ svn unlock raisin.jpg
svn: E195013: 'raisin.jpg' is not locked in this working copy
$ svn info raisin.jpg | grep ^URL
URL: http://svn.example.com/repos/project/raisin.jpg
$ svn unlock http://svn.example.com/repos/project/raisin.jpg
svn: warning: W160039: Unlock failed on 'raisin.jpg' (403 Forbidden)
$ svn unlock --force http://svn.example.com/repos/project/raisin.jpg
'raisin.jpg' unlocked.
$

在上面的例子里, Sally 第一次尝试解锁失败了, 因为她直接在工作副本 的 raisin.jpg 上执行 svn unlock, 而她的工作副本里并没有锁令牌. 为了直接从仓库中删除锁, 她需要向 svn unlock 传递一个 URL 参数. 增加 URL 参数后的第 一次尝试失败了, 因为她没有被授权为锁的所有者 (而且她也没有锁令牌). 但 是增加了选项 --force 后, 锁成功的被打开 (破坏) 了.

仅仅把锁破坏掉可能还不够. Sally 除了要打开 Harry 忘记打开的锁之外, 她 还想重新锁定文件, 以便自己对文件进行编辑. 她可以先用带上选项 --forcesvn unlock 把锁打开, 然后再 用 svn lock 锁定文件. 但是在两个命令之间可能会有 其他用户锁定了文件. 更简单的做法是 窃取 (steal) 锁, 它是把锁的破坏与重新加锁合并成一个 原子操作, 具体的做法是给 svn lock 加上选项 --force:

$ svn lock raisin.jpg
svn: warning: W160035: Path '/project/raisin.jpg' is already locked by user 'h
arry' in filesystem '/var/svn/repos/db'
$ svn lock --force raisin.jpg
'raisin.jpg' locked by user 'sally'.
$

无论锁是被破坏还是被窃取, Harry 都会感到惊讶. Harry 的工作副本仍然包 含原来的锁令牌, 但是锁却不存在了, 此时把锁令牌称为 失效的 (defunct), 锁令牌对应的锁要么被 破坏 (不在仓库里), 要么被窃取 (被另一把不同的锁替换掉). 不管是哪一种 情况, Harry 都可以用 svn status 查看详情:

$ svn status
     K  raisin.jpg
$ svn status -u
     B          32   raisin.jpg
Status against revision:     105
$ svn update
Updating '.':
  B  raisin.jpg
Updated to revision 105.
$ svn status
$

如果仓库的锁被破坏了, svn status --show-updates (-u) 会在文件的旁边显示字符 B (Broken). 如果有一把新锁出现在原来的位置上, 显示的就是字符 T (sTolen). 最后, svn update 会从工作副本中移除所有的失效锁.

锁通信

我们已经介绍了如何使用 svn lock svn unlock 完成锁的创建, 释放, 破坏与窃取. 锁实现了文件 的串行提交, 但是我们应该如何防止浪费时间?

比如说, Harry 锁定了一个图片文件, 然后开始编辑. 同时在几英里之外, Sally 也想编辑同一个文件, 她忘了执行 svn status -u, 所以她完全不知道 Harry 已经锁定了 她要编辑的文件. 她花了几个小时完成了图片的修改, 当她试图提交修改时, 发现文件被锁定或者工作副本里的文件过时了. 无论如何, 她的修改无法与 Harry 的修改合并, 两人中必须有一个人要放弃他的工作成果.

Subversion 的解决办法是提供了一种机制, 这种机制会提醒用户在开始 修改文件之前, 要先锁定文件, 这种机制是一个特殊的属性 svn:needs-lock. 如果文件设置了该属性 (属性值并不重要), Subversion 将试图使用文件系统的权限把文件设置成只读—除非用户显式 地锁定了文件. 如果提供了锁令牌 (svn lock 的运行结果), 文件的权限就变成可读写, 如果锁被释放了, 文件再次变成只读.

如果图片文件设置了属性 svn:needs-lock, 当 Sally 打开并开始修改图片时就会注意到有些地方不对劲: 很多程序在以读 写方式打开文件时, 如果发现文件是只读的, 将会向用户发出警告, 并阻止用户 向只读文件保存修改. 这将会提醒 Sally 应该在修改之前先锁定文件, 到那时 她就会发现锁已经预先被别人锁定了:

$ /usr/local/bin/gimp raisin.jpg
gimp: error: file is read-only!
$ ls -l raisin.jpg
-r--r--r--   1 sally   sally   215589 Jun  8 19:23 raisin.jpg
$ svn lock raisin.jpg
svn: warning: W160035: Path '/project/raisin.jpg' is already locked by user 'h
arry' in filesystem '/var/svn/repos/db'
$ svn info http://svn.example.com/repos/project/raisin.jpg | grep Lock
Lock Token: opaquelocktoken:fc2b4dee-98f9-0310-abf3-653ff3226e6b
Lock Owner: harry
Lock Created: 2006-06-08 07:29:18 -0500 (Thu, 08 June 2006)
Lock Comment (1 line):
Making some tweaks.  Locking for the next two hours.
$
[提示] 提示

建议用户和管理员把属性 svn:needs-lock 设置到 所有不支持合并的文件上, 这是养成良好的加锁习惯和避免浪费时间的主要 方法.

注意 svn:needs-lock 是一个通信工具, 与加锁系统 独立工作. 换句话说, 无论是否设置了这个属性, 文件都可以被锁定, 相反, 设置了这个属性, 仓库也不会要求在提交时必须提供锁.

不幸的是, 这种机制并非毫无缺点. 即使文件设置了属性 svn:needs-lock, 只读提醒也可能不会起作用. 有时候应用 程序不够规范, 会 劫持 只读文件, 然后悄无声息地允许用户修改并 保存文件. Subversion 对这种情况无能为力—目前为止还没有什么方法可以 完全替代人与人之间的交流[28]



[27] Subversion 目前不支持锁定目录.

[28] 除非我们可以像瓦肯人那样做到心灵 融合.