版本控制

一个版本控制系统(或修订控制系统)是跟踪和控制项目文件变更的技术与实践的组合,包括源代码、文档和网页。如果你以前从来没有使用过版本控制,那你最好赶快找一个有经验的人加入。现今,所有的人都希望你的项目源代码存放在版本控制下,如果不使用版本控制,人们将会轻视项目。

版本控制如此广泛的原因是因为它实际上能帮助运营一个项目的所有方面:内部开发者交流、发布管理、Bug管理、代码稳定性和试验开发投入,以及对某个变更所属开发者的归因和授权。版本控制系统为这些领域提供了一个集中的协调力量。版本控制的核心是变更管理:识别对项目文件的每一个不相关的变更,使用元数据例如变更的日期和作者来注解每个变更,之后无论使用什么方法,任何人询问时,重放这个事实。这是一种变更为信息基本单元的交流机制。

这个部分不会讨论使用版本控制的所有方面。它是如此包罗万象,我们不得在本书不时的提及。因此,我们会通过促进协作开发的方式,专注于版本控制系统的选择和设置。

版本控制词汇表

如果你没有用过版本控制,本书不会教你如何使用,但是如果没有一些关键术语,我们就无法讨论。这些术语独立于任何特定的版本控制系统:它们是网络交互的基本名词和动词,将会在本书剩下的部分广泛使用。即使这个世界上没有版本控制,变更管理的问题也会存在,这些术语为我们简明的讨论这些问题提供了一种语言。

提交(commit)

对项目做出一个变更;更正式的,以此方式在版本控制数据库存储一个变更,可以成为项目以后发布的一部分。 “提交”可以作为名词或动词。作为名词,从本质上讲可以看作是“变更(change)”同义词。例如:“我刚刚为报告的Mac OS X上的服务器宕机Bug提交了一个修订。Jay,你能评审一下提交并看一下我有没有误用内存分配吗?”

日志信息(log message)

每次提交所附的注释,描述了提交的性质和目的。日志信息是任何项目最重要的文档:它是单个代码变更的高度技术语言与特性、Bug修订和项目进展这类更面向用户的语言的桥梁。本节的后半部分,我们会关注将日志信息发布给适当读者的方法;另外,Chapter 6, 交流the section called “编制法律的传统”将会讨论鼓励贡献者填写简明和有用日志信息的方法。

更新(update)

请求将其他人的变更(提交)和项目的本地拷贝进行组合;也就是将你的拷贝“保持最新”。这是一个非常常见的操作;大多数开发者每天都会多次更新代码,因此,可以确认他们运行的代码与别人的相同,因此他们看到一个Bug,他们可以确信它还没有被修正。例如:“嗨,我发现索引代码一直会丢掉最后一个字节。这是一个新的bug吗?”“是的,但已经在上周修正了—更新一下,一定是好了。”

版本库(repository)

一个存放变更的数据库。一些版本控制是集中式的:有一个单独的主版本库,会存放项目的所有变更。也有一些分布式的系统:每个开发者都有自己的版本库,变更可以在版本库之间任意交换。版本控制系统跟踪了变更之间的依赖关系,当需要发布时,确认一部分变更集进入发布。集中式还是分布式的问题也是一场不朽的软件开发圣战;也要防止落入在你的项目列表中讨论这个问题的陷阱。

检出(checkout)

从版本库获取项目拷贝的过程。一个检出通常会产生一个叫做“工作拷贝”(看后面)的目录树,可以从这个目录将变更提交回原来的版本库。在一些分布式的版本控制系统,每个工作拷贝本身都是一个版本库,变更可以推出(拖入)到任何愿意接受的版本库。

工作拷贝(working copy)

一个包含项目源代码文件或者网页及其他文档的开发者的私有目录树。一个工作拷贝也可能会包含由版本控制系统管理的元数据信息,可以说明工作拷贝所对应的版本库,以及所展现文件的“修订版本(看后面)”等等。通常情况下,每个开发者都有自己的工作拷贝,他可以在其中进行变更以及测试,并提交。

修订版本(revision), 变更(change), 变更集(changeset)

