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.

使用分支

阅读到这里, 读者应该理解了每一次提交是如何创建一个新的文件系统树 状态 (称为 版本号), 如果还不理解, 读者应该回去阅读 “版本号”一节 的内容.

再次回顾 第 1 章 基本概念 的例子: 你和你的同事—Sally —共享一个包含了两个项目的仓库, 这两个项目是 paintcalc. 如 图 4.2 “仓库的起始布局” 所示, 每一个项目都包含了 子目录 trunkbranches. 读 者很快就会明白如此布局的原因.

图 4.2. 仓库的起始布局

仓库的起始布局

假设 Sally 和你都有一份 calc 项目的工作副本, 更确切 地说, 你们每个人都有一份 /calc/trunk 的工作副本. 项目的所有材料都放在子目录 trunk 内, 而不是直接放到 /calc 里, 因为开发团队把 /calc/trunk 作为开发 主线 (main line).

现在团队要求你为软件项目实现一个比较大的特性, 这项工作的时间会比较 长, 而且会影响到项目内的所有文件. 首先想到的第一个问题是你不想干扰 Sally —她目前正在解决软件的几个小问题. Sally 的工作依赖于这样一个事实, 那就是项目的最新版 (存放在 /calc/trunk) 总是可用的. 如果你开始一点一点地提交你的修改, 那肯定会影响到 Sally 的工作, 甚至包括 团队内的其他成员.

一种可能的办法是在你完成全部的修改之前, 不向仓库提交修改, 也不更新 工作副本, 这种情况会持续几周, 但是这会产生很多问题. 首先这不太安全, 如果 你的工作副本或机器遭到破坏, 之前所有的工作都会白费. 第二, 不够灵活, 除非 你手动地把你的修改复制到其他工作副本或机器中, 否则的话你就只能在一个固定 的工作副本上工作, 如果要把半成员品分享给其他人也很麻烦. 一种比较良好的 软件工程做法是允许团队中的其他成员审查你的修改, 如果别人不能看到你在中间 阶段的提交, 你将得不到别人的反馈, 甚至在错误的方向上努力多日, 直到别人 注意到你的工作. 最后, 当你完成所有的修改时, 你可能会发现很难把你的修改 合并到仓库里. Sally (或其他人) 可能在你工作的过程中向仓库提交了很多修改, 几周后, 当你最终执行 svn update 时, 这些修改很难合并 到你的工作副本里.

更好的做法是在仓库中创建一个属于你自己的分支 (或一条开发线), 这样 你就可以保存尚未完成的工作, 也不会干扰到其他人, 还可以与其他人分享你的 工作进度. 下面我们将会介绍具体的步骤.

创建分支

创建分支非常简单—就是用命令 svn copy 在 仓库中为项目目录树创建一个副本. 因为项目的源代码放在 /calc/trunk, 所以你要复制的就是这个目录. 那么新副本应该 放在哪里? 分支在仓库里的存放位置由项目自己来决定. 最后, 你的分支需要 一个名字, 用于和其他分支区分开. 分支的名字对 Subversion 而言并不重要 —你可以根据工作的特点为分支取一个你认为最好的名字.

假设团队规定分支存放在目录 branches 内 (这是 最常见的情况), 而 branches 是项目主干的兄弟目录 (在我们这个例子里, 存放分支的目录就是 /calc/branches ). 虽然没什么创意, 但你还是想把新的分支叫做 my-calc-branch, 这就意味着你将会创建一个新目录 /calc/branches/my-calc-branch, 新目录的生命周期 以 /calc/trunk 的副本作为开始.

读者应该已经见过如何在工作副本中用命令 svn copy 复制出一个新文件或目录, 除了工作副本, 它还可以完成 远程复制 (remote copy)—复制操作会 立刻提交到仓库中, 产生一个新的版本号, 完全不需要工作副本的参与. 从 命令的形式上看, 只是从一个 URL 中复制出新的一个:

$ svn copy ^/calc/trunk ^/calc/branches/my-calc-branch \
           -m "Creating a private branch of /calc/trunk."

Committed revision 341.
$

上面的命令立刻在仓库中产生了一次提交, 在版本号 341 创建了一个新 目录, 它是目录 /calc/trunk 的拷贝, 如图 图 4.3 “创建了分支后的仓库” 所示. [29] 当然, 使用 svn copy 复制工作副本里的目录来创建分支 也是可以的, 但我们不推荐这种做法, 因为可能会很慢. 在客户端复制目录是 一个线性时间复杂度的操作, 实际上它需要递归地复制目录内的每一个文件和 子目录, 这些文件和子目录都存放在本地磁盘上. 而远程复制是一个时间 复杂度为常量的操作, 大多数用户都是采用这种方式创建分支. 另外, 工作副 本中的目录可能含有混合的版本号, 虽然不会产生有害的影响, 但是在合并时 可能会产生不必要的麻烦. 如果用户选择通过复制工作副本中的目录来创建 分支, 在复制前应该确保被复制的目录不含有本地修改和混合的版本号.

图 4.3. 创建了分支后的仓库

创建了分支后的仓库

在分支上工作

创建完分支后, 用户就可以检出它的工作副本, 然后开始工作:

$ svn checkout http://svn.example.com/repos/calc/branches/my-calc-branch
A    my-calc-branch/doc
A    my-calc-branch/src
A    my-calc-branch/doc/INSTALL
A    my-calc-branch/src/real.c
A    my-calc-branch/src/main.c
A    my-calc-branch/src/button.c
A    my-calc-branch/src/integer.c
A    my-calc-branch/Makefile
A    my-calc-branch/README
Checked out revision 341.

$

