前言这是我学习使用 CVS(Concurrent Versions System)和 Subversion 的过程当中,陆续整理的一些笔记,里面的内容大部分可以在参考文献中找到。整理这份文件的目的,主要是提供一份比较简短的观念说明,让想要学习使用版本控制系统的人,可以先懂一些必要的观念和术语,就立刻学习工具的使用,而不是 K 完一堆手册和 FAQ 之后才有办法使用工具。我不是说只要阅读这份文件就够了,而是当你有了基本的观念让你足以使用工具的一些基础功能之后,就应该去阅读比较完整的手册或书籍,以了解其它细节或进阶的用法。这时候因为已经有了基础,再去阅读其它文件时,就会觉得容易些了。 摘要1 简介在开发过程中,你是否碰到过以下几种情形:
如果有以上情形,你需要的是对项目进行版本控制(version control)。版本控制也有人称它为原始码控制(source code control),它的目的就在于解决上述的各种问题,让你可以:
版本控制系统则是提供上述功能的软件系统,它提供了一个地方让你集中存放开发过程中的所有程序档案及文件,以便达到集中控管的目的。 版本控制与软件建构管理SCM 这个主题很大,这里主要是点出版本控制与 SCM 的关系,若有兴趣进一步了解 SCM,请参考相关的书籍或到 Google 搜寻关键词 “software configuration management”。 2 版本控制系统2.1 档案库(Repository)前面提到,版本控制系统有一个集中存放档案的地方,这个地方有个正式名称,叫做「档案库(repository)」。档案库里面储存了项目档案的所有历史版本(包括目前开发中的版本),有的版本控制系统是以数据库的方式储存,有的是以档案的方式储存,不论储存的方式为何,对使用者来说,最重要的就是要把档案库放在一台稳定、安全的机器上,并且还要定期备份。 主从式架构现在我们知道,档案库既然是档案的集中营,那么一定是放在某台机器上,供所有开发人员存取,其作业方式如下图所示,是一种主从式(Client/Server)的架构: 档案库所在的机器上,必须要安装版本控制系统,以便提供档案存取的服务给各个客户端;而图中的「开发人员A」和「开发人员B」则代表了客户端,客户端机器上必须安装版本控制系统的客户端工具,才能存取档案库。 在联机方式上,客户端可以透过各种 络协议来存取档案库,某些版本控制系统要求你一定要随时与档案库保持联机,才能修改档案内容;某些版本控制系统(例如 Subversion)则采用比较宽松的方式,你可以在沙滩上用笔记型计算机修改程序,等到回办公室时再将档案同步。 2.2 哪些东西要放进档案库/h3>很显然的,程序代码当然要存放在档案库中,以便进行版本控制,那么,还有哪些东西也要版本控管/p> 基本上,你在开发一个项目的过程当中,需要用来建置软件的档案,都可能要放到档案库里面,例如:建置专案的组态档或 makefile、测试资料等等。在决定哪些档案要放进档案库时,你可以问自己一个问题:「如果少了这个档案,能够建置和发行软件吗 衍生的档案(Generated Artifacts)有些档案是建置过程中产生的一些附属产出文件,例如开发 Java 应用程序时,可以利用 JavaDoc 工具来产生原始码的说明文件。像这类从某个档案衍生出来的档案,该不该放到档案库里面/p> 简单的回答是「不要」。因为 :(1) 它不是建置软件的必备条件;(2) 它是重复的信息,而且容易造成档案的不一致,我们经常会修改了程序代码却没有立刻产生 JavaDoc 文件;(3) 当我们需要它的最新版本时,可以随时产生。 以上是针对 JavaDoc 这个例子来说明,但是项目开发过程中,还是有一些其它的衍生档案,并不像 JavaDoc 文件一样可以随时产生。例如,你可能有一些档案是所有开发人员都要共同存取的,或者需要花数个小时才能重建的,这些档案还是应该放进档案库里面。 非程序代码的档案(Non-code Artifacts)除了建置项目所需的档案之外,有些非程序代码的档案也应该纳入档案库,例如:项目管理的文件、团队成员的讨论信件、会议记录、FAQ….等等,任何对项目开发有贡献的信息都可以放到档案库里。 汇入(Import)这个动作指的是把一个完整的目录结构汇入档案库。项目一开始的时候,档案库里面并没有项目的档案和文件,因此当我们决定要把一个新的项目放进档案库进行版本控管时,第一个动作就是建立好一个项目的初始目录结构(其中可能包含一些必要的档案),然后执行汇入。
汇出(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 版本从何而来/h3>到目前为止,我们讨论的都是档案的取出和存入动作,那版本呢/p> 其实版本控制系统在我们每次执行 check in 时,就会把这次存入的档案视为一个新的版本,而每个版本都会记录在档案库里面。也就是说,当你从档案库取出一个档案,修改它,然后存入档案库,在档案库里面就会保留一份原始的版本,以及你修改后的版本(注1)。 每次修改的新版本都会被赋予一个修订版次(revision number),档案的修订版次在每次存入档案库时就会累加。跟版次 码一起储存的 信息可能还包括了档案的修改时间,以及由开发人员额外加注的说明。 某些版本控制系统会在你每次 check in 时,为所有的档案指定一个新的修订版次;某些工具则是针对个别档案的变更来记录版次,例如:
这表示你不能用修订版次来代表项目的发行版本,而应该用标记(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])。 分支就好像是另一个独立的档案库一样,运用分支的技巧,你就不用因为发行软件而将目前的的程序代码冻结起来。 关于分支的其它事项:
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 该怎么办/p> 严格锁定还有可能发生死结(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 讨论如何修改)。 也许你会觉得乐观锁定有可能需要自己手动解决冲突,嫌它太过麻烦,而宁愿采用严格锁定的方式。但实际上你并不需要太担心,因为在开发项目时,通常会事先划分好个人负责的模块或子系统,因此会发生多人同时修改一个档案的机会已经不多;即使有这种情况,两人刚好修改到同一行程序代码的机会更低。所以比较起来,乐观锁定还是比严格锁定方便许多。 最后,再将这两种机制的特性整理成下表,方便参考:
3 导入版本控制系统在了解版本控制系统的基本观念之后,就可以挑选一个版本控制系统,把它安装起来试试看了,如果是一人团队,应该没什么问题;如果是多人团队,要将版本控制系统导入现行的软件开发流程,可能就要多花点准备的功夫了。以下简单说明几点可能的工作项目:
在选择工具方面,这里无法提供什么有用的建议,因为我只接触过 Visual SourceSafe、CVS、和 Subversion,不过如果要从这三个工具中挑选,我会选 Subversion,因为:
另外,这里有一个各家版本控制系统的比较表,也可以参考看看: http://better-scm.berlios.de/comparison/comparison.html 4 总结注1:事实上,大部分的版本控制系统只储存两个版本之间有差异的部分,而不是储存完整的两份档案内容。 术语整理
参考文献
|
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!