版本控制系统的基础观念
 

2009-03-13 作者:蔡焕麟 来源:dyndns.org

 

前言

这是我学习使用 CVS(Concurrent Versions System)和 Subversion 的过程当中,陆续整理的一些笔记,里面的内容大部分可以在参考文献中找到。整理这份文件的目的,主要是提供一份比较简短的观念说明,让想要学习使用版本控制系统的人,可以先懂一些必要的观念和术语,就立刻学习工具的使用,而不是 K 完一堆手册和 FAQ 之后才有办法使用工具。我不是说只要阅读这份文件就够了,而是当你有了基本的观念让你足以使用工具的一些基础功能之后,就应该去阅读比较完整的手册或书籍,以了解其它细节或进阶的用法。这时候因为已经有了基础,再去阅读其它文件时,就会觉得容易些了。

摘要

本文介绍版本控制系统的基础观念及术语,以及导入版本控制系统时应考虑的事项。

1 简介

在开发过程中,你是否碰到过以下几种情形:

  • 档案被别人(或自己)覆盖;
  • 档案遗失(拖放档案时误动作...);
  • 想要比对各版本之间的程序代码有何不同;
  • 想要回到之前修改的版本(需求反复变更、自己改错了...);
  • 这些 code 不是我改的,是谁碰过我的程序代码?
  • 软件发行之后,必须冻结共享的程序代码一段时间,免得其它人在改 bug 的同时,因为你修改了共享的程序而增加更多新的问题。

如果有以上情形,你需要的是对项目进行版本控制(version control)。版本控制也有人称它为原始码控制(source code control),它的目的就在于解决上述的各种问题,让你可以:

  • 随时复原错误,就好像是项目的时光回溯器,可以将档案恢复到以前的任何时候的版本;
  • 多人同时修改同一份程序代码,不会有相互覆盖的情况;
  • 保留所有修改的历程,如果你发现自己的程序代码有被别人更动过,可以很容易找到是谁更改的,以及何时更改的;
  • 在发行正式版的同时,还能继续发展新版本,无须下令冻结所有程序代码。

版本控制系统则是提供上述功能的软件系统,它提供了一个地方让你集中存放开发过程中的所有程序档案及文件,以便达到集中控管的目的。

版本控制与软件建构管理

软件建构管理(Software Configuration Management)简称 SCM 或 CM,是软件工程领域中的一环。SCM 的传统定义是原始码的版本管理,后来则逐渐演进扩大,将软件开发的一些标准和程序纳进来。你可以将 SCM 视为软件演进的过程中,用来管理改变的标准程序,这个改变的来源包括:程序代码的改变、支持多种作业平台、提供多种版本(例如:标准版、专业版)....等等,而版本控制就是用来实现 SCM 的主要工具。

SCM 这个主题很大,这里主要是点出版本控制与 SCM 的关系,若有兴趣进一步了解 SCM,请参考相关的书籍或到 Google 搜寻关键词 "software configuration management"。

2 版本控制系统

2.1 档案库(Repository)

前面提到,版本控制系统有一个集中存放档案的地方,这个地方有个正式名称,叫做「档案库(repository)」。档案库里面储存了项目档案的所有历史版本(包括目前开发中的版本),有的版本控制系统是以数据库的方式储存,有的是以档案的方式储存,不论储存的方式为何,对使用者来说,最重要的就是要把档案库放在一台稳定、安全的机器上,并且还要定期备份。

主从式架构

现在我们知道,档案库既然是档案的集中营,那么一定是放在某台机器上,供所有开发人员存取,其作业方式如下图所示,是一种主从式(Client/Server)的架构:

档案库所在的机器上,必须要安装版本控制系统,以便提供档案存取的服务给各个客户端;而图中的「开发人员A」和「开发人员B」则代表了客户端,客户端机器上必须安装版本控制系统的客户端工具,才能存取档案库。

在联机方式上,客户端可以透过各种网络协议来存取档案库,某些版本控制系统要求你一定要随时与档案库保持联机,才能修改档案内容;某些版本控制系统(例如 Subversion)则采用比较宽松的方式,你可以在沙滩上用笔记型计算机修改程序,等到回办公室时再将档案同步。

2.2 哪些东西要放进档案库?

很显然的,程序代码当然要存放在档案库中,以便进行版本控制,那么,还有哪些东西也要版本控管?