一个“修订版本”通常是一个特定文件或目录的具体化身。例如,如果开始时一个项目有一个文件F,修订版本是6,然后如果有一个文件对文件F提交了一个变更,那么就会产生F的修订版本7。一些系统也使用“修订版本”,“变更”或“变更集”来引用作为一个逻辑概念的一组变更。

这些术语在不同的版本控制系统可能有不同的技术含义,但是大意基本相同:他们提供了一个方法可以精确的描述一个文件或一组文件历史中的精确位置(比方说,恰恰在一个Bug修正之前或之后)。例如:“她在修订版本10修正了那个”或者“他在文件foo.c的修订版本10修正了那个。”

当一个人在不指定特定修订版本的时候谈论一个文件或一组文件时,通常假定是指最近的修订版本。

差异(diff)

变更的文本化展现。一个差异显示了哪些行发生了怎样的改变,以及围绕上下文两侧的几行。一个已经熟悉代码的开发者通常可以阅读区别,并理解变更完成的事情,甚至定位Bug。

标签(tag)

一组特定修订版本的文件的标签。标签通常用来保存人们感兴趣的项目快照。例如,为每个公开发布使用标签,这样人们就可以从版本控制系统获取发布对应的文件/修订版本。常见的标签名称有Release_1_0Delivery_00456等等。

分支(branch)

项目的一个拷贝,在版本控制之下,但却是孤立的,所以分支上的变更不会影响项目的其他部分,或者相反,除非你故意将变更“合并”过去(看后面)。分支也被称为“开发线(lines of development)”。即使当一个项目没有明确的分支,开发也可以被认为是发生在“主分支”,也称为“主线”或“主干(trunk)”。

分支提供了一个将开发线隔离的方法。例如,一个分支可以用来进行对于主干不够稳定的实验开发。或者一个分支可以用来稳定新的发布。在发布过程中,有规律的开发可以继续不受干扰的在版本库的主线进行;与此同时,在发布分支,除非经过发布管理员的确认,不允许任何变更。通过这种方式,让发布不必干扰正在进行的开发工作。关于分支的详细讨论见本章后面的the section called “使用分支来避免瓶颈”

合并(又名搬运)(merge, a.k.a. port)

用来将变更从一个分支搬运到另一个。这包括从主干合并到分支,或者相反。实际上,这是最常见的合并类型;在两个非主分支之间搬运变更的情况很少见。关于此类合并的更多信息可以看the section called “信息单一性”

“合并”也有另一种相关的含义:也就是当两个人对于同一个文件作出没有交叠的变更时,版本控制系统所作的事情。因为两个变更没有互相干扰,当一个人更新其拷贝中的这个文件(已经包含他自己的变更)时,其他人的变更会自动合并进去。这非常常见,特别是当多个人编辑同一代码时。当两个变更确实交叠了,结果就是“冲突”了;看下面。

冲突(conflict)

当两个人希望对于代码的同一个地方作出不同的修改时。所有的版本控制系统会自动监测到冲突,并至少让一个人意识到他们的变更与其他人的冲突了。这依赖于解决(resolve)冲突的人,以及与版本控制系统解决的交流。

锁定(lock)

对于某个文件或目录进行排他变更声明的方法。例如,“我现在不能对网页文件提交任何变更。似乎Alfred已经锁定了所有文件,因为他要修改背景图片。”不是所有的版本控制系统都提供了锁定能力,它们并不都需要使用锁定特性。这是因为平行,同时开发是行为准则,而将人们锁定在文件之外违背了这个思想。

需要锁定才能提交的版本控制系统,我们称之使用锁定-修改-解锁模型。而其他的我们称之为拷贝-修改-合并模型。两种模型的一个完美的深入解释和比较可以看http://svnbook.red-bean.com/svnbook-1.0/ch02s02.html。通常情况下,拷贝-修改-合并适合于开源开发,在本书讨论的版本控制工具都支持这个模型。

选择一个版本控制系统

在写本文的时候,自由软件世界中两个最流行的版本控制系统是并行版本系统CVShttp://www.cvshome.org/)和SubversionSVNhttp://subversion.tigris.org/)。

