第一章 版本控制简史
1.1 为什么要进行版本控制?为什么选择Mercurial?
版本控制是管理一段信息的多个版本的过程。在最简单的情形中,就像许多人手工所做的:每当修改一个文件,就将其保存在一个包含数字的新文件名之下,每个数字都会高于前一个版本的数字。
即使是单个文件,手动管理其多个版本仍然是一项极易出错的任务,因此,自动化这一过程的软件工具早已出现。最早的自动版本控制工具旨在帮助单个用户管理单个文件的各个版本。在过去几十年,版本控制工具的范围扩大了很多。现在,它们可以管理许多文件,并帮助许多人一起工作。最好的现代版本控制工具应对成千上万的人在包含成千上万个文件的项目上一起工作一点问题都没有。
分布式版本控制是最近才出现的,由于人们乐于探索未知领域,到目前为止这个崭新的领域仍在发展。
因为我相信分布式版本控制是一个重要的主题,需要一本专业的指导,因此我要写一本关于它的书籍。最终选择写一本关于Mercurial的书籍,是因为它是学习该领域知识最简单的工具,同时可以应用到到其它版本控制工具无法承担的真实的、挑战性的需求环境中。
1.1.1 为什么要进行版本控制?
你的项目团队为什么需要一个自动化的版本控制工具?有以下几个理由:
它将跟踪项目的历史和演变,所以你不必亲为。对于项目的每个变化,都会有一个日志来记录谁做的修改,为什么修改,什么时候进行的修改,修改了些什么。
当你与其他人合作时,版本控制软件使你们更便于协作。例如,当人们或多或少同时进行可能不兼容的更改时,软件将帮助你识别和解决这些冲突。
它可以帮助你从错误中恢复。如果您进行的更改后来证明是错误的,您可以将一个或多个个文件还原到一个早期的版本。事实上,一个真正好的版本控制工具甚至可以帮助你有效地确定问题是何时引入的(详见“找出错误的源头”)。
可以让你并行地工作,并管理你在项目中多个版本之间的跳转。
一个项目,无论只有你一个人,还是还有几百个别人,这些理由中的大部分都成立——至少在理论上。
在两种不同规模的项目(“个人”和的“大型团队”)中,版本控制的可行性关键在于它的使用收益和使用开销之比。一个难以理解和使用的版本控制工具必定会加大开销。
对于一个五百人的项目,在没有版本控制工具和方法的情况下,由于其自身的体量,有可能会立即崩溃。这种情况下,版本控制的开销几乎可以忽略不计,因为没有它,项目注定会失败。
另一方面,版本控制工具对于一个人的“快速上手”式开发看上去并不合适,因为其真正的使用开销与项目的总体开销相接近,是吗?
Mercurial独有地支持这两种级别的开发。由于容易上手,你可以非常轻松地在几分钟内学会基本知识,并在最小的项目中使用它来进行版本控制。它的简单性意味着不会有很多深奥的概念或命令序列需要你去掌握,而只需专注于你真正想做的工作。同时,Mercurial的高性能和点对点的特性又可以让您轻松的处理大型项目。
任何版本控制工具都不能挽救一个糟糕的项目,但是,选择一个好的工具能够对项目进展是否顺畅造成巨大的影响。
1.1.2 版本控制的许多名称
版本控制用于许多领域,因此,它有许多称呼和缩写。下面是一些最常见的说法:
- 修订控制系统(RCS)
- 软件配置管理(SCM),或配置管理
- 源代码管理
- 源代码控制或源控制
- 版本控制系统(VCS)
有些人认为这些术语实际上有不同的含义,但在实践中它们有很大的重叠,没有一个约定甚至可用的方法来区分它们。
1.2 本书仍在完善中
本书发行的时候仍处于编写之中,希望它对别人有用。我在一个开放的许可下编写了该书,希望你,我的读者,能够提供反馈,也包括你自己的内容。
1.3 关于本书的示例
本书对代码示例采用了一种不常见的方法。每个示例都是“真实”的——每个示例实际上都是 shell脚本的运行结果,这个脚本包含了你见到的Mercurial命令。每当从源文件构建本书的映像时,所有示例脚本都会自动运行,并将其输出结果与预期结果进行比较。
这种方法的优点是示例总是准确的。这些示例完全精确的给出了本书前面所提及的特定版本 Mercurial的行为。如果更新了该书所用的Mercurial的版本,一些命令的输出就会改变,构建也会失败。
这种方法有一个小的缺点,就是你在例子中所看到的日期和时间往往被“挤压”在一起,如果相同的命令是由一个人从键盘输入的,就不会这个样子。自动示例脚本每秒钟会执行许多命令,而一个人每隔几秒才会发出一个命令,从而每个执行结果所对应的输出时间会被拉开。
举个这样的例子,示例中几个连续提交都显示是在同一秒内发生的。你可以在137页“找出错误的源头”中的二分法示例部分发现这种情况。
所以,当你阅读示例时,不要过分关注命令输出中的日期或时间。但要相信你看到的行为是一致的和可重现的。
1.4 该领域的未来趋势
在过去四十年里,在开发和使用版本控制工具方面有一个明确的趋势,因为人们已经熟悉了这些工具的能力并受限与它的局限。
第一代工具只能在单台计算机上管理单个文件。虽然这些工具相比手动版本控制是一个巨大的进步,但锁定模型和对单一计算机的依赖使得它们只能用于小型和紧凑的团队。
第二代工具采用了以网络为中心的架构,并且同时可以管理整个项目,从来突破了这些限制。随着项目越来越大,它们遇到了新的问题。由于客户端需要与服务器频繁地通信,对于大型项目,服务器的规模成了问题。不可靠的网络连接可能会导致远程用户根本无法与服务器进行通信。开源项目允许任何人匿名访问,但只有读取权限,这些没有提交权限的用户无法记录他们所做的修改,从而使用这些工具不能与项目进行正常的交互。
当前的版本控制工具本质上是点对点的。它们都降低了对单个中央服务器的依赖,并允许人们将其版本控制数据发布到实际需要的地方。通过互联网协同工作已经从技术上的限制转变为决策和看法上的问题。现代的工具已经可以长久地在离线状态下自主运作,只有在想将更改同步到另一个存储库时才需要网络连接。
1.5 分布式版本控制工具的优势
几年来,尽管分布式版本控制工具已经同其上一代的同类产品一样稳健可用,但使用旧工具的人们还是不一定能够认识到它们的优点。相对于集中式工具,分布式工具有许多亮点。
对于单个开发人员,分布式工具几乎总是比集中式工具快得多。原因很简单:对于集中式工具,许多常见操作需要通过网络进行,因为大多数元数据只是在中央服务器上保存了一份。分布式工具在本机保存所有元数据。其它都是一样的。网络通讯导致了集中式工具额外的开销。不要低估一个快速响应工具的价值:你将花费大量的时间与你的版本控制软件进行交互。
分布式工具不受服务器基础结构变化的影响,原因同样是因为它们将元数据复制到许多地方。如果你使用集中式系统,并且服务器发生了故障,你最好期望你的备份介质是可靠的,你上次的备份是最新的,并且真的可以恢复。对于分布式工具,可以从每个贡献者的计算机上获取许多备份。
网络可靠性对分布式工具的影响远小于对集中式工具的影响。没有网络连接,您甚至不能使用集中式工具,除非极有限的几个命令。对于分布式工具,如果网络在你工作时断开,你甚至不会注意到。唯一不能做的是与其它计算机上的存储库相通讯,与本地操作相比这是相对少见的。如果你的团队合作者之间相距遥远,这一点可能很重要。
1.5.1 对开源项目的好处
如果你喜欢一个开源项目,并决定改进它,而且这个项目使用了分布式版本控制工具,那么你与那些自认为是项目“核心”成员的人是平等的。如果他们发布了自己的存储库,你就可以作为他们中的一员,使用相同的工具和同样的方法,立即复制他们项目的历史记录,开始进行更改,并记录你的工作。相比之下,对于集中式工具,你必须以“只读”模式使用它,除非有人授予你向中央服务器提交变更的权限。在此之前,你无法记录变更,并且当你尝试更新客户端的存储库视图时,你的本地修改将面临损坏的风险。
分支不是问题
有人认为分布式版本控制工具会对开源项目构成某种风险,因为它们容易造成项目开发的“分支”。当开发组内部成员之间的意见或态度存在差异,而决定不再一起工作时,分支就会发生。每一方都会有一个多少完整的项目源代码副本,并沿着自己的方继续。
有时,分支的各个阵营决定协调分歧。使用集中式版本控制系统,和解的技术历程是痛苦的,大部分工作必须手动完成。你必须决定谁的版本历史“获胜”,然后以某种方式将其它团队的修改移植到该版本树。这样通常会丢失其中一方的部分或全部版本历史。
在分支方面,分布式工具认为分支是项目开发的唯一方法。你所做的每一个改变都是一个潜在的分支点。这种提法的坚实基础是分布式版本控制工具必须真正善于合并分支,因为分支是绝对根本:它们总在发生。
假如每个人所做的每一件工作,都是以分支和合并的方式构成的,那么开源世界所谓的“分支”纯粹是一个社会问题。如果有的话,分布式工具降低了分支的可能性:
它们消除了集中式工具强加的社会差异:该差异存在于内部人员(具有提交权限的人员)和外部人员(无提交权限的人员)之间。
它们使得分支之后的协调更加容易,因为从版本控制软件的角度看,只需要再来一次合并而已。
有些人抵制分布式工具,因为他们希望对其项目保持严格的控制,并认为集中式工具可以提供这种控制。然而,如果你有这个想法,并且公开了你的CVS或Subversion存储库,就有很多工具可以提取整个项目历史(虽然缓慢),并在你不能控制的地方重建它。所以,在这种情况下,你的控制是虚幻的,你正在放弃与别人协作的机会,逼迫他们镜像你的项目历史并创建分支。
1.5.2 对商业项目的好处
许多商业项目是由分散在全球各地的团队负责的。远离中央服务器的贡献者,其命令的执行会很慢,可靠性也可能较低。商业版本控制系统试图通过远程站点复制附件来改善这些问题,这些附件通常很昂贵,并且难以管理。首先,分布式系统不会遇到这些问题。更好的是,您可以轻松地设置多个权威服务器,例如每个站点一个,以便在通过昂贵的长途网络相连接的存储库之间没有多余的通信。
集中式版本控制系统往往具有相对较低的可扩展性。几十个并发用户的总负荷常常会导致一个昂贵的集中式系统倒下来。一个典型的应对往往是采用昂贵和复杂的复制设施。因为使用分布式工具的中央服务器其负何——如果你有一个—— 要低很多倍(因为所有数据被复制到各个地方),单个廉价的服务器就可以满足一个大型的团队的需要,一个简单的脚本就可以进行数据复制来平衡负载。
如果您有一名某领域的员工,正在客户现场解决实际问题,他将受益于分布式版本控制。这个工具可以让他创建定制版本,尝试各种不同的修复方案,通过项目历史高效地在客户环境中进行回溯并在源码中定位缺陷。所有这些工作都不需要连接到公司的网络。
1.6 为什么选择Mercurial?
Mercurial特有的属性使其作为版本控制系统是一个非常好的选择。
- 易学易用。
- 轻量级。
- 卓越的伸缩性。
- 易定制。
如果你熟悉版本控制系统,应该能够在五分钟之内开始运行Mercurial。即使不行,也不会超过几分钟。Mercurial的命令和功能总体上是统一和连贯的,因此你只需要遵循几个普遍的规则,而不用处理大量的例外。
在一个小型项目中,你可以立即开始使用Mercurial。创建新的变更和分支、传输变更(无论是本地还是通过网络)、获取项目历史和状态,这些操作都很快。Mercurial试图通过将较低的门槛和快速的操作结合起来,以另一种方式来保持项目的敏捷。
Mercurial不仅能用于小型项目,对于那些拥有几百甚至几千个参与者,每个参与者包含数万个文件,几百兆源代码的大型项目,它同样适用。
如果Mercurial的核心功能对你来说不够用,你也可以很容易的添加功能。Mercurial非常适合脚本化的任务,它使用Python实现且内部代码清晰,这使的它很容易使用扩展来添加功能。目前,从帮助标识缺陷到提高性能等各个方面,已有许多流行且不错的扩展可供使用。
1.7 比较Mercurial和其它工具
在阅读之前,请理解,这一部分必然掺杂有我的个人经验,兴趣和(我敢说)偏见。我使用过下面列出的每一个版本控制工具,通常都用过几年。
1.7.1 Subversion
Subversion是一种流行的用于替换CVS的版本控制工具。它采用集中式客户端/服务器架构。
Subversion和Mercurial使用类似名称的命令来执行同样的操作,所以如果你熟悉其中之一,那么学习另一个也非常容易。这两种工具都可以移植到所有流行的操作系统。
在版本1.5之前,Subversion不支持对合并进行跟踪。在撰写本书时,合并跟踪功能刚刚添加,复杂且问题多多。
在我所做的基准测试中,Mercurial相较于Subversion,每个版本控制操作都具有显著的性能优势。与Subversion 1.4.3最快的ra_local文件存储方式相比,这个优势在2到6倍之间。现实中的部署会涉及到网络访问,Subversion将更加处于劣势地位。因为许多Subversion命令必须与服务器通信,而Subversion没有有效的的复制工具,所以对于大型项目,服务器容量和网络带宽将会成为主要瓶颈。
此外,对于一些常用操作,如找到已修改的文件(status)以及显示文件相对于当前版本所作的修改(diff),Subversion为了避免网络传输,占用了大量的存储空间。最终,尽管Mercurial存储库包含了完整的项目历史,Subversion工作副本仍然可以与Mercurial存储库及工作目录具有相同的大小,甚至更大。
Subversion有大量的第三方工具支持。目前,Mercurial在这方面相对滞后。然而,这种差距正在缩小,现在,一些GUI工具,其Mercurial版甚至比Subversion版更加出色。与Mercurial一样,Subversion有一个优秀的用户手册。
因为Subversion不在客户端存储版本历史,所以它非常适合管理那些需要处理大量不可阅读的二进制文件的项目。对于一个10MB的不可压缩文件,如果你捡出其50个版本,Subversion的空间占用保持不变。而对于任何分布式SCM,由于各个版本之间差异极大,所占用的空间会与检出的版本数量成比例的快速增加。
此外,通常很难甚至不可能合并不同版本的二进制文件。Subversion能够让用户锁定一个文件,以便他们暂时拥有这个文件的独占权限,并提交对其所作的修改,对于一个广泛使用二进制文件的项目来说,这是一个显着的优势。
Mercurial能够从Subversion存储库导入版本历史。也可以将版本历史导出到Subversion存储库。这使得在确定从其中一个转换到另一个之前,可以同时使用Subversion和Mercurial来“试水”。历史记录的转换是增量式的,因此你可以先执行首次转换,然后再执行额外的小转换,以引入新的文件变更。
1.7.2 Git
Git是一个分布式版本控制工具,开发目的是用于管理Linux内核源代码树。与Mercurial一样,它的早期设计有点受Monotone的影响。
Git有一个非常大的命令集,版本1.5.0提供了139个单独的命令。它出了名的难学。与Git相比,Mercurial更注重简洁。
在性能方面,Git非常快。至少在Linux下,多数情况要比Mercurial快,尽管个别操作Mercurial做得更好。但在Window下,到目前为止,Git的性能和综合支持级别仍远远落后于Mercurial。
可是Mercurial存储库不需要维护,而Git存储库需要频繁地对其其元数据手动“重新打包”。否则,性能就会下降,而空间占用则会快速增长。一个包含许多Git存储库的服务器,如果不经常对它们重新打包,备份就会严重地受限于磁盘空间,而且每天备份的时间会远远超过24小时。与Mercurial存储库相比,一个刚刚打包过的Git存储库要稍小一些,但是一个未打包的存储库则要大上几个数量级。
Git内核是使用C语言编写的。许多Git命令是用shell或Perl脚本实现的,这些脚本的品质差别很大。在脚本存在错误的情况下盲目的运行,结果往往是致命的。我已经遇到过几次这样的情况。
Mercurial能够从Git存储库导入版本历史。
1.7.3 CVS
CVS可能是世界上使用最广泛的版本控制工具。由于其年代久远且内部混乱,多年来它只进行了少量的维护。
它使用了集中式客户端/服务器架构。它不会将相关联的文件变动组合成一个原子提交,从而使人们很容易“破坏构建”:一个人可以成功地提交一部分变动,剩余的变动则由于需要合并而被阻止提交。这会导致其他人只能看到他工作成果的一部分。这也会影响你对项目历史记录的使用。如果要查看某人对任务中某个部分所作的全部修改,则需要手动检查他对每个文件所做更改的描述和时间戳(如果你恰好知道这些文件有哪些)。
CVS的标签和分支概念很混乱,此处不再详述。它也不支持文件和目录的重命名,导致存储库非常容易被破坏。它几乎没有内部的一致性检查功能,因此,甚至常常都不知道存储库是否已损坏,或者它是如何损坏的。对于任何项目都不推荐使用CVS,不论是现有的项目还是新启动的项目。
Mercurial可以导入CVS的版本历史记录。但是有些问题要注意,这些问题同样存在于使用其它版本控制工具从CVS导入版本历史。由于CVS缺乏对版本的原子提交,且文件系统的层次结构没有被版本化,所以不可能完全精确地重建CVS历史记录。有些工作像猜谜,重命名通常不会显示。因为很多高级CVS管理必须手工完成,所以容易出错,从CVS导入时常常就会发生多种存储库损坏问题(我个人只经历过两个不太有趣的问题,完全错误的版本时间戳和文件被锁定超过十年)。
Mercurial能够从CVS存储库导入版本历史。
1.7.4 商业工具
Perforce具有集中式的客户端/服务器架构,客户端没有任何数据缓存。与现代的版本控制工具不同,Perforce需要用户为每个准备编辑的文件运行一个命令来通知服务器。
Perforce的性能对小型团队来说相当不错,但随着用户数量增长到超过几十个,Perforce的性能迅速下降。安装较大的Perforce需要部署代理来应对用户负载。
1.7.5 选择版本控制工具
除了CVS之外,上面列出的所有工具都有其独特的优势来适应特定的工作方式。没有一个版本控制工具在所有情况下都是最好的。
例如,对于频繁编辑二进制文件的工作,Subversion是一个好选择,因为它实质上是集中式的且支持对文件加锁。
我个人认为,Mercurial 拥有简单,高性能和良好的合并支持,并成功地将这些特性组合了起来,我已经使用它好几年了。
1.8 从其它工具切换至Mercurial
Mercurial捆绑了一个叫做convert的扩展,它可以从其它的几个版本控制工具中增量式导入版本历史。“增量”的意思是,到目前为止项目的所有历史可以被一次性转换,随后可以重新运行转换以获得初次转换之后新发生的变更。
扩展convert支持以下版本控制工具:
- Subversion
- CVS
- Git
- Darcs
此外,convert可以将版本从Mercurial导出到Subversion。这使得在确认转换到Mercurial之前,可以尝试同时使用Subversion和Mercurial,从而不会有丢失任何已完成工作的风险。
convert命令很容易使用,只需指定源存储库的路径或URL,可选地,也可以指定目标存储库的名称,这样就可以工作了。首次转换之后,只需再次运行相同的命令即可导入新的变更。
1.9 版本控制简史
早期最著名的版本控制工具是Marc Rochkind在20世纪70年代初在贝尔实验室编写的SCCS(源代码控制系统)。SCCS对单个文件进行操作,并且要求项目中的每个人都可以存取单个系统上的共享工作空间。任何时候只有一个人可以修改文件,并通过对文件加锁来解决访问冲突。锁定文件后忘记解锁的事情经常发生,导致其他人无法修改这些文件,除非请求管理员的帮助。
在80年代初,Walter Tichy针对SCCS开发了一种免费的替代方案,叫做RCS(修订控制系统)。与SCCS一样,RCS需要开发人员在单个共享工作空间中工作,并且锁定文件以防止多个人同时修改它们。
在80年代后期,Dick Grune使用RCS作为基础创建了一组shell脚本,最初叫做cmt,但后来又改名为CVS(并行版本系统)。CVS的重大创新是它让开发人员在自己的个人工作空间中同时并相对独立地工作。而SCCS和RCS中常见的情况,就是个人工作空间始终阻碍着开发者前进的步伐。CVS项目中,每个开发人员都有项目中所有文件的副本,并且可以独立地修改这些副本。在将变更提交到中央存储库之前他们必须合并其修改。
Brian Berliner用C语言重写了Grune的原始脚本,并于1989年发布,自此,这些代码逐渐发展成为了现代版本的CVS。后来,CVS拥有了通过网络连接进行操作的能力,它使用了客户端/服务器架构。CVS的架构是集中式的,只有服务器拥有项目的历史记录。客户端工作空间只包含项目文件最新版本的副本,以及一些元数据来告诉他们服务器在哪里。CVS取得了巨大的成功,它可能是世界上使用最广泛的版本控制系统。
在90年代初期,Sun Microsystems公司开发了一个早期的分布式版本控制系统,称为TeamWare。TeamWare的工作区包含完整的项目历史。TeamWare也没有中央存储库的概念。(CVS依赖RCS保存历史记录,而TeamWare使用SCCS。)
伴随着90年代的步伐,人们逐渐地认识到了CVS的一些问题。同时发生的修订被分别地记录到多个文件,而不是在逻辑上将它们组合在一起作为单个原子操作;不对文件的层次结构进行管理;很容易通过重命名文件和目录使一个存储库变的混乱。更糟的是,它的源代码很难阅读和维护,这使得修复这些架构问题令人望而却步。
2001年,两位原CVS的开发者Jim Blandy和Karl Fogel启动了一个新的项目,使用更好的架构和更简洁的代码来替换CVS。结果就是Subversion,它延续了CVS的集中式客户端/服务器架构,但添加了多文件原子提交,更好的命名空间管理,和许多其它功能,使其成为一个比CVS更好的工具。因此它一经发布,便迅速普及。
几乎同时,Graydon Hoare启动了一个雄心勃勃的分布式版本控制系统项目,称之为Monotone。虽然Monotone解决了许多CVS的设计缺陷,并拥有一个点对点的架构,但相较早期(和后续)的版本控制工具,它的许多创新做法过于超前。它使用了加密的散列值作为标识符,整体概念是“信任”来自不同来源的代码。
Mercurial诞生于2005年。虽然其设计受到Monotone多方面的影响,但Mercurial更专注于在大型项目上的易使用,高性能和可伸缩性。