基本上,你在开发一个项目的过程当中,需要用来建置软件的档案,都可能要放到档案库里面,例如:建置专案的组态档或 makefile、测试资料等等。在决定哪些档案要放进档案库时,你可以问自己一个问题:「如果少了这个档案,能够建置和发行软件吗?」

衍生的档案(Generated Artifacts)

有些档案是建置过程中产生的一些附属产出文件,例如开发 Java 应用程序时,可以利用 JavaDoc 工具来产生原始码的说明文件。像这类从某个档案衍生出来的档案,该不该放到档案库里面?

简单的回答是「不要」。因为 :(1) 它不是建置软件的必备条件;(2) 它是重复的信息,而且容易造成档案的不一致,我们经常会修改了程序代码却没有立刻产生 JavaDoc 文件;(3) 当我们需要它的最新版本时,可以随时产生。

以上是针对 JavaDoc 这个例子来说明,但是项目开发过程中,还是有一些其它的衍生档案,并不像 JavaDoc 文件一样可以随时产生。例如,你可能有一些档案是所有开发人员都要共同存取的,或者需要花数个小时才能重建的,这些档案还是应该放进档案库里面。

非程序代码的档案(Non-code Artifacts)

除了建置项目所需的档案之外,有些非程序代码的档案也应该纳入档案库,例如:项目管理的文件、团队成员的讨论信件、会议记录、FAQ....等等,任何对项目开发有贡献的信息都可以放到档案库里。

汇入(Import)

这个动作指的是把一个完整的目录结构汇入档案库。项目一开始的时候,档案库里面并没有项目的档案和文件,因此当我们决定要把一个新的项目放进档案库进行版本控管时,第一个动作就是建立好一个项目的初始目录结构(其中可能包含一些必要的档案),然后执行汇入。

提示

要在 CVS 里面执行档案目录的搬移会有些麻烦,因此最好一开始多考虑一下要放入档案库的项目目录结构,免得造成日后的困扰。Subversion 虽然改善了 CVS 的这个缺点,连目录的变动都可以进行历史版本的记录,但是经常搬动目录仍然会有些副作用,汇入项目之前还是多想一下比较好。

汇出(Export)

汇出是把整个项目或模块从档案库中取出来,取出来的档案不包含版本控制系统的管理档案,也就是说,汇出的模块将不再由版本控制系统控管。

2.3 工作区(Workspace)与管理的档案

工作区(Workspace)

档案库存放了项目开发所需的所有档案及其历史版本,但是对于团队成员个人而言,并不需要全部的档案,我们只需要自己负责的部分就够了。因此,我们会从档案库中取出(复制)一部分自己需要的档案到自己本机的硬盘里,这些存放在本机的档案,就称为本地复本(local copy),而存放本地复本的地方,则称为工作区(workspace)。相对于本地复本,储存在档案库中的版本,则称为主拷贝(master copy)

对于小型项目而言,本地复本可能就是项目的所有原始码和文件,而大型项目可能会切割成数个子系统或模块,所以开发人员只要取出自己负责的子系统就行了。

工作区有时候也称为工作目录(working directory)或程序代码的工作复本(working copy)。

取出(Check Out)

一开始,我们个人的工作区都没有任何档案,因此第一个动作就是要从档案库中取出我们需要的工作复本,这个动作称为:取出(check out)。当你执行 check out 时,版本控制系统就会从档案库拷贝一份你需要的工作复本到你的工作目录,这个工作复本的所有档案目录结构都会跟档案库里面的目录结构一模一样。

存入(Commit or Check In)

当你取出工作复本之后,就可以修改档案内容,等到你觉得修改得差不多了,或者已经改完了,就可以把修改过的档案存入档案库,这个动作称为:存入(commit,或者 check in)

更新(Update)

当你修改自己的工作复本时,当然其它的团队成员也可能正在修改一些档案,每个人的修改作业都是独立进行且不会相互影响的,别人修改的结果也不会立即反映在你本机的工作复本上。如果你要看到别人修改的最新版本,你就必须执行更新(update)这个动作。当你的同僚执行 update 时,他们也会取得你最近 check in 的版本。

Check out 和 update 的行为有些相似,尽管他们的使用时机和目的不尽相同,有时候我们还是会交互使用这两个术语。

2.4 项目、模块、与档案(Projects, Modules, and Files)

大部分的版本控制系统允许你针对单一档案进行取出与存入的动作,但是大部分的项目会有数十个到数百个以上的档案,如果要对每一个档案执行取出与存入,就太麻烦了,因此版本控制系统提供了不同层级的操作,让我们能够以逻辑组成的档案群组来执行版本控制。