CVS已经存在很长时间了。大多数有经验的开发者已经熟悉了它,它或多或少满足了你的需要,而且因为它已经流行了很长时间了,你可能不会陷入它是否为正确选择的争论。CVS有一些缺点。它不支持简单的引用多个文件的变更;它不支持版本控制下的文件重命名和拷贝(这种情况下如果你需要识别出项目开始后的代码树,会非常头痛);它对合并的支持很弱;它处理大文件和二进制文件不佳;以及在操作很多文件时操作会非常慢。

CVS的这些Bug都不是致命的,所以它一直非常流行。然而,最近几年Subversion逐渐被人们所接受,特别是在新创建的项目中。[15]如果你新开始一个项目,我推荐Subversion。

在另一方面,因为我参与了Subversion项目,我的客观性有理由值得怀疑。在最近几年,许多新的开源版本控制系统已经出现。Appendix A, 自由版本控制系统列出了所有我知道的,大体上根据流行性排序。就像列表说明的,决定使用何种版本控制系统可能成为一项终身的研究项目。有可能你也会省掉选择的过程,因为你的主站已经做出了选择。但如果你必须做出选择,请咨询那些有经验的人,然后选择一个并使用起来。任何稳定的产品状态的版本控制系统可以实现;你不必担心会做出灾难性的错误决定。如果你无法下决心,那就Subversion吧。它学起来相对简单,在近几年里还应该保持标准的地位。

使用版本控制系统

这一部分的建议不针对任何特定版本控制系统,它们都应当可以简单的实现。详细信息请参考特定系统的文档。

版本化所有的东西

不要仅仅将项目的源代码纳入到版本控制下,也应该包括网页、文档、FAQ、设计注释和任何人们希望编辑的内容。让他们与源代码尽量接近,在同一个版本库树中。任何值得写下来的信息都应该纳入版本控制—也就是任何可能会变更的信息。不会发生变更的东西都应该归档,而非版本化。例如,一个邮件一旦发布,就不会变更;因此,将其版本化没有任何意义(除非它成为一个较大的、进化文档的一部份)。

在一个地方版本化所有的东西的原因非常重要,这样人们只需要学习一种提交变更的方式。例如,经常是一个贡献者开始编辑一个网页或者文档,然后接着做一些代码变更。当项目对于所有的提交使用相同的提交方式时,人们只需要学习一次。一起版本化所有的东西也意味着随着文档的更新,新特性可以一起提交,而代码分支时也是对文档分支等等。

不要将生成的文件置入版本控制。那些应该不是可编辑的数据,因为它们是程序方式由其他文件产生的。例如,一些构建系统根据模板configure.in文件产生configure。为了改变configure,我们需要编辑configure.in,然后重新生成;因此,只有文件configure.in是“可编辑文件。”只版本化模板—如果你也版本化结果文件,人们在对模板进行修改后会不可避免的忘记重新生成它,结果的不一致会导致无休止的混淆。[16]

所有可编辑数据必须存放在版本控制下的规则也有一个不幸的例外:Bug跟踪。Bug数据库保存了大量的可编辑数据,但是因为技术原因不能将数据存放在版本控制系统。 (一些跟踪工具有一些原始的版本控制特性,然而,独立于项目的主版本库。)

可浏览性

项目的版本库应该能够通过web浏览。这不仅是意味着浏览最新修订的能力,也包括回到过去查看早先的版本,查看修订之间的区别,以及阅读针对特定变更的日志信息等等。

可浏览性非常重要,因为它是一个轻量级的项目数据门户。如果不能通过web浏览版本库,那一个人如果希望检查特定的文件(例如,看一下某个bug修正是否已经进入代码),他必须在本地安装版本控制客户端,这会让一项只需要两分钟的任务变成一项半小时或更长的任务。

可浏览性也暗示了浏览文件特定修订版本的标准URL,以及任意给定时间最近的修订。在技术讨论或向人们指明作为证据时这非常有用。例如我们不会说“关于调试服务器的提示,可以看你工作拷贝中的www/hacking.html”,而会说“关于调试服务器的提示,可以看http://svn.collab.net/repos/svn/trunk/www/hacking.html,”给定一个会一直指向hacking.html最新修订的URL会更好。因为它不会导致混淆,也避免了用户是否有最新工作拷贝的问题。

