第1章 版本控制的前世和今生
除了茫然未知的宇宙,几乎任何事物都是从无到有,从简陋到完善。随着时间车轮的滚滚向前,历史被抛在身后逐渐远去,如同我们的现代社会,世界大同,到处都是忙碌和喧嚣,再也看不到已经远去的刀耕火种、男耕女织的慢生活岁月。
版本控制系统是一个另类。虽然其历史并不短暂,也有几十年,但是它的演进进程却一直在社会的各个角落重复着,而且惊人的相似。有的人从未使用甚至从未听说过版本控制系统,他和他的团队就像停留在黑暗的史前时代,任由数据自生自灭。有的人使用着有几十年历史的CVS或其改良版Subversion,让时间空耗在网络连接的等待中。以Git为代表的分布式版本控制系统已经风靡整个开源社区,正等待你的靠近。
1.1 黑暗的史前时代
谈及远古,人们总爱以“黑暗”来形容。黑暗实际上指的是秩序和工具的匮乏,而不是自然。如果以自然环境而论,由于工业化和城市化对环境的破坏,现今才是最黑暗的年代。对于软件开发来说也是如此,在C语言一统天下的日子里我们的选择很简单,如今面临Java、.Net和脚本语言时,我们的选择变得复杂起来,但是从工具和秩序上讲,过去的年代是黑暗的。
回顾一下我经历的版本控制的“史前时代”吧。在大学里,代码分散地拷贝在各个软盘中,最终我被搞糊涂,不知道哪个软盘中的代码是最优的,因为最新并非最优,失败的重构会毁掉原来尚能运作的代码。在我工作的第一年,代码的管理并未得到改善,还是以简单的目录拷贝进行数据的备份,三四个程序员利用文件服务器的共享目录进行协同,公共类库和头文件在操作过程中相互覆盖,痛苦不堪。很明显,那时我尚不知道版本控制系统为何物。我的版本控制史前时代一直延续到2000年,那时CVS已经诞生了14年,而我在那时对CVS还一无所知。
实际上,即便是在CVS出现之前的“史前时代”,也已经有了非常好用的用于源码比较和打补丁的工具:diff和patch,它们今天生命力依然顽强。大名鼎鼎的Linus Torvalds先生(Linux之父)也对这两个工具偏爱有加,在1991~2002年之间,Linus一直顽固地使用diff、patch和tar包管理着Linux的代码,虽然不断有人提醒他有CVS的存在[1]。
那么来看看diff和patch,熟悉它们将对理解版本控制系统(差异存储)和使用版本控制系统(代码比较和冲突解决)都有莫大的好处。
1.用diff命令比较两个文本文件或目录的差异
先来构造两个文件:
对这两个文件执行diff命令,并通过输出重定向,将差异保存在diff.txt文件中。
$diff-u hello world>diff.txt
上面执行diff命令的-u参数很重要,使得差异输出中带有上下文。打开文件diff.txt,会看到其中的差异比较结果。为了说明方便,为每一行增添了行号。
1 —-hello 2010-09-21 17:45:33.551610940+0800
2 +++world 2010-09-21 17:44:46.343610465+0800
3 @@-1,4+1,4@@
4 -应该杜绝文章中的错别子。
5 +应该杜绝文章中的错别字。
6
7 但是无论使用
8 *全拼,双拼
9 @@-6,6+6,7@@
10
11 是人就有可能犯错,软件更是如此。
12
13 -犯了错,就要扣工资!
14 -
15 改正的成本可能会很高。
16 +
17 +但是“只要眼球足够多,所有Bug都好捉”,
18 +这就是开源的哲学之一。
上面的差异文件,可以这么理解:
第1行和第2行分别记录了原始文件和目标文件的文件名及时间戳。以三个减号(—-)开始的行标识的是原始文件,以三个加号(+++)开始的行标识的是目标文件。
在比较内容中,以减号(-)开始的行是只出现在原始文件中的行,例如:第4、13、14行。
在比较内容中,以加号(+)开始的行是只出现在目标文件中的行,例如:第5行和16-18行。
在比较内容中,以空格开始的行,是在原始文件和目标文件中都出现的行,例如:第6-8、10-12和第15行。这些行是用作差异比较的上下文。
第3-8行是第一个差异小节。每个差异小节以一行差异定位语句开始。第3行就是一条差异定位语句,其前后分别用两个@进行标识。
第3行定位语句中-1,4的含义是:本差异小节的内容相当于原始文件的从第1行开始的4行。而第4、6、7、8行是原始文件中的内容,加起来刚好是4行。
第3行定位语句中+1,4的含义是:本差异小节的内容相当于目标文件的从第1行开始的4行。而第5、6、7、8行是目标文件中的内容,加起来刚好是4行。
因为命令diff是用于行比较的,所以即使改正了一个字,也显示为一整行的修改(参见差异文件第4、5行)。Git对diff进行了扩展,并且还提供一种逐词比较的差异比较方法,参见本书第2篇的第11.4.4小节。
第9-18行是第二个差异小节。第9行是一条差异定位语句。
第9行定位语句中-6,6的含义是:本差异小节的内容相当于原始文件的从第6行开始的6行。第10-15行是原始文件中的内容,加起来刚好是6行。
第9行定位语句中+6,7的含义是:本差异小节的内容相当于目标文件的从第6行开始的7行。而第10-12、15-18行是目标文件中的内容,加起来刚好是7行。
2.命令patch相当于diff的反向操作
有了hello和diff.txt文件,可以放心地将world文件删除或用hello文件将world文件覆盖。用下面的命令可以还原world文件:
$cp hello world
$patch world<diff.txt
也可以保留world和diff.txt文件,删除hello文件或用word文件将hello文件覆盖。用下面的命令可以恢复hello文件:
$cp world hello
$patch-R hello<diff.txt
命令diff和patch还可以对目录进行比较操作,这也就是Linus在1991~2002年用于维护Linux不同版本间差异的办法。在没有版本控制系统的情况下,可以用此命令记录并保存改动前后的差异,还可以将差异文件注入版本控制系统(如果有的话)。
标准的diff和patch命令存在一个局限,就是不能对二进制文件进行处理。对二进制文件的修改或添加会在差异文件中缺失,进而丢失对二进制文件的改动或添加。Git对差异文件格式提供了扩展支持,支持二进制文件的比较,解决了这个问题。这一点可以参考本书第7篇第38章的相关内容。
[1]Linus Torvalds于2007年5月3日在Google的演讲:http://www.youtube.com/watch?v=4XpnKHJAok8
[2]此处是故意将“字”写成“子”,以便两个文件进行差异比较。