最上层的逻辑单位,就是项目(projects),在项目底下又可分成几个模块(modules)以及子模块(submodules),你得为这些模块命名,以便团队成员透过名称来存取它们。例如一个汽车保养场的信息系统,可能会分成进厂维护管理模块、库存零件管理模块、结帐模块...等等,各开发人员只需要取出自己负责的模块就行了。当然,如果你要的话,也可以把整个项目都取回自己的工作区。

你可以把项目看成是某个目录阶层的根目录,而模块和子模块则是底下的一堆子目录和档案。但是记住,模块只是档案的逻辑组成单位,相同的档案可以出现在不同的模块里面,而模块也可以跨项目共享,你只要将一些共享的档案归纳到一个共享的模块里面就行了。

2.5 版本从何而来?

到目前为止,我们讨论的都是档案的取出和存入动作,那版本呢?

其实版本控制系统在我们每次执行 check in 时,就会把这次存入的档案视为一个新的版本,而每个版本都会记录在档案库里面。也就是说,当你从档案库取出一个档案,修改它,然后存入档案库,在档案库里面就会保留一份原始的版本,以及你修改后的版本(注1)。 每次修改的新版本都会被赋予一个修订版次(revision number),档案的修订版次在每次存入档案库时就会累加。跟版次号码一起储存的 信息可能还包括了档案的修改时间,以及由开发人员额外加注的说明。

某些版本控制系统会在你每次 check in 时,为所有的档案指定一个新的修订版次;某些工具则是针对个别档案的变更来记录版次,例如:

file1.java 1.10
file2.java 1.7
file3.java 1.9

这表示你不能用修订版次来代表项目的发行版本,而应该用标记(tags)来作为项目的版本号码。

2.6 标记(Tags)

前面提过,修订版次(revision numbers)是用来表示个别档案的版本,它会由版本控制系统自动累加,不适合用来表示项目的发行版本。当我们要为项目订一个好记的版本名称时,例如:Pre-Relase2, 应该使用标记(tags)

标记不只可以作为项目的版本命名,你也可以为特定模块或某些档案订一个标记名称,例如前面举的例子:file1.java 1.10、file2.java 1.7、file3.java 1.9,你可以为这三个档案订一个标记名称,以后可以使用这个标记名称来一次取出这三个档案。

总之,标记代表了项目开发过程中,某一个时间点的状态,或者里程碑

2.7 分支(Branches)

在开发过程中,通常所有的程序设计师都是工作在同一个程序代码基础(code base)上,他们虽然各自负责撰写不同部分的程序代码,但主要都依循「从档案库取出,修改,然后存入」的工作模式,这些目前大家所修改的档案库中的程序代码,就称为项目的主线(mainline)。参考下图以了解主线的概念(取自 [1])。

然而有些情况不允许我们共同修改主线的程序代码,例如,当项目上线之后,Mary 负责修正使用者陆续反映的程序臭虫,这段期间可能要维持一两个月左右,可是这时候负责撰写程序主架构与共享组件的 John 发现了一些必须改进的地方,John 不能等 Mary 把所有臭虫都解决了才进行,他必须立刻着手改进现有的主架构和共享组件。如果 John 依照以往的方式,修改了主架构或共享组件之后,执行 check in 的动作,这样势必造成其它已经上线的程序产生新的问题,甚至无法运作,而必须全部都修改一遍,如此一来,Mary 的负担就更重了,她得一边处理臭虫,还要应付新的架构和组件所带来的问题。你或许会想,John 可以一直修改他自己的工作复本,但是都不要执行 check in 就行了,但是这并不符合一般人的工作习惯,我们通常修改程序到某个阶段时,就会执行 check in 以确保程序存在一个安全的地方,而且万一 John 哪天忘了,习惯性地执行 check in,那就糟了。

分支(branches)的用处就在这里,以上个例子而言,Mary 可以建立一个分支,以继续修改程序上线后的臭虫,而 John 则可以继续维护目前的产品主线。此时 John 和 Mary 都一样可以执行 check in 的动作,只是 Mary 取出和存入的都是档案库中的一个独立分支,跟 Mary 维护的主线是完全分离的。下图描绘了上述的作业方式(取自 [1])。

分支就好像是另一个独立的档案库一样,运用分支的技巧,你就不用因为发行软件而将目前的的程序代码冻结起来。