一些版本控制系统包含内置的版本库浏览机制,而其他一些依赖于第三方的工具实现。这类工具有ViewCVShttp://viewcvs.sourceforge.net/)、CVSWebhttp://www.freebsd.org/projects/cvsweb.html)以及WebSVNhttp://websvn.tigris.org/)。第一个工具可以支持CVS和Subversion,而第二个只支持CVS,而第三个只支持Subversion。

提交邮件

对版本库的每一次提交应当能够产生一个邮件,包含谁做出修改的、何时作出的修改、修改的文件和目录以及为什么修改。邮件必须发送到专注于提交邮件的特别邮件列表,在邮件列表中能够同普通人的邮件区分开来。必须鼓励开发者和其他感兴趣的参与者订阅这个提交列表,这是从代码级别来跟踪项目的最有效方法。除了同级评审(见the section called “实践明显的代码评审”)这一明显的技术益处,提交邮件帮助我们建立了一种社区意识,因为他们创建了一个共享环境,在其中人们可以对其他人也可见的事件(提交)作出反应。

设置提交邮件的具体方法依赖于你的版本控制系统,但通常有一些脚本或其他工具可以完成这个工作。如果你在寻找过程中遇到困难,可以查一下钩子(hooks)的文档,特别是post-commit hook,CVS中也称作loginfo hook。Post-commit钩子大意上就是对提交做出自动化的响应。这个钩子会由每个提交出发,并提供关于提交的所有信息,你可以自由的使用这些信息做任何事情—例如,发送一个邮件。

通过预先包装的提交邮件系统,你可能会希望修改一些默认的行为:

  1. 一些提交邮件程序会在邮件中包含实际的区别,而不是提供在web上使用版本库浏览系统查看变更的URL。虽然提供URL非常好,这样后面就可以引用这个变更,但是在邮件本身中包含区别同样非常重要。阅读邮件已经成为人们的例行的一部分,如果变更的内容在提交邮件中,开发者可以立刻进行评审,不需要离开邮件阅读器。如果他们需要点击一个URL来评审变更,大多数人就不会作了,因为需要开始一个新动作,而不是延续一个已发生的动作。此外,如果评审者希望询问变更的某些事情,可以直接选择带原文回复并注解区别,这样就不必访问网页并辛勤的从web浏览器拷贝粘贴到邮件客户端。

    (当然,如果区别非常大,诸如大片的新代码正文添加到了版本库,那么省掉区别而只提供URL就比较有意义。大多数提交邮件程序可以自动执行这种限制。如果你的不支持,那包含区别也没有关系,只是偶尔会有些大邮件,比完全没有区别更好。便利的评审和回复是协作开发的奠基石,是否存在十分关键。)

  2. 提交邮件必须将Reply-to头设置为普通的开发邮件列表,而不是提交邮件列表。那是因为,当有人评审一个提交并撰写了一个回复,这个回复应当自动转向到人们的开发邮件列表,也就是通常人们讨论技术问题的地方。有这么几个原因。首先,你希望将技术讨论保持在一个邮件列表中,因为这也是人们所期望的情况,而且这种情况下只需要搜索一个归档。其次,有一些感兴趣的参与者可能没有订阅提交邮件列表。第三,提交邮件列表把自己当作一个监视提交的服务,而不是用来关注提交偶尔进行技术讨论。订阅提交邮件列表的人除了提交邮件没有订阅其他东西;通过那个列表发送其他内容违反了隐含的契约。第四,人们经常写程序来阅读提交邮件列表并处理结果(例如为了显示一个网页)。这些程序预备好了处理一致格式的提交邮件,但与人写的邮件不匹配。

    请注意这里设置Reply-to的建议与本章前面the section called “伟大的Reply-to辩论”中说的并不矛盾。对于信息的发送者来说,设置Reply-to非常正常。在这个情况下,发送者是版本控制系统本身,它设置Reply-to是为了说明回复的合适地方是开发邮件列表,而不是提交列表。

