当今IT界,VCS版本控制系统的的使用已经成了一种日常不可或缺部分。不光是程序员、IT界甚至在其他领域也是逐渐了有了共识。通过Github写书也不再是天方夜谭或者技术段子,而是实实在在天天发生的事情。本文,我们回顾下历史上主要版本控制系统(VCS)的发展过程,涉及了内容有:SCSS、RCS、CVS、SVN,Git和Mercurial。
概述
回顾VCS的发展历程,总体上可以划分为三个阶段。
第一代VCS,包括SCSS和RCS。立足于对单个文件变化的跟踪,检出的文件一次只能由一个用户在本地进行编辑,用户通过自己的帐户登录到同一共享Unix主机方式实现。
第二代VCS,包括CVS和SVN。通过引入网络,从而形成了包含正式意义上的项目版本的集中式版本存储库。相比第一代VSC,有了实质性的发展,可以供多个用户同时检出并使用代码,但是他们都需要重新提交到同一中央存储库。存在的问题是严重依赖于中央存储库,对网络和实时性同步要求很大。
第三代VSC,包括Git和Mercurial。V到现在发展成为了分布式VCS。在分布式VCS中,创建存储库的所有副本都是相同的,无需一个集中的中央存储库。无需通过网络实时同步内容,只需本地创建提交,分支和合并打开了路径,在合适时候再推送到远端库。
VCS重要软件发展历史时间表:
SCCS第一代版本管理系统
SCCS(Source Code Control System)是最早创建的VCS工具。它由贝尔实验室的Marc Rochkind于1972年用C开发。SCCS旨在解决源文件修订跟踪的问题。此外,它还解决程序bug错误定位的问题。SCCS是现代VCS的鼻祖,至此后VCS发展30年到现在茁壮发展。
功能
和大多数现代的VCS一样,SCCS支持一组命令,供开发人员做文件版本控制。主要实现的功能有:
检入文件以使用SCCS跟踪其历史记录;
检出特定的文件修订以供审核或编译;
检出特定的文件修订以进行编辑;
检入新文件修订以及说明更改的注释;
还原检出文件中所做的更改;
基本分支和变更合并;
提供文件修订历史记录。
技术实现
当添加文件到SCCS进行跟踪时,会创建一种称为s文件或历史文件特殊类型的文件。该文件使用以s开头的原始文件名来命名,存储在名为SCCS的子目录中。比如,一个名为test.txt的源文件将在./SCCS/目录中创建一个名为s.test.txt的历史文件。创建后,历史记录文件将包含原始文件的初始内容以及一些元数据以帮助进行版本跟踪。文件校验和存储在历史记录文件中,以验证内容是否遭到篡改。历史记录文件的内容未经过压缩或编码。由于原始文件的内容存储在历史记录文件中,因此可以将检出到到工作目录进行查看,编译或编辑。可以将对文件所做的进一步更改(例如,行添加,修改和删除)检入到历史文件中,并增加修订号。
SCCS检入仅存储增量或文件更改,而不是每次存储整个文件内容。这样可以减小历史记录文件的大小。每次检入时,增量都存储在历史记录文件内部的称为增量表的结构中。如前所述,实际文件内容或多或少是逐字复制的,带有特殊的控制序列,用于标记已添加和已删除内容的各个部分的开头和结尾。由于SCCS历史记录文件不使用压缩,因此它们通常比要跟踪的实际文件大。
SCCS使用一种称为交错增量的增量方法,支持恒定时间检出,而不管检出的修订版有多老。即较旧的修订版检出所花费的时间不会比新的修订版更长。
需要注意的重要一件事是,所有文件都在SCCS中被单独跟踪和检入。没有办法将更改作为一个原子单位的一部分检入多个文件(和Git提交一样)。每个跟踪的文件都有一个对应的历史文件,用于存储其修订历史。
当检出文件以在SCCS中进行编辑时,为了防止更改被其他用户覆盖,文件上将放置一个锁,但是这会限制多用户的编辑从而开发效率。
SCCS支持可以在特定文件内存储更改序列的分支。分支可以与原始版本合并回去,也可以与同一父级的其他分支版本合并。
基本命令
以下是最常见的SCCS命令的列表。
sccs create <filename.ext>:将新文件检入SCCS并为其创建新的历史记录文件(默认在./SCCS/目录中)。
sccs get <filename.ext>:从相应的历史文件中检出文件,并以只读模式将其放置在工作目录中。
sccs edit <filename.ext>:从相应的历史文件中检出文件进行编辑。锁定历史记录文件,以便其他用户无法修改它。
sccs delta <filename.ext>:检入对指定文件的修改。将提示填写commit,将更改存储在历史记录文件中,然后删除锁。
sccs prt <filename.ext>:显示跟踪文件的修订日志。
sccs diffs <filename.ext>:显示文件的当前工作副本与检出时文件状态之间的差异。
版本文件
一个SCCS历史记录文件示例:
RCS版本管理系统
RCS(Revision Control System)由Walter Tichy于1982年用C编写,用以替代SCCS,SCCS当时还不是开源的。
功能
RCS与它的前任SCCS有许多共同之处,包括:
逐个文件处理修订;
跨多个文件的更改不能一起归类为原子提交;
跟踪文件旨在一次由一个用户修改;
没有网络功能;
每个跟踪文件的修订都存储在相应的历史文件中;
基本分支和修订在单个文件中的合并。
技术实现
当将文件检入RCS时,会在当前目录下的./RCS/目录中创建相应的历史文件。该文件后缀有.v,比如,test.txt的文件将将创建test.txt.v的跟踪文件。
RCS使用反向增量方案来存储文件更改。检入文件后,文件内容的完整快照将存储在历史记录文件中。修改文件并再次检入后,将根据现有历史文件内容计算增量。旧的快照将被丢弃,新的快照将被保存,并与增量一起恢复到较早的状态。之所以称为反向增量,是因为要检出旧版本,RCS需要从文件的最新版本开始并应用连续的增量,直到达到旧版本为止。由于始终可以使用当前修订的完整快照,因此该方法可以非常快速地检出当前修改。但是,检出版本越老,检出花费的时间越长,因为需要针对当前快照计算越来越多的增量。
相比较SCCS花费相同的时间来获取任何修改。RCS历史记录文件中没有存储校验和,因此无法确保文件完整性。
基本命令
以下是最常见的RCS命令的列表:
ci <filename.ext>:将一个新文件检入RCS并为其创建一个新的历史记录文件(默认在./RCS/目录中)。
co <filename.ext>:从相应的历史文件中检出文件,并以只读模式将其放置在工作目录中。
co -l <filename.ext>:从相应的历史文件中检出文件以进行编辑。锁定历史记录文件,以便其他用户无法修改它。
ci <filename.ext>:检入文件更改并在其对应的历史文件中为其创建一个新修订。
merge <file-to-merge-into.ext> <parent.ext> <file-to-merge-from.ext>:合并更改来自同一父文件的两个修改后的子对象。
rcsdiff <filename.ext>:显示文件的当前工作副本与检出时文件状态之间的差异。
rcsclean:删除没有锁的工作文件。
RCS更多操作介绍,详见GNU RCS说明手册。
版本文件
RCS .v历史记录文件样例
CVS第二代版本管理系统
CVS(Concurrent Versions System)由Dick Grune于1986年创建,主要是在第一代单机版本控制工具基础上添加了网络,使其网络化协作化。CVS也是用C语言编写的。CVS开启了VCS发展的第二个里程碑,开启了第二代VCS工具的发展。CVS的网络化使地理上分散的开发团队可以协同开发。
功能
CVS采用C/S架构体系,其代码都都存储在服务器端,开发者需从服务器上获得一份代码复制到本机,然后开发。开发者可随时将新代码提交给服务器,也可以通过更新操作获得最新的代码,保持与其他开发者的一致。CVS提供了一组用于与项目中的文件进行交互的命令,但是使用RCS历史文件格式和后台命令。
VCS历史上,CVS首次允许多个开发人员检出并同时处理相同的文件。CVS在处理多人同时修改页面时,采用"先允许修改,再处理冲突"。
检出:用来下载文件和建立服务器和本机目录之间的对应关系。不会修改本机已有文件的"只读"属性;
Export输出:用来下载服务器文件到本机,从而进行软件的编译发布;
更新:用于获取当前最新版本,也可以用于获取某个特定版本;
Edit编辑:仅用来通知服务器,要编辑某个文件。
unedit:仅用来通知服务器,完成某个文件的编辑了,同时将本机文件置为"只读";
Watch:实现监视协作者edit文件状态的情况,要实现这种监控,要求所有人在自己本机修改文件之前,都edit一下,并在commit之后,unedit一下,别人才能收到通知;
Commit:类似于检入,不同的是,其不修改本机文件的"只读"属性,commit后你仍然可以继续修改本机文件,必须unedit后文件属性才改完"只读"。
技术实现
CVS通过使用集中式存储库模型,第一步是使用CVS在远程服务器上建立集中式存储库。然后就可以将项目导入到存储库中,将项目导入CVS后,每个文件都被转化为.v历史文件,并存储在被称为模块的中央目录中。该存储库通常位于可通过本地网络或Internet访问的远程服务器上。
开发人员通过检出该模块的副本,并复制到本地工作目录中。在此过程中不会文件被锁定,因此可以同时无限制的进行文件检出。开发人员可以修改检出的文件并根据需要提交更改。如果开发人员要提交更改,则其他开发人员将需要在提交更改之前先通过自动合并过程更新其工作副本。必要时候,需要先解决合并冲突。 CVS还提供了创建和合并分支的功能。
基本命令
export CVSROOT=<path/to/repository>:设置CVS存储库根目录,无需在每个命令中都指定它。
cvs import -m 'Import module' <module-name> <vendor-tag> <release-tag>:将文件目录导入CVS模块。
cvs checkout <module-name>:将模块复制到工作目录。
cvs commit <filename.ext>:将更改的文件提交回模块。
cvs add <filename.txt>:添加一个新文件以跟踪修订。
cvs update:通过合并远程存储库中存在的已提交更改而不是工作副本来更新工作目录。
cvs status:显示有关已检出模块工作副本的常规信息。
cvs tag <tag-name> <files>:将识别标记添加到单个文件或一组文件中。
cvs tag -b <new-branch-name>:在存储库中创建一个新分支(必须先检出,然后在本地进行操作)。
cvs checkout -r <branch-name>:将现有分支检出到工作目录。
cvs update -j <branch-to-merge>:将现有分支合并到本地工作副本中。
更多CVS操作,详见GNU CVS手册。
版本文件
CVS历史记录文件示例:
SVN,集大成者
Subversion由Collabnet公司在2000年创建,后交由Apache 软件基金会维护。SVN也是用C编写的,用于改善CVS,实现更强大的集中式解决方案。时至当下,仍有大量的公司依赖于SVN实现其项目管理。
技术实现
和CVS一样,SVN也使用集中式存储库模型。远程用户必须依赖网络来实现连接才能将其更改提交到中央存储库。
Subversion引入了原子提交的功能,确保提交将完全成功,或者在发生问题时被完全放弃。在CVS中,如果提交操作中途失败(例如,由于网络中断),则存储库可能损坏和不一致的状态。
Subversion中的提交或修订可以包含多个文件和目录。这样可以允许用户以项目为单位的跟踪相关更改集,而无需分别跟踪每个文件的更改。
Subversion用于跟踪文件的存储模型称为FSFS(File System atop the File System),使用与运行的操作系统文件系统相匹配的文件和目录结构来创建其数据库结构。Subversion文件系统的独特之处在于,它不仅可以跟踪其包含的文件和目录,还可以跟踪这些文件和目录的不同版本,并且它们会随着时间变化。它是一个具有附加时间维度的文件系统。
Subversion以文件夹为基本管理单位。可以在Subversion中提交空文件夹,而在其它(甚至是Git)VCS中,无法管理空文件夹。
创建Subversion存储库后,将创建一个空的文件和文件夹数据库作为其一部分。将创建一个名为db/revs的目录,其中存储了已检入(已提交)文件的所有修订跟踪信息。每次提交(可以包括对多个文件的更改)都存储在revs目录中的新文件中,并以从1开始的顺序数字标识符命名。当首次提交文件时,将存储其全部内容。为了节省空间,同一文件的在次提交时候将仅存储变化部分,也称为diffs或deltas。
另外,SVN还使用lz4或zlib压缩算法压缩增量,以进一步减小其大小。
尽管每次都存储文件增量而不是整个文件确实节省了存储空间,但由于需要将所有增量捆绑在一起以重新创建文件的当前状态,因此增加了检出和提交操作的时间。默认情况下,Subversion在存储文件的新完整副本之前,每个文件最多可以存储1023个增量。这样可以实现存储和速度之间的良好平衡。
SVN不使用常规的分支和标记系统。一般的Subversion存储库布局是在根目录中包含三个文件夹:
trunk/
brances/
tags/
trunk文件夹用于应用程序的生产版本。brances文件夹用于保存与各个分支相对应的子文件夹。tags文件夹用于保存特定(通常是重要的)项目修订的标签。
基本命令
svn create <path-to-repository>:在指定目录中创建一个新的空存储库。
svn import <path-to-project> <svn-url>:将文件目录导入指定的Subversion存储库路径。
svn checkout <svn-path> <path-to-checkout>:将存储的存储库路径复制到所需的工作目录。
svn commit -m 'Commit message':提交一组更改的文件和文件夹以及描述性的提交消息。
svn add <filename.txt>:添加一个新文件以跟踪修订。
svn update:通过合并svn服务器中存在的已提交更改而不是工作副本来更新工作副本。
svn status:显示工作目录中已更改的跟踪文件的列表。
svn info:显示有关已检出副本的常规详细信息列表。
svn copy <branch-to-copy> <new-branch-path-and-name>:通过复制现有分支来创建一个新分支。
svn switch <existing-branch>:将工作目录切换到现有分支。这将检出指定的分支。
svn merge <existing-branch>:将指定的分支合并到工作目录中检出的当前分支中。请注意,这需要在以后提交。
svn log:显示活动分支的提交历史记录和相关的描述性消息。
版本文件
SVN修订文件示例如下
第三代版本管理系统Git
Git由Linux之父Linus Torvalds于2005年创建用来替代Linux内核开发用的商业的版本管理软件BitKeeper。主要是用C结合一些Shell脚本编写的。由于其功能,灵活性和速度,分布式版本管理、协作性,使得Git成长为一个最出色的VCS软件。由于GitHub,Gitlab等添加的协作性,社交性功能使得Git风靡于世,被广泛使用。
技术实现
Git是分布式VCS,无需集中式的中央存储卡,就可以正常工作。在Git中所有副本都创建为相等,即便是远程Git服务上也是是等价的副本。这是与第二代VCS最明显得差异,第二代集中式的版本管理,必须要依靠中央服务器来提供用户检入和检出。而在Git中开发人员可以本地任意开发,即使连不到远程库也不受任何影响,只需在网络通畅时候,再将变化推送服务器即可。开发人员可以脱机在本地工作,直到准备与他人共享他们的工作为止。此时,可以将更改推送到其他存储库以进行检查,测试或部署。
Blob对象
添加文件以使用Git进行跟踪时,Git使用zlib压缩算法对其进行压缩。使用SHA-1哈希函数对结果进行哈希处理。这将产生一个唯一的哈希值,该值对应于该文件中的内容。Git将其存储在位于隐藏的.git/objects文件夹中的对象数据库中。文件的名称为生成的哈希值,内容为压缩的内容。这些对象文件称为Blob,每次将新文件添加到存储库时会创建Blob对象。
Git实现了一个staging索引,该索引被设计为在提交的中间区域。在准备提交新更改时,它们的压缩内容在特殊的索引文件中被引用,该文件采用树对象的形式。
树对象是另一种Git对象,对应于文件目录,它将blob对象连接到它们的真实文件名,文件许可权和到其他树的链接,并以此方式表示特定文件和目录集的状态。一旦所有相关更改都准备好提交,索引树就可以提交到存储库,该存储库在Git对象数据库中创建一个commit对象。
commit对象保存特定修订的标题树以及提交作者,电子邮件地址,日期和描述性提交消息。每个commit对象还保存对其父提交的引用,因此随着时间的推移,将建立项目开发的历史记录。
如前所述,所有Git对象(Blob,tree和commit)都根据其哈希值进行压缩,哈希处理并存储在对象数据库中。这些被称为松散对象。Git实现中没有通过差异来节省空间,而都是全部内容哈希键索引和压缩镜像,所以,Git非常快,因为每个文件修订版的全部内容都可以作为一个松散的对象来访问。但是,某些操作(例如,将提交推送到远程存储库,存储太多对象或手动运行Git的垃圾收集命令)可能会导致Git将对象重新打包为打包文件,在打包过程中,采用反向差异并进行压缩以消除多余的内容并减小尺寸。该过程将生成包含对象内容的.pack文件,每个文件都有一个对应的.idx索引文件,其中包含对打包对象及其在打包文件中位置的引用。
当将分支推送到远程存储库或从远程存储库拉出分支时,这些打包文件将通过网络传输。提取或获取分支时,将打包文件解压缩以在对象存储库中创建松散对象。
基本命令
git init:将当前目录初始化为Git存储库(创建隐藏的.git文件夹及其内容)。
git clone <git-url>:在指定的URL下载Git存储库的副本。
git add <filename.ext>:将未跟踪的文件或更改的文件添加到暂存区(在对象数据库中创建相应的条目)。
git commit -m '提交消息':提交一组更改的文件和文件夹以及描述性提交消息。
git status:显示与工作目录,当前分支,未跟踪的文件,已修改的文件等状态有关的信息。
git branch <new-branch>:基于当前检出的分支创建一个新分支。
git checkout <branch>:将指定的分支检出到工作目录中。
git merge <branch>:将指定的分支合并到工作目录中检出的当前分支中。
git pull:更新工作c通过合并远程存储库中存在的已提交更改而不是工作副本来进行操作。
git push:将本地活动分支提交的松散对象打包到打包文件中,并传输到远程存储库。
git log:显示活动分支的提交历史记录和相关的描述性消息。
git stash:将工作目录中所有未提交的更改保存到缓存中,以便以后可以检索。
有关Git内部的更多信息,请参阅Pro Git书籍中有关Git内部的章节
版本文件
Git对象文件示例:
Blob对象
.git/objects/a8/420ef73065a9e3e57fe8fd2d32dad28e332bd0
表示哈希值a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
的Blob对象
可以使用:
git cat-file -t a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
查看器对象类型为blob
使用git cat-file -p a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
查看器内容为hello,Chongchong!
Tree对象
.git/objects/ba/0ea2e2c2f9fc822ca16046f8d3f1f24660014c
表示哈希值为ba0ea2e2c2f9fc822ca16046f8d3f1f24660014c
的tree对象,其类型为tree,值为:
040000 tree 887ad439b842a19be9a1922253872427763b0376 hello
Commit对象
.git/objects/da/7a3c9c71eb9da8022018fb5ce02a4625b753d5
表示哈希值为da7a3c9c71eb9da8022018fb5ce02a4625b753d5
的commit对象,其类型为commit,值为
该次commit关联到da7a3c9c71eb9da8022018fb5ce02a4625b753d5
的tree对象,commit消息为init
。
Mercurial 即将死去的另外一个选择
Mercurial由Matt Mackall于2005年创建,采用 Python编写。它也是从托管Linux代码库的目标开始的,是目前次于Git的第二受欢迎的分布式VCS,但使用频率不多。随着最近BitBucket宣布将停止对Mercurial的支持,表示着Mercurial即将死去,除了Git后没有了另外的选择。
技术实现
和Git一样,Mercurial是一个分布式版本控制系统,它允许任何数量的开发人员独立于其他人使用他们自己的项目副本。 Mercurial利用了许多与Git相同的技术,例如压缩和SHA-1哈希,但是采用了不同的方式。
当提交新文件以在Mercurial中进行跟踪时,将在隐藏目录.hg/store/data/中为其创建相应的revlog文件。可以将revlog(或修订日志)文件看作是较旧的VCS(如CVS,RCS和SCCS)使用的历史记录文件的现代化版本。与Git为每个暂存文件的每个版本创建一个新的Blob不同,Mercurial只是在该文件的revlog中创建一个新条目。为了节省空间,每个新条目仅包含先前版本的增量(更改)。一旦达到阈值数量的增量,将再次存储文件的完整快照。在应用许多增量来重建特定文件修订版时,这减少了查找时间。
这些文件修订日志的名称与它们跟踪的文件匹配,但是后缀为.i和.d扩展名。.d文件包含压缩的增量内容。.i文件用作索引,以快速跟踪.d文件中的不同修订版本。
Mercurial使用了另一种类型的修订日志,称为变更日志。更改日志包含条目列表,这些条目将每个提交与以下信息相关联:
Manifest nodeid:标识特定时间存在的完整文件修订集;
父提交节点ID:这使Mercurial可以建立时间表或项目历史记录的分支。根据提交的类型(正常vs合并),存储一个或两个父ID;
提交人;
提交日期;
提交信息;
每个变更日志条目还会生成一个称为其节点ID的哈希。
基本命令
hg init:将当前目录初始化为Mercurial存储库(创建隐藏的.hg文件夹及其内容)。
hg clone <hg-url>:在指定的URL下载Mercurial存储库的副本。
hg add <filename.ext>:添加新文件以进行修订跟踪。
hg commit -m '提交消息':提交一组更改的文件和文件夹以及描述性提交消息。
hg status:显示与工作目录,未跟踪文件,已修改文件等状态有关的信息。
hg update <revision>:将指定的分支检出到工作目录中。
hg merge <branch>:将指定的分支合并到工作目录中检出的当前分支中。
hg pull:从远程存储库下载新修订,但不要将其合并到工作目录中。
hg push:将新修订版本传输到远程存储库。
hg log:显示活动分支的提交历史记录和相关的描述性消息。
版本文件
Mercurial 版本文件示例:
Manifest revlog
hey.txt208b6e0998e8099b16ad0e43f036ec745d58ec04
hi.txt74568dc1a5b9047c8041edd99dd6f566e78d3a42hi.txt74568dc1a5b9047c8041edd99dd6f566e78d3a42
revlog
总结
本文中,我们回顾了VCS版本控制的发展历史,介绍了VCS版本控制系统技术演进过程,对历史上曾经出现的主要VCS软件进行了介绍和比较、还有各自的技术实现以及常见操作命令。