关于分支的其它事项:

  • 分支是以标记(tag)来识别。
  • 即使你的版本控制系统允许你建立分支的分支,但是最好不要这么做,以免横生枝节。
提示

对于初学者来说,标记和分支可能不会太快用到,可以先尝试把一个项目放进版本控制系统,运行顺利一阵子之后,再逐渐学习使用这些进阶的功能。

2.8 合并(Merging)

当你要 check in 某个档案时,如果档案已经先被其它人修改过并且 check in 了,版本控制系统便会侦测到,并且不允许你 check in 这个档案。此时便需要使用合并(merge)的技术,将两个人修改的内容进行合并,以确保彼此不会相互覆盖,又能保留各自修改的内容。由于两个人修改同一个档案时,通常不会碰巧都修改到相同的部分,因此版本控制系统会帮我们自动完成合并的动作 ;万一两个人修改的部分正好重迭,这种情况称为冲突(conflict),此时就必须由后来 check in 的那个人手动解决冲突的部分(可能会和另一个修改此档案的人讨论为什么会发生这种情况,以及应如何修改)。这部分的处理过程在稍后还会有进一步的讨论。

除了解决冲突的情况,合并还有另一个用途:用来合并分支和主线。例如当你为正式发行的版本建立一个分支以后,再这个分支里面修正了一些臭虫,而你发现这些臭虫也存在主线的程序里,此时就可以用合并的方式,让版本控制系统把你在分支里面做的修正套用到主线的程序代码。

2.9 锁定机制(Locking Options)

前面提到过两个人修改同一个档案所造成的冲突,是以合并的技术来解决,不过,各种版本控制系统采用的方式可能不尽相同,其相异之处,基本上只是对于档案锁定(locking)的处理方式不一样而已。锁定机制可分为两种:严格锁定(strict locking)乐观锁定(optimistic locking)

采用严格锁定的版本控制系统,采用的是事先避免冲突的态度,也就是当一个人 check out 某个档案时,该档案就会被锁定成只读状态,此时别人可以读取这个档案的内容,或者用它来建置项目,但无法修改,这样就不会造成冲突了 ,只是后来想要修改的人,必须等到取得锁定的人 check in 档案之后,才能修改。参考下面的图例(取自 [2]):

严格锁定可以避免冲突,但实际用起来却不大方便,因为一个档案同时间只有一个人能取得修改权,其它人得排队等候,像上面的例子,Sally 就要等 Harry 执行 check in 之后才能修改档案,万一 Harry 一直改不完,或者他忘了,然后渡假去了,Sally 该怎么办?

严格锁定还有可能发生死结(deadlock)的情况,例如 A 档案和 B 档案两个是相关的程序,Harry 先取出 A 档案,且 Sally 取出了 B 档案;之后 Harry 跟 Sally 又分别要取出 B 档案和 A 档案进行修改时,两个人都无法修改,因为档案都被对方锁住了。

乐观锁定就没有这些问题,因为乐观锁定根本就不锁定档案,任谁都可以同时取出同一个档案进行修改,当发生冲突时,再使用合并的方式解决。参考下面的图例(修改自 [1]):

图中显示 Fred 和 Wilma 都取出了 File1.java,而 Fred 先修改完并且 check in(commit),当 Wilma 也修改好,要执行 check in 时,版本控制系统会告诉她,她本机上的 File1.java 复本过时了(out-of-date),也就是说,档案库里的 File1.java 从她上次取出后已经被别人更动过了,因此她必须先更新本机的复本,然后再跟本机修改过的复本进行合并,于是最后 check in 的结果就包含了 Fred 和 Wilma 修改的内容。由于两个人修改的是同一个档案的不同部分,因此版本控制系统能够顺利完成合并的动作,如果两个人修改到相同的部分,Wilma 就得自己解决这个冲突了(可能会和 Fred 讨论如何修改)。

也许你会觉得乐观锁定有可能需要自己手动解决冲突,嫌它太过麻烦,而宁愿采用严格锁定的方式。但实际上你并不需要太担心,因为在开发项目时,通常会事先划分好个人负责的模块或子系统,因此会发生多人同时修改一个档案的机会已经不多;即使有这种情况,两人刚好修改到同一行程序代码的机会更低。所以比较起来,乐观锁定还是比严格锁定方便许多。

最后,再将这两种机制的特性整理成下表,方便参考:

  作业模式 优点 缺点