使用分支来避免瓶颈

非专家的版本控制用户有时候会担心分支与合并。这可能是CVS流行性的副作用:CVS对于分支和合并的接口和我们的知觉不太一致,所以很多人学着完全避免此类操作。

如果你周围有很多这类人,立即下决心战胜所有恐惧,并花时间学会如何分支和合并。它们不是什么困难的操作,一旦你习惯了,对于项目获取更多的开发者这日益重要。

分支非常有价值,因为它将一项稀缺的资源—项目代码的工作空间—变得充足。一般情况下,所有的开发者在同一个沙盒一起工作,建设同一座城堡。当某个人希望添加一个新的吊桥,但是不能确认其他人是否正在工作,分支使她被隔离到一个角落并做出尝试成为可能。如果这种投入成功了,她可以要求其他开发者检验这个结果。如果所有人认可结果,他们可以告诉版本控制系统将吊桥从分支城堡移动(“合并”)到主城堡。

很容易看到这项能力是如何帮助协作开发的。人们需要自由尝试新问题的感觉,而无须担心干扰其他人的工作。同样重要的是,当为了完成bug修正或发布稳定化(见<Chapter 7, 打包、发布和日常开发the section called “稳定发布版本”the section called “维护多发布线”),代码需要从日常的开发中分离出来时,所花费的时间数倍于无需担心跟踪一个移动目标的情况。

不受限制的使用分支,并鼓励其他人也这样做。但是要确保任何特定分支只保持必要的最短时间。任何活动分支都会让社区分神。即使不是在分支工作的人也需要对此有所了解。这种了解是应该的,当然,对于分支的提交应该能和其他提交一样发送提交邮件。但是分支不应该成为分割开发社区的机制。除了很少的例外,大多数分支最终必须合并回它们的主干并消失。

信息单一性

合并有一个推论:不要将同一个变更提交两次。也就是一个修改只进入一次版本控制系统。变更的修订(或一组修订)可以在其进入版本控制系统之后拥有唯一标示。如果它需要应用到还没有应用过的分支,那么它应该从最初的入口点合并到其他目标—而不是直接提交相同的文本,这样虽然对代码的效果是一样的,但会导致我们无法进行精确的记录和发布管理。

这个建议对于不同版本控制系统的实践效果不尽相同。在一些系统,合并是特殊的事件,从根本上与提交不同,并包含他们自己的元数据。而另外一些系统,合并的结果就像其他变更一样是提交到了系统,所以区别“合并提交”和“新变更提交”的主要方法是使用日志信息。在合并的日志信息中,不会再重复原始变更的信息。而只是指明这是一个合并,并提供原始变更的修订版本,以及一段说明其效果的文字。如果有人希望看到完整的日志信息,她应该参考原来的修订。

避免在提交之后再重复日志信息的原因非常重要,因为日志会在提交后被修改。如果变更日志在每个合并目标中重复,那有朝一日她修改了最初的信息,而那些重复还是会保持错误—混淆就会持续下去。

同样的原理也适用于撤销一个变更。如果一个变更从代码中撤销,那么这个撤销的日志信息也应该仅仅是指明撤销的是哪些特定的修订版本,而不是描述撤销过程中实际变更的代码,因为变更的内容可以通过阅读原来的日志信息和修订获得。当然,修订版本日志信息也应当说明恢复变更的原因,但它不应该从原始变更日志信息复制任何东西。如果可能,回到原来的变更日志信息,并指明它已经撤销了。

前面所说的都暗示了你应该使用一致的语法来引用这些修订版本。这不仅仅在日志信息中有益,在邮件、Bug跟踪和其他地方也同样重要。如果你使用CVS,我建议使用“path/to/file/in/project/tree:REV”,其中的REV就是CVS的修订版本好吗,例如“1.76”。如果你使用Subversion,修订版本1729的标准语法是“r1729”(文件路径不是必需的,因为Subversion使用全局修订版本号)。在其他系统中,也都有一些表达变更集的标准语法。无论对你的系统合适的语法是什么,鼓励人们使用它们来引用变更。对变更名一致的表达方法可以帮助项目更简单的纪录(在Chapter 6, 交流Chapter 7, 打包、发布和日常开发我们将会看到),而且因为许多纪录是由志愿者完成的,它应该尽可能的简单。

