我们在上一篇文章中已经介绍了Git的一个典型应用,但我们忽略了其中的某些问题与Git的内部机制。下面就在介绍一些Git实现中的基本概念,它主要来自于《Git版本控制管理》第四章节。意料之中的,掌握典型使用总是比掌握设计原理简单的多~~~
Git版本库(repository)只是一个简单的数据库,其中包含所有用来维护与管理项目的修订版本和历史的信息。在Git中,跟大多数VCS一样,一个版本库维护项目整个生命周期的完整副本。然而,Git不仅提供版本库中所有文件的完整副本,还提供版本库本身的副本。
Git在每个版本库里维护一组配置值,如之前设置的版本库的用户名和email地址等。不像文件数据和其他版本库的元数据,在把一个版本库clone或者复制到另一个版本库的时候配置信息是不跟着转移的。相反,Git对每个网站、每个用户、和每个版本的配置和设置信息都进行管理和检查。
在版本库中,Git维护两个主要的数据结构:对象库(Object store)和索引(index)。所有这些版本库数据放在工作目录根目录下的名为.git的隐藏子目录中。
对象库是Git版本库实现的心脏。它包含你的原始数据文件和所有日志消息、作者信息、日期,以及其他用来重建项目任意版本或分支的信息。
Git放在对象库里的对象只有4种类型:块(blob)、目录树(tree)、提交(commit)和标签(tag)。这4种原子对象构成Git高层数据结构的基础。
块(blob)
文件的每一个版本表示为一个块。blob是“binary large object”的缩写,是计算机领域的常用语,用来指代某些可以包含任意数据的变量或文件,同时其内部结构会被程序忽略。一个blob被视为一个黑盒。一个blob保存一个文件的数据,但不包含任何关于这个文件的元数据,甚至连文件名也没有
目录树(tree)
一个目录树对象代表一层目录信息。它记录blob标识符、路径名和在一个目录里所有文件的一些元数据。它可以递归引用其他目录书或子树对象,从而建立一个包含文件和子目录的完整层次结构。
提交(commit)
一个提交对象保存版本库中每一次变化的元数据,包括作者、提交者、提交日期和日志消息。每一个提交对象指向一个目录树对象,这个目录树对象在一张完整的快照中捕获提交时版本库的状态。最初的提交或者根提交是没有父提交的。大多数提交都有一个父提交,但也存在例外。
标签(tag)
一个标签对象分配一个任意的且人类可读的名字给一个特定对象,通常是一个提交对象。虽然85c1c0a43f18923fe32ceb57d55116d42c04d496指的是一个确切且定义好的提交,但是一个更熟悉的标签名(如Ver-1.0)可能会更有意义。
随着时间的推移,所有信息在对象库中会变化和增长,项目的编辑、添加和删除都会被跟踪和建模。为了有效利用磁盘空间和网络宽带,Git把对象压缩并存储在打包文件(pack file)中,这些文件也在对象库里。
索引是一个临时的、动态的二进制文件,它描述整个版本库的目录结构。更具体的说,索引捕获项目在某个时刻的整体结构的一个版本。项目的状态可以用一个提交和一颗目录树表示,它可以来自项目历史中的任意时刻,或者它可以是你正在开发的未来状态。
Git的关键特色之一就是它允许你用有条理的、定义好的步骤来改变索引的内容。索引使得开发的推进与提交的变更能够分离开来。
下面是它的工作原理。作为开发人员,你通过执行Git命令在索引中暂存(stage)变更。变更通常是添加、删除或者编辑某个文件或某些文件。索引会记录和保存那些变更,保障它们的安全直到你准备好提交了。还可以删除或替换索引中的变更。因此,索引支持一个由你主导的从复杂的版本库状态到一个可推测的更好状态的逐步过渡。
Gi对象库被组织及实现成一个内容寻址的存储系统。具体而言,对象库中的每个对象都有一个唯一的名称,这个名称是向对象的内容使用SHA1得到的SHA1散列值。因为一个对象的完整内容决定了这个散列值,并且认为这个散列值能有效 并唯一地对应特定的内容,所以SHA1散列值用来做对象数据库中对象的名字和索引是完全充分的。文件的任何微小变化都会导致SHA1散列值的改变,使得文件的新版本被单独编入索引。
SHA1的值是一个160位的数,通常表示为一个40位的十六进制数,比如85c1c0a43f18923fe32ceb57d55116d42c04d496。有时候 ,在显示期间,SHA1值被简化成一个较小的、唯一的前缀。Git用户所说的SHA1、散列码和对象ID都是指同一个东西。
PS:SHA1散列计算的一个重要特性是不管内容在哪里,它对同样的内容始终产生同样的ID。换言之,在不同目录甚至不同机器中的相同文件内容产生的SHA1哈希ID是完全相同的。因此,文件的SHA1散列ID是一种有效的全局唯一标识符。这里有一个强大的推论,在互联网上,文件或者任意大小的blob都可以通过比较它们的SHA1标识符来判断是否相同。
Git除了是VCS,同时还是一个内容追踪系统。Git的内容追踪主要表现为两种关键的方式。
首先,Git的对象库基于其对象内容的散列计算的值,而不是基于用户原始文件布局的文件名或目录名设置。因此,当Git放置一个文件到对象库中的时候,它基于数据的散列值而不是文件名。事实上,Git并不追踪那些与文件次相关的文件名或目录名。再次强调,Git追踪的是内容而不是文件。
如果两个文件的内容完全一样,无论是否在相同的目录,Git在对象库里只保存一份blob形式的内容副本。如果文件有相同的SHA1值,它们的内容就是相同的。
如果这些文件中的一个发生了变化,Git会为它重新计算一个新的SHA1值,识别出它现在是一个不同的blob对象,然后把这个心的blob对象加到对象库里。原来的blob在对象库里保持不变,为没有变化的文件所使用。
其次,当文件从一个版本到下一个版本的时候,Git的内容数据库有效地存储每个文件的每个版本,而不是它们的差异。因为Git使用一个文件的全部内容的散列值作为文件名,所以它必须对每个文件的完整副本进行操作。Git不能将工作或者对象库条目建立在文件内容的一部分或者文件的两个版本之间的差异上。
Git需要维护一个明确的文件列表来组成版本库的内容。然而,这个需求并不需要Git的列表基于文件名。实际上,Git把文件名视为一段区别于文件内容的数据。文件名和目录名来自底层的文件系统,但是Git并不真正关心这些名字。Git仅仅记录每个路径名,并且确保通过它的内容精确地重建文件和目录,这些都是由散列值来索引的。
Git使用了一种叫做打包文件(pack file)的高效的存储机制。要创建一个打包文件,Git首先定位内容非常相似的全部文件,然后为它们之一存储整个内容。之后计算相似文件之间的差异并且只保存差异。例如,如果你只是更改或添加文件中的一行,Git可能会存储新版本的全部内容,然后记录那一行更改作为差异,并储存在包里。
存储一个文件的整个版本用来构造其他版本的相似文件的差异是一个被广泛使用的伎俩了。然而,Git文件打包的 非常巧妙。因为Git是内容驱动的,所以它并不真正关心它计算出来的两个文件之间的差异是否属于同一个文件的两个版本。这就是说,Git可以在版本库里的任何地方取出两个文件的差异,只要它认为它们足够相似来产生良好的数据压缩。因此,Git有一套相当复杂的算法来定位和匹配版本库中潜在的全局候选差异。此外,Git可构造一些列差异文件,从一个文件的一个版本到第二个、第三个,等等。Git还维护打包文件表示每个完整文件(包括完整内容的文件和通过差异重建出来的文件)的原始blob的SHA1值。这给定位包内对象的索引机制提供了基础。打包文件跟对象库中其他对象存储在一起。它们也用于网络中版本库的高效数据传输。