斩白蛇而起
早期(1970年到1980年代)的软件开发大部分是愉快的个人创作。比如UNIX下的sed是L. E. McMahon写的Python的第一个编译器是Guido写的Linux最初的内核是Linus写的 (好吧awk是个例外它的名字是三位作者的首字母但也只是三个人)。这些程序员可以用手工的方式进行备份并以注释或者新建文本文件来记录变动。正如现在普通用户常做的当时的程序员常用cp备份:$cp dev.c dev.bak更有条理一些的程序员会加上一个时间标记比如:$cp dev.c dev.bak.19890908程序员很可能会用vi创建一个LOG文件来做日志:1989-09-08 02:00:00 Old input method is stupid Add command-line input function在一个版本发布的时候程序员可能做一个tar归档将所有的文件归为同一个.tar文件。$tar -cf project_v1.0.tar project上面的工具构成了一套人工VCS。上面的这套组合也非常符合UNIX的模块化理念让每个应用专注于一个小的功能使用者根据需要将这些功能连接起来。你还可以写一个shell脚本将上面的功能都写在里面。当需要的时候调用该脚本就可以了。(这样一个shell脚本并不复杂而且挺有用的可以作为学习shell编程的小练习)再说一下早期的合作开发模式。如在Python简史中看到的Guido通过电子邮件接收补丁(patch)并将补丁应用到原来的代码文件。实际上一个补丁(patch)的主要功能是描述两个文件的改变(change, or file delta)。 假设我们有两个文件a.c和b.c内容分别为:a.c (有bug的代码)int sum(int a, int b) { int c; c a 1; return c; }b.c (修正后的代码)int sum(int a, int b) { int c; c a b; return c; }在UNIX系统下运行$diff a b iss01.patchiss01.patch就是一个补丁文件它看起来如下4c4 c a 1; --- c a b;这个补丁表示更改原文件第四行的c a 1;改为c a b;更改后的这一行位于新的文件的第四行。使用patch命令将iss01.patch应用到a.c上相当于将 b.c-a.c 的改变作用在a上a.c将和b.c有一样的内容$patch a.c iss01.patch当我发现a.c的代码有错误时可以将我修改后的b.c与原来的a.c做diff获得补丁文件并将补丁发给Guido并告诉他该补丁是为了修正a.c代码中的加法错误。Guido确认之后就可以使用patch应用该补丁了。在后面我们将看到这种diff-patch的工作方式被VCS不同程度的采用。东汉末年早在70年代末80年代初VCS的概念已经存在比如UNIX平台的RCS (Revision Control System)。RCS是由Walter F. Tichy使用C开发。RCS对文件进行集中式管理主要目的是避免多人合作情况下可能出现的冲突。如果多用户同时写入同一个文件其写入结果可能相互混合和覆盖从而造成结果的混乱。你可以将文件交给RCS管理。RCS允许多个用户同时读取文件但只允许一个用户锁定(locking)并写入文件 (类似于多线程的mutex)。这样当一个程序员登出(check-out见RCS的co命令)某个文件并对文件进行修改的时候。只有在这个程序完成修改并登入(check-in见RCS的ci命令)文件时其他程序员才能登出文件。基本上RCS用户所需要的就是co和ci两个命令。在co和ci之间用户可以对原文件进行许多改变(change, or file delta)。一旦重新登入文件这些改变将保存到RCS系统中。通过check-in将改变永久化的过程叫做提交(commit)。RCS互斥写入RCS的互斥写入机制避免了多人同时修改同一个文件的可能但代价是程序员长时间的等待给团队合作带来不便。如果某个程序员登出了某个文件而忘记登入那他就要面对队友的怒火了。(从这个角度上来说RCS造成的问题甚至大于它所解决的问题……)文件每次commit都会创造一个新的版本(revision)。RCS给每个文件创建了一个追踪文档来记录版本的历史。这个文档的名字通常是原文件名加后缀,v (比如main.c的追踪文档为main.c,v)。追踪文档中包括最新版本的文件内容每次check-in的发生时间和用户每次check-in发生的改变。在最新文档内容的基础上减去历史上发生的改变就可以恢复到之前的历史版本。这样RCS就实现了备份历史和记录改变的功能。RCS历史版本追踪相对与后来的版本管理软件RCS纯粹线性的开发方式非常不利于团队合作。但RCS为多用户写入冲突提供了一种有效的解决方案。RCS的版本管理功能逐渐被其他软件(比如CVS)取代但时至今日它依然是常用的系统管理工具。RCS就像是东汉王室飘摇多年而不倒。挟天子令诸侯1986年Dick Grune写了一系列的shell脚本用于版本管理并最终以这些脚本为基础构成了CVS (Concurrent Versions System)。CVS后来用C语言重写。CVS是开源软件。在当时Stallman刚刚举起GNU的大旗掀起开源允许的序幕。CVS被包含在GNU的软件包中并因此得到广泛的推广最终击败诸多商业版本的VCS呈一统天下之势。CVS继承了RCS的集中管理的理念。在CVS管理下的文件构成一个库(repository)。与RCS的锁定文件模式不同CVS采用复制-修改-合并(copy-modify-merge)的模式来实现多线开发。CVS引进了分支(branch)的概念。多个用户可以从主干(也就是中心库)创建分支。分支是主干文件在本地复制的副本。用户对本地副本进行修改。用户可以在分支提交(commit)多次修改。用户在分支的工作结束之后需要将分支合并到主干中以便让其他人看到自己的改动。所谓的合并就是CVS将分支上发生的变化应用到主干的原文件上。比如下面的过程中我们从r1.1分支出rb1.1.2.*并最终合并回主干构成r1.2copy-modify-mergeCVS与RCS类似使用,v文件记录改变以便追踪历史。在合并的过程中CVS将两个change应用于r1.1就得到了r1.2:r1.2 r1.1 change(rb1.1.2.2 - rb1.1.2.1) change(rb1.1.2.1-r1.1)上面的两个改变都记录在,v文件中所以很容易提取。在多用户情况下可以创建多个分支进行开发比如在这样的多分支合并的情况下有可能出现冲突(colliding)。比如上图中第一次合并和第二次合并都对r1.1文件的同一行进行了修改那么r1.3将不知道如何去修改这一行 (第二次合并比图示的要更复杂一些分支需要先将主干拉到本地合并过之后传回主干但这一细节并不影响我们这里的讨论)。CVS要求冲突发生时的用户手动解决冲突。用户可以调用编辑器对文件发生合并冲突的地方进行修改以决定最终版本(r1.3)的内容。CVS管理下的每个文件都有一系列独立的版本号(比如上面的r1.1,r1.2,r1.3)。但每个项目中往往包含有许多文件。CVS用标签(tag)来记录一个集合这个集合中的元素是一对(文件名版本号)。比如我们的项目中有三个文件(file1, file2, file3)我们创建一个v1.0的标签tag v1.0 (file1:r1.3) (file2:r1.1) (file3:r1.5)v1.0的tag中包括了r1.3版本的文件file1r1.1版本的file2…… 一个项目在发布(release)的时候往往要发布多个文件。标签可以用来记录该次发布的时候是哪些版本的文件被发布。CVS应用在许多重要的开源项目上。在90年代和00年代初CVS在开源世界几乎不二选择 (RCS也是开源的但正如我们已经提到的RCS无法与CVS媲美)。CVS就像是官渡之战后的曹魏挟开源运动号令天下。时至今天尽管CVS已经长达数年没有发布新版本我们依然可以在许多项目中看到CVS的身影。青出于蓝正如曹操的统治富有争议一样(比如非汉祚以臣欺君等等)CVS也有许多常常被人诟病的地方比如下面几条合并不是原子操作(atomic operation)如果有两个用户同时合并那么合并结果将是某种错乱的混合体。如果合并的过程中取消合并不能撤销已经应用的改变。文件的附加信息没有被追踪一旦纳入CVS的管理文件的附加信息(比如上次读取时间)就被固定了。CVS不追踪它所管理文件的附加信息的变化。主要用于管理ASCII文件不能方便的管理Binary文件和Unicode文件分支与合并需要耗费大量的时间CVS的分支和合并非常昂贵。分支需要复制合并需要计算所有的改变并应用到主干。因此CVS鼓励尽早合并分支。CVS还有其它一些富有争议的地方。随着时间人们对CVS的一些问题越来越感到不满 (而且程序员喜欢新鲜的东西)Subversion应运而生。Subversion的开发者Karl Fogel和Jim Blandy是长期的CVS用户。赞助开发的CollabNet, Inc.希望他们写一个CVS的替代VCS。这个VCS应该有类似于CVS的工作方式但对CVS的缺陷进行改进并提供一些CVS缺失的功能。这就好像刘备从曹营拉出来单干的刘备一样。总体上说Subversion在许多方面沿袭CVS也是集中管理库通过记录改变来追踪历史允许分支和合并但并不鼓励过多分支。Subversion在一些方面得到改善。Subversion的合并是原子操作。它可以追踪文件的附加信息并能够同样的管理Binary和Unicode文件。但CVS和Subversion又有许多不同与CVS的,v文件存储模式不同Subversion采用关系型数据库来存储改变集。VCS相关数据变得不透明。CVS中的版本是针对某个文件的CVS中每次commit生成一个文件的新版本。Subversion中的版本是针对整个文件系统的(包含多个文件以及文件组织方式)每次commit生成一个整个项目文件系统树的新版本。Subversion依赖类似于硬连接(hard link)的方式来提高效率避免过多的复制文件本身。Subversion不会从库下载整个主干到本地而只是下载主干的最新版本。在Subversion刚刚诞生的时候来自CVS用户的抱怨不断。他们觉得在Subversion中有太多的改动有些改动甚至是相对于CVS的倒退。比如CVS中的tag在Subversion中被改为直接复制版本的文件系统树到一个特殊的文件夹。然而随着时间的推移Subversion逐渐推广 (Subversion已经是Apache中自带的一个模块了Subversion应用于GCC、SourceForge新浪APP Engine等项目)并依然有活跃的开发而CVS则逐渐沉寂。事实上许多UNIX的参考书的新版本中都缩减甚至删除了CVS的内容。别开生面CVS和Subversion有很多不同的地方。但如果将这两者和git比较那么git看起来就像孙权的碧眼有一些怪异。git的作者是Linus Torvald。对就是写Linux Kernel的那个Linus Torvald。Linus在贡献了最初的Linux Kernel源代码之后一直领导着Linux Kernel的开发。Linus Torvald本人相当厌恶CVS(以及Subversion)。然而操作系统内核是复杂而庞大的代码“怪兽” 2012年的Linux Kernel有1500万行代码Windows的代码不公开估计远远超过这一数目。Linux内核小组最初使用.tar文件来管理内核代码但这远远无法匹配Linux内核代码的增长速度。Linus转而使用BitKeeper作为开发的VCS工具。BitKeeper是一款分布式的VCS工具它可以快速的进行分支和合并。然而由于使用证书方面的争议(BitKeeper是闭源软件但给Linux内核开发人员发放免费的使用证书)Linus最终决定写一款开源的分布式VCS软件git。git在英文中比喻一个愚蠢或者不愉快的人(a stupid or unpleasant person)。Linus说这个比喻是在说自己Im an egotistical bastard, and I name all my projects after myself. First Linux, now git.(这里Linus似乎并不是在贬低自己见Linus和Eric S. Raymond的争论: The curse of the gifted)对于一个开发项目git会保存blob, tree, commit和tag四种对象。文件被保存为blob对象。文件夹被保存为tree对象。tree对象保存有指向文件或者其他tree对象指针。上面两个对象类似于一个UNIX的文件系统构成了一个文件系统树。一个commit对象代表了某次提交它保存有修改人修改时间和附加信息并指向一个文件树。这一点与Subversion类似即每次提交为一个文件系统树。一个tag对象包含有tag的名字并指向一个commit对象。