Chapter 7, 打包、发布和日常开发the section called “发布和日常开发”也有介绍。

授权

大多数版本控制系统提供了控制特定人可以从版本库特定子区域提交的特性。根据这个原理,当人们手握锤子时,就开始到处找钉子,许多项目开始恣意使用这种特性,小心的为每个人只赋予他们被确认的权限,而不能在任何其他地方提交。 (看Chapter 8, 管理志愿者the section called “提交者”来确认何人可以在何处提交。)

执行这样严格的控制可能会有一些害处,一个宽松的政策也足够好。一些项目会简单得使用一个荣誉系统:当一个人被赋予提交权限,即使只是版本库的一个子区域,他也会收到一个可以在项目所有地方可以提交的密码。他们只是被告知要在自己的区域提交。请记住这里没有真正的危险:在一个活跃的项目里,所有的提交会被审核。如果有人在不被允许的地方提交,其他人会发现这一点并说出来。如果这个变更需要被回退,很简单—因为所有地变更都在版本控制之下,只需要回退。

这种宽松的方法有许多好处。首先,当开发者扩展了他们的活动范围(如果他们一直在项目中,这是一个通常的情况),赋予更宽泛的权限无需额外的管理工作。一旦作出了这样的决定,这个人就可以立刻在新区域提交。

第二,扩展可以以一种更细致的方式实现。通常情况下,区域X的提交者如果希望扩展到区域Y,他会通过发表对Y的一个补丁并寻求评审开始。如果对区域Y有提交权限的人看到这个补丁并确认,他可以直接让提交者直接提交这个变更(当然也要在日志信息中提及评审者/确认者的名字)。这样,这个提交就会成为实际编写变更者的作品,无论从信息管理角度还是审计角度这样都更好。

最后,可能是最重要的,使用荣誉系统来鼓励互相尊重和信任的氛围。给一个人对某一区域的访问权限是对他们已经完成技术准备的证明—这是说:“我们已经看到了你已经具备了对某一领域做出修改的专业知识,那就继续吧。”但设置严格的授权控制则是暗示:“我们不仅仅确认你专业知识的限制,而且我们对你的目的也保持怀疑。”如果可以避免的话,你一定不愿意做出这样的评价。将一个人引入为项目的提交者是将其引入互相信任循环的机会。好的方法是给他们比期望所能发挥作用更大的权力,然后告诉他们是否保持在规定的限制内完全依赖于他们自己。

Subversion项目按照荣誉系统的方式已经运作超过4年了,在写作时包含了33个完全的和43个部分提交者。系统实际强制的只是提交者和非提交者的区别;更细的划分只由人为控制。但是我们从来没有遇到故意在领域外提交的问题。只是有一两次由于对个人提交权限范围的误解造成的错误,而且都能够迅速和亲切的得到解决。

很明显,当自律的方式不够实际时,那你就需要严格的授权控制。但这种情形非常少见。即使当有几百万行的代码,以及数百或上千的开发者时,对于任何给定模块的提交,也必须经过模块上工作的人们的评审,而且他们可以识别出提交者是否得当。如果没有有规律的提交评审,那么项目恐怕就会遇到比没有授权系统更大的问题了。

归纳起来,不要在版本控制的授权系统上花费太多时间,除非你有特别的原因。复杂的授权系统不能带来实际的好处,依赖人为控制有更多的优点。

当然,上面所说的并不意味着限制本身不重要。项目不应该鼓励人们在不够格的地方提交。此外,在很多项目中,完全(无限制)的提交访问有一个特别的状态:它隐含了项目范围问题的投票权。提交访问的政治方面将会在Chapter 4, 社会和政治的基础架构the section called “谁进行表决?”详细讨论。



[16] 对于版本化configure的不同意见,可以看Alexey Makhotkin的文章,在http://versioncontrolblog.com/2007/01/08/configurein-and-version-control/的“configure.in and version control”。