现代软件非常复杂,团队用于管理软件开发的方法反映了这种复杂性。尽管许多组织在项目发展过程中使用修订控制软件来跟踪和管理项目的复杂性,但是如何明智地选择修订控制工具的话题却很少受到关注。直到最近,修订控制的世界都是死气沉沉的,所以在这个主题上没有什么可说的。
然而,在过去的五年里,版本控制软件的创造力爆发了,现在团队的领导者们面临着一系列令人眼花缭乱的选择。
并发版本系统(CVS)是十多年来占主导地位的开源版本控制系统。尽管它有许多严重的缺点,但它仍然作为遗留系统被广泛使用。为取代CVS而编写的Subversion在2000年代中期流行起来。(Perforce是Subversion的一个著名商业竞争对手)Subversion和CVS都遵循客户机-服务器模型:一个中央服务器托管项目的元数据,开发人员将这些数据的有限视图“检出”到他们工作的机器上。
在21世纪初,一些项目开始远离集中开发模型。在最初的6个左右的产品中,今天最受欢迎的是Git和Mercurial。这些分布式工具的显著特征是它们以点对点的方式操作。项目的每个副本都包含项目的所有历史记录和元数据。开发人员可以在任何适合他们需求的安排中共享更改,而不是通过中央服务器。
无论是集中式的还是分布式的,修订控制系统都允许团队成员执行少量的核心任务:
当子项目上的工作完成时,它可以集成回更大的项目中。这被称为合并。
每个版本控制工具都强调一种不同的工作和协作方法。这反过来又会影响团队的工作方式。因此,没有任何修订控制工具适合每个团队:每个工具都有一组复杂的权衡,甚至很难看到,更不用说评估了。
在大型项目中,管理并发开发是一个重要的症结。不幸的是,开发人员很熟悉由于不相关模块中的bug而导致功能进程停滞的情况,所以他们更喜欢通过在独立的分支中工作来管理这种风险。当一个分支被隔离的时间过长时,就会出现一种不同的风险:在不同分支中工作的团队对相同的代码做出相互冲突的更改。
Subversion和分布式工具之间的主要区别是:在Subversion中,隐式地提交更改会发布它,而在分布式工具中,两者是解耦的。
将一个分支的更改合并到另一个分支中可能是令人沮丧和危险的,它可能会默默地重新引入已修复的bug或产生全新的问题。这些风险可以通过以下几种方式产生:
由于合并引入的风险超出了正常开发所引起的类型,因此修订控制系统如何处理分支和合并是非常重要的。在Subversion中,创建新分支就是对现有分支进行复制,然后检出它的本地视图。尽管创建分支的成本相对较低,但Subversion允许多个开发人员在单个分支中并发工作。由于在单个分支的基础上工作不会立即带来明显的成本,所以大多数团队维护的分支很少。
这种工作模式带来了新的风险。假设Alice和Bob同时处理单个分支中的相同文件。Subversion将分支的历史视为线性的:版本103在版本102之后,在版本104之前。Alice和Bob各自从服务器检出了分支的第105版副本到他们自己的笔记本电脑上。这些工作副本包含他们未提交的工作,彼此隔离,直到提交他或她的更改。
如果Alice先提交她的作品,它将成为第106版。在Bob将他的工作与Alice的修订版106合并之前,Subversion将不允许他提交修订版107的工作。既然Bob不能提交他的工作,那么如果他的合并出错了,会发生什么情况呢?他将没有他所做的任何永久记录,并面临一些可怕的可能性:他的作品可能丢失或悄悄损坏。因为Subversion提供了在共享分支之外工作的最小阻力路径,开发人员倾向于盲目地这样做,而不了解他们所面临的风险。事实上,风险甚至更微妙:假设Alice的更改在文本上与Bob的更改不冲突;她在提交之前不会被迫签出Bob的更改,因此她可以不受阻碍地将更改提交到服务器,从而形成一个人类从未见过或测试过的新树状态。
Mercurial和Git是分布式的,因此它们缺乏Subversion的单一中心服务器(托管元数据)的概念。存储库包含项目完整历史的独立副本和包含项目文件快照的工作目录。如果Alice和Bob一起工作在一个项目中,Alice可能克隆Bob的存储库的一个副本,或者她可以从某个服务器上克隆一个副本。当她提交一个更改时,它将停留在她机器上的存储库本地,直到她选择以某种方式共享它。她可以通过将它发布到服务器或让Bob直接从她那里提取它来实现这一点。
Mercurial和Git都将获取远程更改与本地更改的合并解耦。如果Bob获取Alice的修订,他仍然可以提交自己的更改,而不需要首先与Alice的合并。当他随后合并时,他仍然有一个提交的更改的永久记录。如果合并遇到麻烦,他将能够恢复他早期的工作。
在修订控制的分布式视图下,每个提交都可能是自己的一个分支。如果Bob和Alice从完全相同的历史视图开始,并且每个人都进行了提交,那么他们已经在项目的历史中创建了一个很小的匿名分叉。在一方引入另一方的更改之前,双方都不会知道这一点,到那时,他们将不得不与对方的更改合并。
这些微小的分支和合并在Mercurial和Git中是如此频繁,以至于这些工具的用户看待分支和合并的方式与Subversion用户非常不同。项目开发的并行性和分支性在它的历史中是显而易见的,这使得谁在什么时候做了哪些更改,以及他们基于哪些其他更改变得很明显。Mercurial和Git都可以将名称与较长时间的开发线相关联(例如,“将成为2.0版本的代码”),因此一个足够重要的开发可以有一个名称。
看看Subversion和分布式工具在哪些方面为用户提供了自由度是有指导意义的。Subversion对其管理的文件和目录的层次结构几乎没有施加任何结构。除了通过svn copy命令提供的功能外,它缺乏分支的概念。用户通过约定在人们一致认为分支应该存在的层次结构的一部分中找到分支。按照惯例,一条单一的“开发主线”被称为/主干,分支位于/分支之下。
因为Subversion没有强制执行构造分支的策略,所以它有一些有趣的行为。要跨整个分支执行操作,您必须知道分支的根在命名空间中的位置。大多数Subversion命令只在指定的名称空间的任何部分上操作。如果Alice签出/branches/myfeature并在/branches/myfeature/deep/sub/dir的工作副本中运行svn commit,那么她将只提交分支的deep/sub/dir目录中的更改。从错误的目录中心不在焉的提交可能会让Alice认为一切都很好,但留给她的同事一个不一致的、破碎的树。
svn update命令的操作方式与此相同:可以将工作副本的各个部分同步到分支历史的不同版本。这很容易导致工作副本看起来不一致,而实际上它偶然地由分支历史中不同时间的片段组成。
相反,分布式工具将存储库的整个内容视为要处理的单元。如果你跑Git提交-a
在存储库中的任何目录中,它将获取所有未完成更改的快照。水银,hg更新
操作类似,使整个工作目录相对于历史上的某个特定点保持最新。这两种工具都不可能意外签出不一致的分支视图。如果您手动将文件或目录恢复到某个特定版本,则用户界面将显示受影响的文件为已修改的,从而明确这一点。
尽管Subversion没有对使用分支的项目施加结构,但它建议使用命名分支的约定。因此,通过中央服务器进行协作的Subversion用户很可能很容易找到彼此的项目。Mercurial和Git都使在服务器上发布只读存储库变得相当容易,但存储库的所有者必须告诉其他人存储库的位置:它可以是Internet上的任何地方,而不仅仅是单个服务器主机上的一个众所周知的位置。此外,这两种系统都不能使读写发布特别容易。这是设计好的。
Subversion的单服务器模型要求想要与他人共享更改的协作者必须拥有对共享存储库的写访问权,这样他们才能发布更改。对于Git和Mercurial,当然可以遵循这种集中式模型,但这是一个约定问题。用户经常在自己的服务器或托管提供商托管他们的存储库。他们的合作者不是将他们的更改发布到共享服务器,而是从他们那里获取更改,并在其他地方发布他们自己的修改。
Subversion和分布式工具之间的主要区别是:在Subversion中,隐式地提交更改会发布它,而在分布式工具中,两者是解耦的。在所有参与者都有对服务器的写访问权限且所有人总是连接到同一网络的设置中,将提交和发布结合起来非常方便。将两者分离增加了一个额外的发布步骤,但打开了离线工作和使用新发布技术的可能性。
举一个新发布的例子,Mercurial支持使用其内置Web服务器在LAN上临时发布存储库,并支持使用Bonjour协议发现存储库。这对于快速开发设置(如软件项目的冲刺)是一个强有力的组合:只需打开您的笔记本电脑,共享您的存储库,您的Wi-Fi邻居就可以立即查找并提取您的更改,而不需要服务器基础设施。
集中式和分布式发布方法都提供了折衷方案。对于一个紧密联系的小型团队来说,先提交后发布看起来是一个更容易的选择。在组织更松散的环境中,例如,当团队成员经常出差或花大量时间在客户站点时,提交与发布的分离可能是更好的选择。
集中式工具非常适合高度结构化的“铁腕统治团队”管理模式。访问可以由管理人员控制,而不是对等体。树的整个部分只能由具有特定级别权限的员工写入或读取。去中心化系统目前除了能够将敏感数据分割到单独的存储库之外没有提供太多功能,这有点尴尬。
许多团队开始使用分布式版本控制系统的方式几乎与他们正在取代的集中式系统完全相同。每个人都克隆几个中央存储库中的一个,并将更改推回。这种熟悉的模式可以很好地让玩家感到舒适,但它只是触及了可能的交互风格的表面。
由于分布式模型强调将更改拉入本地存储库,因此它很自然地适合于支持代码评审的开发模型。假设Alice管理的存储库将成为她的团队软件项目的2.4版本。Bob告诉她,他已经准备好提交一些更改,并给了她一个URL,她可以从中提取他的更改。当她通读他的修改时,她注意到他的代码不能正确处理错误条件,所以她要求他在接受、合并并发布之前修改他的工作。
当然,团队可能同意在集中式系统中使用“在合并之前审查”的策略,但是软件的默认行为是更加允许的。因此,团队必须采取明确的步骤来约束自己。
考虑到它们的背景,Mercurial和Git在合并更改方面有相似的方法,而Subversion的做法则不同,这并不奇怪。
由于合并在Mercurial和Git中频繁发生,它们在这一领域具有良好的设计能力。在合并期间绊倒修订控制系统的典型情况是被重命名或删除的文件和目录。Mercurial和Git都可以干净地处理重命名。
Subversion的合并机制既复杂又脆弱。例如,被重命名的文件通常会在合并中消失。这个严重的错误已经被部分解决,因此文件现在被重命名,但它们可能包含错误的内容。目前尚不清楚这是否真的是向前迈出的一步。
跨平台开发团队经常遇到一个关于文件命名的更微妙的问题。Windows、OSX和Unix系统对于处理文件名的情况有不同的约定(例如,对于FOO。TXT与foo.txt同名)。Mercurial在这方面胜过了竞争对手。它可以在默认情况下对大小写敏感的操作系统上使用不区分大小写的文件系统,并安全地使用该文件系统。
通常,开发人员收到新bug报告的第一反应是查看项目的历史记录,看看最近发生了什么更改,或者注释源文件,看看谁在什么时候修改了它们。使用分布式工具,这些操作是即时的,因为所有数据都存储在开发人员的计算机上,但是当在远程或拥塞的Subversion服务器上运行时,这些操作可能会很慢。由于人类是没有耐心的生物,额外的等待时间将减少这些有用命令的运行频率。这是响应性对人们如何使用软件产生不成比例影响的另一种方式。
虽然简单地显示历史记录是有用的,但如果能有一种自动确定错误来源的方法,那就有趣得多了。Git引入了一种通过bisect命令来实现此目的的技术(它被证明非常有用,Mercurial获得了一个平分
自己的命令)。这种技术学习起来很简单:您可以在您知道没有bug的修订版本上使用bisect命令,而在您知道有bug的修订版本上使用bisect命令。然后,它检查出一个修订版本,并询问你该修订版本是否包含错误;它重复这个过程,直到识别出第一次出现错误的修订版本。
这对开发人员很有吸引力,部分原因是它很容易实现自动化。编写一个小脚本来构建你的软件并测试bug的存在;发射平分
;然后稍后再回来,找出是哪个修订引入了问题,而不需要进一步的手动干预。另一个原因是平分
最吸引人的是它在对数时间内运行。让它搜索1000个修订范围,它只会问大约10个问题。将搜索范围扩大到10,000个修订,问题的数量就会增加到14个。
的重要性再怎么强调也不过分平分
.它不仅完全改变了您发现错误的方式,而且如果您经常使用脚本驱动它,您将有效地免费开发动态回归测试。保存这些测试并使用它们!
狡猾的读者会发现,使用Subversion搜索提交历史记录要比使用分布式工具容易得多,因为Subversion的历史记录更加线性。与此相反的是平分
命令被内置到其他工具中,因此更容易获得并适应可靠的自动化。
一旦你在一个软件中发现了一个错误,仅仅修复它是远远不够的。假设您的bug已经存在了好几年,并且在该领域中有三个版本的软件需要进行修补。每个版本都可能有一个“持续的”分支,在那里累积bug修复。问题是,尽管将修复程序从一个分支复制到另一个分支的想法很简单,但实践却不是那么直接。
Mercurial、Git和Subversion都能够从一个分支中选择更改并将其应用到另一个分支。摘樱桃的麻烦在于它很脆。变更并不只是在空间中自由浮动:它依赖于围绕它的代码的上下文。其中一些依赖是语义的,会导致更改被干净地挑选出来,但随后会失败。许多依赖仅仅是文本的:有人遍历并更改了单词的每个实例香蕉来橙色在目标分支中,引用香蕉的精心挑选的更改不能再被干净地应用。
当由于文本问题而导致选择失败时(不幸的是,这种情况很常见),通常的方法是用肉眼检查更改,然后在文本编辑器中手动重新输入。分布式版本控制系统已经提出了一些强大的技术来处理这类问题。
也许最强大的方法是由Darcs所采取的,这是一个分布式版本控制系统,它在如何看待变化方面是真正革命性的。与简单的变化链或变化图不同,Darcs有一个更强大的理论来解释变化是如何相互依赖的。这使得它在选择更改方面比任何其他分布式版本控制系统都要成功得多。那么,为什么不是所有人都使用Darcs呢?多年来,它一直存在严重的性能问题,使其完全不切实际。这些问题已经得到了解决,但目前的进展只是相当缓慢。更根本的问题是,它的理论很难掌握,所以两个不了解Darcs知识的开发人员很难判断他们是否有相同的更改。
让我们回到Mercurial和Git的阵营。由于这些工具提供了在任何修订之上提交的能力,从而产生一个小的匿名分支,一个可行的替代选择是:使用等分法来识别出现错误的修订;看看那个修订版;修复bug;并将修复作为引入错误的修订的子版本提交。这个新的更改可以很容易地合并到任何有原始bug的分支中,而不需要任何粗略的挑选动作。它使用了一种版本控制工具的常规合并和冲突解决机制,因此它远比采樱桃(采樱桃的实现几乎总是一系列怪诞的hack)更可靠。
这种回溯历史修复bug,然后将修复合并到现代分支的技术,被Monotone(一个很有影响力的分布式版本控制系统)的作者命名为“daggy fixes”。修复被称为daggy因为他们利用了一个项目的历史被结构化为有向无环图(dag)的优势。虽然这种方法可以用于Subversion,但与分布式工具相比,它的分支是重量级的,这使得daggky -fix方法不太实用。这强调了一个观点,即工具的优势会影响用户使用的技术。
选择修订控制系统是一个绝对答案少得惊人的问题。需要考虑的基本问题是您的团队使用何种类型的数据,以及您希望团队成员如何进行交互。
分布式工具在与集中式工具竞争时遇到的一个问题是二进制文件的管理,尤其是大型文件。尽管许多软件学科都有一个政策,即永远不要将二进制文件置于修订控制系统的管理之下,但这样做在某些领域是很重要的,如游戏开发和EDA(电子设计自动化)。例如,对于一个游戏项目来说,通常会对数十gb的纹理、骨架、动画和声音进行版本化。二进制文件与文本文件的不同之处在于通常难以压缩和无法合并。这些都带来了各自的挑战。
如果在修订控制下存储一个中等大小的二进制文件并修改多次,那么存储每个修订版本所需的空间很快就会超过所有文本文件所需的空间之和。在集中式系统中,这种开销只在中央服务器上支付一次。在分布式系统中,每台笔记本电脑上的每个存储库都有该文件历史的完整副本。这不仅会破坏性能,还会增加不可接受的存储成本。
当两个人修改一个二进制文件时,对于大多数文件格式来说,没有办法告诉他们的文件版本之间有什么不同,软件帮助解决他们各自修改之间的冲突就更罕见了。作为避免合并二进制文件的一种方法,集中式系统提供了锁定文件的能力,以便在任何时候只有一个人可以编辑给定分支中的文件。分布式系统本质上不能提供锁定,因此它们必须依赖于社会规范(例如,只有一个人修改某些类型的文件的团队策略)。
与分布式工具相比,集中式工具将使分支的历史表现得更加线性。这是优点还是缺点似乎是一个角度的问题。更线性的历史记录更容易理解,因此对开发人员的修订控制要求更低。另一方面,包含大量小分支和合并的历史记录更准确地反映了项目的真实历史,并使团队成员的代码在工作时基于哪个项目状态更加清晰。对于喜欢保持项目历史记录整洁的团队,Git和Mercurial都提供了这样的功能变基
这些命令可以将一个特性的混乱历史变成一个逻辑变化的更整洁的集合,更适合最终合并到项目的主要分支中。
集中式工具可以提供分布式工具难以实现的策略优势。例如,可以配置一个预提交脚本,该脚本将在引入自动测试套件失败时拒绝尝试的提交。使用分布式工具,这种检查可以放在共享的中央服务器上,但这不能保护开发人员在横向上(从一台膝上型电脑到另一台膝上型电脑)共享无意破坏的更改。
廉价的本地提交的可用性使得使用分布式工具的快速风格开发具有吸引力。假设Alice正在进行一项复杂的更改,她决定对一段代码进行投机性重构。使用分布式工具,她可以按原样提交她的更改,而不必过多担心项目是否处于正常状态,并尝试她的推测性更改。如果试验失败,她可以将其还原并继续执行,最终使用rebase命令消除在她弄清楚自己在做什么时所执行的一些未完成的提交。
虽然这种风格的开发在Subversion中当然是可能的,但经验表明它在分布式工具中更为常见。我的猜想是,开发人员笔记本电脑上分支的私密性,加上分布式工具的即时响应能力,以某种方式结合起来,鼓励更积极和更广泛地使用修订控制。
我在合并中也观察到类似的效果。因为它们是使用分布式工具的基本活动,所以在许多项目中,它们比使用集中式工具出现的频率要高得多。尽管所有的合并都需要付出努力并承担风险,但当分支合并更频繁时,合并就更小,危险也更小。询问任何经验丰富的开发人员,在几个月的独立工作之后,他们会发现合并工作拖延了很长时间。
在修订控制系统的发展过程中,我们绝不接近道路的尽头。学术界对这一领域的关注时断时续。在它的正式基础上还有很多工作可以做,这可以为开发人员提供更强大、更安全的合作方式。唉,在过去的十年里,我只知道一篇关于这个话题的著名出版物。1作为一个简单的例子,当合并潜在的冲突变更时,几乎每个人都使用三向合并(这已经有几十年的历史了)或未发布的特别方法(在这种方法中没有什么自信的理由)。
更实际的是,分布式工具在处理具有深厚历史的大型项目的方式上有很多进步,因为涉及的数据量很大,它们不适合这样做。对于在保证和安全方面有敏感需求的组织,集中式工具比分布式工具做得好一些,但两者都可以大幅改进。
选择修订控制系统是一个绝对答案少得惊人的问题。需要考虑的基本问题是您的团队使用何种类型的数据,以及您希望团队成员如何进行交互。如果您有大量频繁编辑的二进制数据,分布式修订控制系统可能根本不适合您的需要。如果敏捷性、创新和远程工作对您很重要,那么分布式系统更有可能满足您的需求;相比之下,集中式系统可能会降低团队的速度。
还有许多二级考虑。例如,防火墙管理可能是一个问题:Mercurial和Subversion在HTTP和SSL(安全套接字层)上工作得很好,但Git在HTTP上非常慢。为了安全起见,Subversion提供了对单个文件级别的访问控制,但Mercurial和Git没有。为了便于学习和使用,Mercurial和Subversion具有彼此相似的简单命令集(简化从一个到另一个的转换),而Git暴露了潜在的大量复杂性。当涉及到与构建工具、bug数据库等的集成时,这三者都很容易编写脚本。许多软件开发工具已经支持其中一个或多个工具的插件。
考虑到可移植性、简单性和性能方面的需求,我通常会在新项目中选择Mercurial,但具有不同需求或偏好的开发人员或团队可以合理地选择其中任何一个,并从长远来看皆大欢喜。我们很幸运,这三个系统之间很容易进行互操作,所以对未知进行试验是简单和无风险的。
我要感谢Bryan Cantrill、Eric Kow、Ben Collins-Sussman和Brendan Cully对本文草稿的反馈。
相关文章
在queue.acm.org
与史蒂夫·伯恩、埃里克·奥尔曼和布莱恩·坎特里尔的对话
http://queue.acm.org/detail.cfm?id=1454460
分布式开发:经验教训
迈克尔Turnlund
http://queue.acm.org/detail.cfm?id=966801
Kode Vicious再次出击
http://queue.acm.org/detail.cfm?id=1036484
1.Löh, A., swerstra, W., Leijen, D.版本控制的原则性方法,2007;http://people.cs.uu.nl/andres/VersionControl.html.
©2009 acm 0001-0782/09/0900 $10.00
允许为个人或课堂使用本作品的全部或部分制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或付费。
数字图书馆是由计算机协会出版的。版权所有©2009 ACM, Inc.
没有找到条目