严格锁定 锁定-修改-解锁(lock-modify-unlock) 可避免冲突。 同一时间只有一个人可取得修改权,其它人必须排队等候,可能造成工作无法顺利进行,甚至造成相互锁住对方要修改的档案的情况。
乐观锁定 复制-修改-合并(copy-modify-merge) 所有人可修改任何档案。 当两个人修改同一个档案的相同部分时,需要手动解决冲突,但发生这种情况的机率很低。

3 导入版本控制系统

在了解版本控制系统的基本观念之后,就可以挑选一个版本控制系统,把它安装起来试试看了,如果是一人团队,应该没什么问题;如果是多人团队,要将版本控制系统导入现行的软件开发流程,可能就要多花点准备的功夫了。以下简单说明几点可能的工作项目:

  1. 选择合适的工具。目前市面上可以买到或免费取得的版本控制系统有很多,你可能要花一点时间比较一下各种产品的功能,并且根据自己的需求和预算,挑选最适合自己团队的工具。
  2. 安装并测试版本控制系统的各项功能。
  3. 选择一名管理员。团队中必须有一个人负责管理档案库、建立项目的初始目录结构、定期备份文件库等工作。
  4. 教育训练。教导开发人员如何在日常的开发工作中使用版本控制系统,让他们了解跟之前的作业方式有什么差别、可以获得哪些好处,以减少因为改变工作习惯而产生的阻力。
  5. 正式将项目纳入版本控管。

在选择工具方面,这里无法提供什么有用的建议,因为我只接触过 Visual SourceSafeCVS、和 Subversion,不过如果要从这三个工具中挑选,我会选 Subversion,因为:

  1. 喜新厌旧;
  2. Subveriosn 改进了 CVS 的缺点,连目录的变更也会记录版本(档案和目录的搬移更方便);
  3. 安装 Subversion 的过程顺利(安装在 Windows 2000 Server 上);
  4. 有很方便的客户端工具:TortoiseSVN,可减少导入 Subversion 的阻力;
  5. Subversion 的文件写得不错。(感谢 Plasma 提供繁体中文版的 Subverion 电子书

另外,这里有一个各家版本控制系统的比较表,也可以参考看看:

http://better-scm.berlios.de/comparison/comparison.html

4 总结

本文介绍了版本控制的一些基本观念和术语,也大概提了一下导入版本控制系统所需的准备工作,在具备了这些基础概念之后,便可以开始学习工具的使用,希望本文提供了足够的基础,作为您进一步学习使用版本控制系统的跳板。

注1:事实上,大部分的版本控制系统只储存两个版本之间有差异的部分,而不是储存完整的两份档案内容。

术语整理

英文 中文 说明
check out 取出 从档案库中取出档案。
commit/check in 存入 将档案从本地端存入档案库。
export 汇出 把整个模块从档案库中取出来,取出来的档案不包含版本控制系统的管理档案,也就是汇出的模块将不再由 版本控制系统控管。
import 汇入 把整个目录结构汇入档案库。当你要把一个新的项目放进档案库进行版本控管时,就需要执行这个动作。
local copy 本地复本 放在客户端机器的工作目录中的项目复本。
master copy 主拷贝 放在档案库里的项目复本。
module 模块 一个目录阶层,通常一个项目就是一个模块。
release 发行版本 软件产品的一个版本。为了区别产品的版本以及个别档案的修订版次,因此不使用 version,而用 release。
repository 档案库 存放所有档案(包含历史版本)的地方,客户端执行 check out 时就是从这里取出档案。
revision number 修订版次 一个档案的修改版本,例如:1.1、1.3.2.2。
tag 标记 在开发过程的某个时间点上,为一组档案提供的符号名称。透过 tag 一群档案,你可以很容易在某个 release 里面找出这些档案。
update 更新 从档案库中取得其它人修改的档案,以更新本机的副本(local copy)。
workspace/
working directory
工作区/工作目录 本机的工作目录,又称为沙盒(sandbox)。

参考文献

[1] Pragmatic Version Control with CVS. Dave Thomas and Andy Hunt. The Pragmatic Programmers, LLC. 2003.
[2] Version Control with Subversion Draft version 9837. Ben Collins-Sussman, Brian W. Fitzpatrick, and C. Michael Pilato. http://svnbook.red-bean.com/(繁体中文版:http://freebsd.sinica.edu.tw/~plasma/svnbook/
[3] CVS 入门。作者:卧龙小三。http://linux.tnc.edu.tw/techdoc/cvs/book1.html

 


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织