目录
版本控制是管理一些信息的多个版本的过程。它最简单的形式莫过于:每次你修改一个文件后都重新命名保存,文件名中包含一个数字,每次修改后这个数字都增加。这通常是许多人手工完成的。
然而,手工管理即使是一个文件的多个版本也是很容易出错的,所以很早就有软件工具来使这个过程自动化。最早的自动化软件管理工具的目标是帮助一个用户管理一个文件的版本。在过去的几十年里,版本控制工具的范围得到极大扩展;现在它们可以管理多个文件,帮助许多人一起工作。最新的版本控制工具可以支持上包含成数十万个文件,几千个人一起工作的项目。
分布式版本控制是最近才出现的,但是由于人们愿意探索新的疆域,目前这一新的领域已经得到了长足的发展
我写这本关于分布式版本控制的书是因为我相信这个领域应该有一个指南。我选择Mercurial是因为它是在这个领域中学习最容易上手的工具,并且它能够满足真实的,挑战性的环境的要求,而其他版本控制工具只能望而兴叹。
为什么你或者你的团队可能需要在项目中使用自动化版本控制工具呢?有很多理由。
因为它能记录你的项目的历史和演化,所以你不必再给每个变更都记录日志,谁做的;为什么他们这样做;什么时候做的;做了什么修改。
当你和其他人一起工作的时候,版本控制工具让你合作的更容易。例如,当许多人或多或少的同时进行有可能冲突的修改的时候,软件可以帮助你确定和解决这些冲突。
它能帮助你修正错误。如果你做了一个修改,但是后来发现这是个错误,你能恢复到一个或者多个文件以前的版本。实际上,一个相当好的版本控制工具甚至会帮助你找出什么什么是时候引入的这个问题(详细信息参考第 9.5 节 “查找问题的根源”)。
大多数这些理由都是等效的—至少在理论上—不管你是一个人做项目还是和几百个人一起。
版本控制在在不同尺度上(“单个黑客”和“一个大项目组”)实践的一个关键问题是它的性价比怎么样。一个很难理解和使用的版本控制工具会让你的代价很高。
如果没有版本控制工具和过程,一个五百人的项目很快就可能就将自己压垮。在这种情况下,版本控制的代价基本上不用考虑,因为没有它,失败几乎是一定的。
另一方面,一个人的“快速编程”看起来并不适合使用版本控制工具,因为使用一个版本控制工具的代价就是整个项目的代价,对吧?
Mercurial的独特之处就是它同时支持这两种尺度的开发。你可以在几分钟之内学会基本的使用,因为代价很小,你可以很方便的在最小的项目上应用版本管理。它的简洁性意味着不会有很多艰深的概念和命令干扰你真正的工作。同时,Mercurial高性能和点对点的特性可以让你轻易的应对大的项目。
本书采用了一种非同寻常的示例编程方法。每个例子都是“活的”—每一个示例都是一个shell脚本执行的实际结果,这个脚本执行了你看到的Mercurial命令。每次从源代码构建本书的映像时,所有的示例脚本都自动执行,然后将预期结果和实际执行结果比较。
这种方法的优点是每个例子都是永远精确的,它们是本书前面描述的Mercurial版本的实际运行结果。如果我更新了描述的Mercurial的版本,同时有些命令的输出变化了,创建就会失败。
这种方法也有小的缺点,就是你在示例中看见的日期和时间被“压缩”了,看起来这些命令不是人输入的。人不可能再一秒之内执行一条以上的命令,相应的时间戳都应该展开,我的自动化脚本中一秒之内可以执行很多命令。
举例来说,示例中的几个连续的提交可能显示发生于同一秒之内。你可以在bisect
示例中看到例子第 9.5 节 “查找问题的根源”。
在过去的四十年中,随着人们越来越熟悉他们的工具的能力和限制,开发和使用版本控制工具出现了明确的趋势。
第一代软件开始于在个人计算机,人们用这些软件管理单个文件。虽然这些工具比手工管理版本有了巨大的飞跃。但是加锁模型和依赖于单个计算机限制了他们使用范围,只用于小的,组织严密的团队。
第二代软件放松了这些限制,因为它们采用了以网络为中心的结构,并且能够一次管理整个项目。随着项目增长,又出现了新的问题。客户需要频繁的和服务器交互,服务器的可伸缩性成为大项目的主要问题。不可靠的网络会妨碍客户和服务器的交互。随着开源项目开始开放只读权限给匿名用户,没有提交权限的用户发现他们不能以自然的方式使用工具和项目交互,因为他们不能记录他们的修改。
新一代的版本控制工具本质上是点对点的。所有的这些系统都抛弃了对单个中央服务器的依赖,允许用户将他们的版本控制数据发布到任何需要的地方。通过互联网的协作摆脱了技术的限制,走向选择和审查。现代的工具可以进行自治的,不受限制的离线操作。只需要在有网络的时候和其他的版本库同步即可。
与它们的上一代竞争者相比,虽然分布式版本控制工具多年来已经很稳定和实用了,但是旧的工具的使用者还没有完全了解它们的优点,分布式的工具在很多方面明显由于集中式的工具。
对于个人开发者,分布式工具几乎永远比集中式工具快的多。原因很简单:集中式工具的很多操作需要网络交互,因为大部分元数据都只在中央服务器上有一份拷贝。而一个分布式工具将所有的元数据保存在本地。其他的相同,通过网络的交互增加了集中式工具的负担。不要低估了反应迅速的工具的价值:你要花很多时间和你的版本控制软件交互。
分布式工具对你的服务器结构并不感冒,因为他们在将元数据复制到很多地方。如果你的集中式系统和你的服务器着火了,你最好希望你的备份介质是可靠的,同时你最后的备份是最近的,而且还能用。而对于分布式工具,你在贡献者的计算机上有很多备份。
网络的可靠性对分布式工具的影响要远远小于集中式工具。如果没有网络你根本不能使用集中式工具,除了少数几个功能有限命令。而对于分布式工具,即使在你工作的时候网络瘫痪了,你可能根本不会注意到。你不能做的事情仅仅是不能和其它计算机上的版本库交互了,这种情况在本地操作相当罕见。而如果你的团队有异地的人员,那这就很有可能了。
如果你喜欢一个开源项目并且决定准备开始改进它,同时这个项目使用分布式版本控制工具的话,你立刻可以和其他人一样认为自己成为项目的“核心”。如果他们发布了他们的版本库,你可以立即拷贝他们的项目。开始修改,记录你自己的工作,和内部人员一样使用同样的工具。相比之下,如果使用集中式工具,除非有人给你向中央服务器提交修改的权限,你只能用只读的方式使用软件。这样,你就不能记录你的修改,而且当你从版本库更新的时候,本地的修改随时有崩溃的可能。
有人说分布式版本控制工具给开源项目带来了某种风险,因为在项目的发展过程中很容易出现“分支”。当不同组的开发人员有不同的意见和看法,决定不再一起工作的时候,就会产生新的分支,每个组的人都或多或少的获得项目代码的完整拷贝,然后向自己的方向发展。
有时候,不同分支的阵营决定协调他们之间的分歧。如果使用了集中式版本控制系统 技术上的问题使协调过程非常痛苦,大部分需要手动解决。你必须决定那个版本历史“赢”了,然后将其它团队的更改合并到代码中。这通常造成别的团队某些或者全部版本历史丢失。
分布式工具认为分支是项目开发的唯一方式。每个单独的变更都是潜在的分支点。这种方法的巨大力量在于,分布式版本控制工具非常善于合并分支,因为分支是非常基本的操作:它们随时都在发生。
如果所有的人在任何时候的做任何工作都这样使用合并和分支,那么对开源世界来讲,“分支”就变成了纯粹的社会事件。如果真的有什么影响到话,就是分布式工具降低了分支的可能性:
有些人拒绝分布式工具因为他们想保持对项目的控制,他们相信集中式工具会给他们这种控制。如果你这种想法,并你将CVS或者Subversion版本库向大众发布了,那么别人可用现有的很多工具将整个项目的历史抓取出来(可能很慢),在其它你控制不了的地方重建版本库。这时你的控制只不过是一种幻觉罢了,一些人被迫从你的版本库创建映像和分支,而你失去了和他们协作的机会。
遍布全球的团队正在进行许多商业项目。远离中央服务器的贡献者会发现执行命令速度很慢同时不怎么可靠。商业的版本控制系统改善这个问题的办法就是让你购买远程复制插件,这通常很昂贵,并且很难管理。分布式系统首先不会有这样的问题。其次,你可以很容易的建立多个授权服务器,假设每个站点一个,这样可以避免在昂贵的长途线路上的冗余的通讯。
集中式的版本控制系统的扩展性相对较低。只要不多的并行用户的组合负载就可以将一个昂贵的中央服务器压垮。同样,典型的反应就是昂贵笨重的复制设备。因为中央服务器的最大负载—如果你有的话—比分布式工具低很多(因为所有的数据要北复制到其它地方),一个廉价的服务器就可以满足一个相当大的团队的要求,为平衡负载而进行的复制只需要简单的脚本就够了。
如果你有员工需要在客户方解决问题,那么他们会受益于分布式版本控制。工具允许他们创建定制的环境,互相独立的尝试不同的解决方案。并且可以高效的从历史代码中查找bug的根源,在客户环境中进行回归,所有的这些都不需要连接公司的网络。
Mercurial是一个非常好的版本控制系统,因为它有很多独一无二的特点。
如果你熟悉版本控制系统,你在五分钟之内只能就可以使用Mercurial工作了。即使你不熟悉,也不过是再多花几分钟。Mercurial的命令和功能集非常统一一致,你只要遵守几个通用的规则就够了,很少有例外。
在很小的项目上,你马上就可以使用Mercurial开始工作。创建新的修改和分支;到处交换修改(不管本机还是通过网络);获取历史和状态数据非常快。Mercurial努力保持小巧灵活,在眨眼之间就能完成操作。
不仅小的项目可以使用Mercurial:有成百上千的贡献者的项目也在使用它,这些项目每个都有上万个文件和几百兆的源码。
如果觉得Mercurial的核心功能不能满足要求,你很容易在现有基础上开发。Mercurial非常适合于脚本任务,它的核心简洁,并且用Python实现,用扩展的方式增加新的功能非常方便,现在已经有很多流行和有用的扩展了,像帮助你确定bug或者提高性能等。
在你继续阅读之前,请理解本节完全反映了我的个人经验,兴趣,和偏好(我敢说)。下面列的每个版本控制工具我都使用过,大多数情况下都用过好几年。
Subversion是一个流行的版本控制工具,是用来替代CVS的,它采用的是集中式的客户/服务器结构。
Subversion和Mercurial相同操作的命令的命名上非常相似,所以如果你熟悉一个,很容易学会另外一个,两个工具都可以在大多数平台上运行。
在1.5版之前,Subversion对合并的支持并不好。在我写本书的时候,它刚新增了并跟踪的功能,出名的复杂和容易出错。
在我测量过的每个版本控制操作上Mercurial都比Subversion有很大的性能优势。差距从两个数量级到六个数量级不等,我用的是Subversion 1.4.3的ra_local文件存储方式,这是已知的最快的存取方式了。在实际的部署中包括网络存储,那么Subversion的劣势更大。因为很多Subversion命令必须和主机交互,并且因为Subversion没有好的复制机制,使得对于中的等大小的项目而言,服务器容量和网络带宽成为主要瓶颈。
另外,Subversion以在客户端使用了更多存储空间的方法,换取降低几个常用操作的网络负载,例如查找修改过的文件(status
)和显示对于当前版本的更改(diff
)。结果Subversion的工作副本经常和Mercurial的版本库和工作目录一样大,或者更大,虽然Mercurial的版本库包括了项目的完整历史。
Subversion有很多的第三方工具支持。Mercurial现在在这方面稍微欠缺。然而差距正在逐渐缩小,实际上一些有Mercurial的GUI工具比Subversion类似工具还略胜一筹。和Mercurial一样,Subversion也有完善的用户手册。
Subversion并不在客户端存储版本历史,所以它很适合管理那些有很多大的二进制文件的项目。如果你对一个未压缩的10MB文件检入了五十次,Subversion客户端的占用的磁盘空间基本上保持不变。而对于分布式SCM软件,磁盘空间会随着版本数量的增加成而迅速增长,因为每个版本之间的差异非常大。
另外,合并不同版本的二进制文件非常困难,换句话说,基本上不可能。Subversion提供了锁定功能,用户可以锁定一个文件,这样他就取得了对这个文件临时的独占的提交权,这对于广泛使用二进制文件的项目而言是个明显优势。
Mercurial可以从Subversion的版本库中导入历史版本。它也可以向Subversion的版本库输出历史版本。这样在决定转换之前很容易先“试一下水”,同时并行的使用Mercurial和Subversion。版本史的转换是递增的,你可以先构造一个初始版本,每次有了新的更改后加入一个小的转换。
Git是为了管理Linux内核代码而开发的一个分布式版本控制工具。它和Mercurial一样在设计上受了Monotone的影响。
Git有非常大的命令集,1.5.0版本提供了139个单独的命令。它以难学而闻名于世。与Git相比,Mercurial力求简洁。
就性能而言,Git非常快。在很多情况下它都比Mercurial快,至少是在Linux上,但Mercurial在其它操作上有优势。而在Windows上,Git不管是性能还是提供的支持,都比Mercurial差很多,至少在本书写作的时候是这样。
Mercurial的版本库不需要维护,但Git的版本库需要频繁的手工维护,将其元数据“repacks”,如果不这样做,性能就会下降,磁盘空间也会迅速增加。有多个Git版本库的服务器需要严格和频繁的重新打包,否则在备份的时候就会成为严重的瓶颈,曾经有过运行每日备份超过24小时的例子。一个新打包的Git版本库比Mercurial稍小一些,但是未打包的版本库则会大几个数量级。
Git的核心由C语言编写,许多Git命令是用shell或者perl脚本实现的,这些脚本的质量差别很大。我碰到过好几次这样的情况,明明已经出现了致命错误了,脚本还在盲目的执行。
CVS可能是世界上使用最广泛的版本控制工具。因为它是太古老和内部实现很混乱,许多年来都处于维护状态。
CVS采用的是集中式客户/服务器结构。它不会将相关的文件变更一起作为原子提交,这使得它很容易“破坏构建”:一个人成功的提交了一部分更改,然后要停下来处理合并,这使得其他人只能看见他们的部分工作。这样也会影响你和项目历史的工作方式。如果你想看到其他人针对他的那部分任务做的全部修改,你必须手动检查每个受影响的文件的变更描述和时间戳(假如你知道是哪些文件)。
CVS的分支和标签的概念实在是太混乱了,我都不想给你介绍。它也不支持文件和目录的重命名。这使得版本库非常容易崩溃。它几乎没任何的内部一致性检查功能,所以通常你不可能知道版本库是不是崩溃了。不管是新项目还是老项目,我都不推荐使用CVS。
Mercurial可以导入CVS的版本历史。然而,这里面有很多限制;对其版本控制工具的CVS导入程序也是一样。因为CVS缺少原子更改并且不支持文件系统层次的版本控制,所以不可能完全精确的重建CVS的历史。有些需要猜测,重命名通常发现不了。因为CVS的很多的高级管理功能必须手动完成,因此非常容易出错。碰到崩溃的版本库,CVS导入程序通常会出现很多问题(完全伪造的版本时间戳,若干文件被锁定十多年,这是我个人经历的两个不太有趣的问题)。
Mercurial包括一个叫convert
的扩展,它可以递增的从几个其他的版本控制工具导入版本历史。
“增量”的意思是你可以在某天将整个项目历史转换,以后再次进行转换获得初始版本以后新增的变更。
另外convert
可以从Mercurial向Subversion导出历史,这可以让你在切换之前让Mercurial和Subversion并行工作,而不会丢失任何工作。
convert命令非常简单。只要给出源版本库的URL或者路径,给出目标版本库的名称(可选),它就开始工作了。第一次转换之后,只要运行同样的命令就可以导入新的变更。
最有名史前的版本控制工具是SCCS(源代码控制系统),它是由Marc Rochkind于七十年代在贝尔实验室完成的。SCCS只能控制单个文件,这就要求项目组人员只能使用一个系统上的共享工作空间。任何时候一个文件只能由一个人修改;通过加锁来保证。人们很容易锁定一个文件,然后忘了解锁,没有管理员的帮助,任何人都不能修改那些文件。
Walter Tichy在八十年代早期开发了一个开源的SCCS替代软件;他称之RCS(版本控制系统)。和SCCS一样,RCS要求开发人员在一个共享的工作空间工作,同时锁定文件,以防止多个人他同时修改。
八十年代后期,Dick Grune在RCS的基础上开发了一套脚本,他开始称之为cmt,后来又改名为CVS(并行版本系统)。CVS的最大的创新在于让开发人员可以在自己的工作空间里同时而且独立的工作。私人的工作空间防止了开发人员总是互相干扰,这在SCCS和RCS的使用中很常见。项目中的每个文件,开发者都有自己的拷贝,可以独立的修改自己的拷贝。他们可以在提交到中央版本库之前合并更改。
Brian Berliner接管了Grune的脚本,用C重写了一遍,并在1989年发布了这些代码,现在的CVS就是从那时逐渐发展起来的。CVS随后增加了网络的功能,形成了客户/服务器结构。CVS的结构是集中式的;仅在服务器上保存一份项目的历史拷贝。客户工作空间仅包含项目中的文件的最近版本的拷贝,一些元数据告诉它们服务器的地址。CVS获得了极大成功;它可能是世界上应用最广泛的版本控制系统。
90年代早期,Sun公司开发了一个早期的分布式版本控制系统,叫做TeamWeare。TeamWare的工作空间包括项目历史的完整拷贝。TeamWare没有中央版本库的概念。(CVS使用RCS存储其历史,TeamWare使用SCCS。
在90年代中,随着时间的流逝,CVS逐渐暴露出很多问题。它对多个文件同时发生的变更不能一起记录,不能按照层次管理文件;对目录和文件的重命名很容易破坏版本库。更糟的是,它的源代码很难阅读和维护,修复这些架构上的问题的非常困难。
2001年,原来维护过CVS的两个开发者Jim Blandy和Karl Fogel,开始了一个新项目,其目标是替代CVS,新的软件将采用更好的架构和更整洁的代码。于是Subversion诞生了,它并保留了CVS集中式的客户/服务器模型,但是增加了多文件原子提交,更好的命名空间管理,和其他一些功能。总而言之,它比CVS好的多。Subversion在发布后迅速流行起来。
大概相同时间,Graydon Hoare开始了一个野心勃勃的分布式版本控制项目,他称之为Monotone。Monotone解决了很多CVS设计上的漏洞并且采用了点对点的架构,它在创新方面比以前(和以后)的版本控制系统走的更远。它使用加密哈希作为标识符,并且对于不同源的代码有了完整的 “信任”概念。
Mercurial诞生于2005年。设计方面受了Monotone的一些影响,Mercurial的目标是易用,高性能,对大的项目的良好扩展性。