layout | title |
---|---|
default |
Mercurial |
Mercurial是一个现代的分布式版本控制软件(VCS),主要用Python编写,少部分为提高性能采用C编写。在这一章,我将讨论与设计Mercurial算法和数据结构相关的一些选择。首先,让我简单介绍一下版本控制系统的历史,来引入必要的背景。
尽管本章主要讨论Mercurial的软件架构,很多概念和别的版本控制系统是相同。为了更好地讨论Mercurial,首先,我来举一些版本控制系统的概念和功能的名称。To put all of this in perspective, 我也将简单地介绍这个领域的历史。
版本控制系统设计用来帮助开发人员同时开发同一个软件系统。没有版本控制系统,开发人员只能相互交换完整的拷贝,并自行维护文件修改记录。我们用文件树来抽象软件的源代码。版本控制的一个主要功能就是分发文件树的变更。一个基本的周期就像这样:
- 从别人那里获取最新的文件树
- 对这个版本的文件树作一组变更
- 发布变更,这样别人就可以获取到了
第一个功能,得到本地文件树,叫做签出。存储我们获取、提交的变更的叫仓库,而签出的结果叫工作目录、工作树或工作拷贝。从仓库的最新文件更新本地拷贝就叫更新,有时候这需要合并,即合并不同用户对同一文件的修改。你可以用diff命令来检查一个文件树或者文件的两次修订之间的变更情况。最常用来检查你的工作拷贝里的本地(未发布的)变更。用提交命令可以发布变更,这会把工作目录中的变更保存到仓库。
第一个版本控制系统是Source Code Control System, SCCS。在1975年第一次有人论述它。它主要是提出了一种保存单个文件的更改的方法,这种方法比保存多个拷贝更加经济。它并不能帮你向别人发布这些变更。1982年出现了Revision Control System,RCS。这是一个更加成熟并且自由的SCCS替代品(GNU工程仍在维护它)。
在RCS之后是CVS,协作版本系统,在1986年第一次发布的时候,是一组脚本,用来按组操作RCS修订的文件。CVS的大革新是认为若干用户可以同时编辑,在编辑之后进行合并。这就需要能编辑冲突。开发人员只能提交基于仓库最新可用版本的文件。如果在仓库和我的工作目录同时有变更,我需要解决这些变更带来的冲突(编辑同时变更的行)。
CVS对分支和标签的看法也是很先进的。CVS的分支允许开发者同时在不同的文件上工作,CVS的标签可以给一个稳定的快照命名以方便引用。最初CVS只能通过在共享文件系统上的仓库交换更改,后来又实现了用于在大型网络(如互联网)交换的主从架构。
在2000年,三个开发者聚到一起开发一个新的版本控制系统,叫做Subversion,企图弥补CVS的一些主要缺陷。最重要的,Subversion每次操作都是针对整棵树的,这意味着每次修订的变更都必须具有原子性、一致性、独立性、持久性。Subversion工作拷贝同时也维护一个在工作目录签出的修订的干净版本。于是,一个常见的diff操作(把本地树和一个签出的变更集进行比较)是在本地进行的,因此会比较快。
Subversion有一个有意思的概念,标签和分支都是项目树的一部分。一个Subversion项目通常分成三个部分:标签,分支和主干。这样的设计非常符合不熟悉版本控制系统的用户的直觉,尽管这种设计固有的灵活性对转换工具造成了不计其数的困难,主要就是因为标签和分支在别的系统中具有更结构化的表示。
上述系统都被认为是集中式的,即便(从CVS开始)它们可以交换变更,它们还是依赖另一台计算机来维护仓库的历史。而分布式版本控制系统在每个有工作目录的计算机上都有仓库的全部或者部分历史的一份拷贝。
尽管相比CVS,Subversion有明显的进步,但是,依然有一些短处。首先,在所有集中式系统中,提交一个变更集和将它发布实际上是同一回事,因为仓库的历史集中在一处。这意味着不能访问网络就不能提交变更。其次,访问集中式系统的仓库总是在网络一次或者多次往返。这就使得它比只需要本地访问的分布式系统慢。第三,之前讨论的系统在追踪合并方面都不是很好(有的已经在这方面有所改善了)。对于大的协作组来说,版本控制系统能记录在一些新的修订中包含了那些变更,这样不会丢失且后续的合并可以利用这些信息。第四,传统版本控制系统需要集中,有时候这看上去更符合流程,更适合在同一处集成。分布式版本控制系统的拥护者认为,一个更加分布的系统更适合一个更加统一的组织,只要项目需要,开发人员可以向任意一点推送并在那里集成。
已经有一些新的工具被开发出来满足这些需求。在开源世界,2011年最著名的三个工具是Git、Mercurial和Bazaar。Git和Mercurial都是从2005年开始的。当时,Linux内核开发人员决定不再使用专有的BitKeeper系统。这两个项目都是由Linux内核开发人员发起的(分别是林纳斯·托瓦兹和Matt Mackall),都试图实现一个版本控制系统,这个系统能够满足处理对于数万个文件的数十万的变更集的需求(比如,内核)。Matt和林纳斯都受到Monotone的强烈影响。Bazaar是单独开发的,但在Canonical采用它来管理他们的项目后也得到了广泛的应用。
开发一个分布式版本控制系统显然有一些挑战,相当一部分是分布式系统固有的挑战。集中式系统的源代代码管理服务器都能提供统一的历史,这在分布式版本控制系统里并不存在。因为可以同时提交变更,在任何仓库都不可能对修订按时间排序。
几乎所有分布式系统都采用有向无环图来排列变更集,而不是线性排列。即,一个新提交的变更是它所基于的修订的子修订,一个修订不能基于它自己,也不能基于它的后续修订。采用这样的方案,我们有三种特殊的修订,根修订,它们没有父修订(一个仓库可以有多个根)、合并修订,它们有多个父修订、以及头修订,它们没有子修订。每个仓库都从一个空的根修订开始,处理一系列的变更,在一个或多个头修订结束。当两个用户分别提交以后,其中一方想从另一方那里拉取变更,他需要显式地合并另一方的变更,他随后提交的就是一个合并修订。
注意到有向无环图模型帮助解决了一些在集中式版本控制系统中难以解决的问题:合并修订用来记录有向无环图中新合并的分支的信息。产生的图形也可以用来表示很多平行的分支,合并成少数几个分支,最后合并到一个特殊的被认为是正式的分支。
这种方法要求系统维护变更集之间的继承关系,为便于交换变更集数据,这通常是通过在变更集中记录它们的父修订来完成的。这么做,变更集显然需要某种标识。有些系统使用UUID或者类似的方案,而Git和Mercurial都使用变更集内容的SHA1散列。这么做有一个额外的好处,变更集的ID可以用来校验变更集的内容。事实上,因为父修订都记录在散列数据里,任意修订的历史都可以通过散列来校验。作者的名字,提交的消息,时间戳和其他变更集元数据和变更集的文件内容合在一起进行散列,所以它们也一样可以校验。而且,在提交时记录了时间戳,对于任意仓库,它们也不是必须是线性增长的。
这些对那些之前只使用集中式版本控制系统的人来说很难适应:修订没有全局统一的编号或者名称,只有一个40个字符的16进制字符串。而且,没有全局统一的顺序,只有本地顺序,或者说全局同一的顺序是一个有向无环图而不是一条线。当你向一个已经有另一个子变更集的修订提交时,如果你习惯于版本控制系统发出警告,你会对意外地开始另外一个头感到困惑。
幸运的是,有工具可以帮你可视化树的顺序,Mercurial提供了变更集散列的一个不重复的缩写,以及只在本地可用的线性数值以方便区分。后者是一个单调递增的整数,用来提示变更集进入克隆的顺序。因为在不同克隆之间,这个顺序可以不同,所以非本地操作不能依赖这个整数。