和其他工作副本相比, 这个工作副本并没有什么特别的地方, 它只不过是映射 到了仓库的另一个目录. 而 Sally 在更新时将不会看到在这个工作副本里提 交的修改, 因为她的工作副本映射的是 /calc/trunk. (记得看一下本章后面的 “遍历分支”一节, 它是 创建分支工作副本的另一种办法)

假设分支创建后又过了一周, 期间提交了下面这些修改:

  • 在版本号 342 修改了文件 /calc/branches/my-calc-branch/src/button.c

  • 在版本号 343 修改了文件 /calc/branches/my-calc-branch/src/integer.c

  • Sally 在版本号 344 修改了文件 /calc/trunk/src/integer.c.

现在, 文件 integer.c 产生了两条独立的开发线, 如 图 4.4 “一个文件历史的分叉” 所示.

图 4.4. 一个文件历史的分叉

一个文件历史的分叉

当用户查看文件 integer.c 副本的修改历史时, 事 情开始变得有趣起来:

$ pwd
/home/user/my-calc-branch

$ svn log -v src/integer.c
------------------------------------------------------------------------
r343 | user | 2013-02-15 14:11:09 -0500 (Fri, 15 Feb 2013) | 1 line
Changed paths:
   M /calc/branches/my-calc-branch/src/integer.c

* integer.c:  frozzled the wazjub.
------------------------------------------------------------------------
r341 | user | 2013-02-15 07:41:25 -0500 (Fri, 15 Feb 2013) | 1 line
Changed paths:
   A /calc/branches/my-calc-branch (from /calc/trunk:340)

Creating a private branch of /calc/trunk.
------------------------------------------------------------------------
r154 | sally | 2013-01-30 04:20:03 -0500 (Wed, 30 Jan 2013) | 2 lines
Changed paths:
   M /calc/trunk/src/integer.c

* integer.c:  changed a docstring.
------------------------------------------------------------------------
…
------------------------------------------------------------------------
r113 | sally | 2013-01-26 15:50:21 -0500 (Sat, 26 Jan 2013) | 2 lines
Changed paths:
   M /calc/trunk/src/integer.c

* integer.c: Revise the fooplus API.
------------------------------------------------------------------------
r8 | sally | 2013-01-17 16:55:36 -0500 (Thu, 17 Jan 2013) | 1 line
Changed paths:
   A /calc/trunk/Makefile
   A /calc/trunk/README
   A /calc/trunk/doc/INSTALL
   A /calc/trunk/src/button.c
   A /calc/trunk/src/integer.c
   A /calc/trunk/src/main.c
   A /calc/trunk/src/real.c

Initial trunk code import for calc project.
------------------------------------------------------------------------

注意到 Subversion 在追溯分支 my-calc-branch 中的文件 integer.c 的历史时, 即使到达了创建分支 的时间点, 也仍然会继续往下追踪. 在历史中显示的是分支被创建的事件, 这 是因为当 /calc/trunk/ 中所有的文件都被复制时, 自然也就复制了 integer.c. 现在再看一下 Sally 在 她的副本上执行同样的命令会输出什么内容:

$ pwd
/home/sally/calc

$ svn log -v src/integer.c
------------------------------------------------------------------------
r344 | sally | 2013-02-15 16:44:44 -0500 (Fri, 15 Feb 2013) | 1 line
Changed paths:
   M /calc/trunk/src/integer.c

Refactor the bazzle functions.
------------------------------------------------------------------------
r154 | sally | 2013-01-30 04:20:03 -0500 (Wed, 30 Jan 2013) | 2 lines
Changed paths:
   M /calc/trunk/src/integer.c

* integer.c:  changed a docstring.
------------------------------------------------------------------------
…
------------------------------------------------------------------------
r113 | sally | 2013-01-26 15:50:21 -0500 (Sat, 26 Jan 2013) | 2 lines
Changed paths:
   M /calc/trunk/src/integer.c

* integer.c: Revise the fooplus API.
------------------------------------------------------------------------
r8 | sally | 2013-01-17 16:55:36 -0500 (Thu, 17 Jan 2013) | 1 line
Changed paths:
   A /calc/trunk/Makefile
   A /calc/trunk/README
   A /calc/trunk/doc/INSTALL
   A /calc/trunk/src/button.c
   A /calc/trunk/src/integer.c
   A /calc/trunk/src/main.c
   A /calc/trunk/src/real.c

Initial trunk code import for calc project.
------------------------------------------------------------------------

Sally 看到了她提交的版本号 344, 但没有看到版本号 343. 对 Subversion 而言, 这两个提交影响的是存放在仓库中不同位置上的不同文件, 而 Subversion 的输出 确实 表明了这两个文件共享一段 相同的历史—在创建分支 (版本号 341) 之前, 它们是同一个文件. 也就是因为这个原因, 所以你和 Sally 都能看到版本号 8 到版本号 154 的提交 历史.

分支背后的关键概念

在阅读完这一节后, 读者应该牢记以下两点. 第一, 在 Subversion 内部是 没有分支这个概念的—它只知道如何复制. 当用户复制一个目录时, 产生 的新目录被称为 分支 完全是用户赋予它的意义, 用户也可以 从其他角度看待它, 但是对于 Subversion 而言, 它只是一个含有额外历史信息 的普通目录.

第二, Subversion 的分支作为 普通的文件系统目录 存在于仓库中, 这和其他版本控制系统不太一样, 其他版本控制系统创建分支的 典型做法是为文件集添加处于额外维度的 标签. Subversion 不关心分支目录的存放位置, 但是大多数开发团队都遵循传统做法: 把所有的 分支都放在 branches/ 目录内, 当然, 用户也可以制订 自己的策略.



[29] Subversion 不支持在不同的仓库间复制, 当命令 svn copysvn move 的参数 带有 URL 时, 这些 URL 必须都在同一个仓库内.