本节书摘来自异步社区《Git版本控制管理(第2版)》一书中的第4章,第4.1节,作者:【美】Jon Loeliger , Matthew McCullough著,更多章节内容可以访问云栖社区“异步社区”公众号查看
第4章 基本的Git概念
4.1 基本概念
前一章介绍了Git的一个典型应用,并且可能引发了相当多的问题。Git是否在每次提交时存储整个文件?.git目录的目的是什么?为什么一个提交ID像乱码?我应该注意它吗?
如果你用过其他VCS,比如SVN或者CVS,那么对最后一章的命令可能会很熟悉。事实上,你对一个现代VCS期望的所有操作和功能,Git都能提供。然而,在一些基本的和意想不到的方面,Git会有所不同。
本章会通过讨论Git的关键架构组成和一些重要概念来探讨Git的不同之处和原因。这里注重基础知识并且演示如何与一个版本库交互;第12章会介绍如何操作很多关联的版本库。追踪多个版本库可能看起来是个艰巨的任务,但是你在本章学到的基本原则是一样适用的。
4.1.1 版本库
Git版本库(repository)只是一个简单的数据库,其中包含所有用来维护与管理项目的修订版本和历史的信息。在Git中,跟大多数版本控制系统一样,一个版本库维护项目整个生命周期的完整副本。然而,不同于其他大多数VCS,Git版本库不仅仅提供版本库中所有文件的完整副本,还提供版本库本身的副本。
Git在每个版本库里维护一组配置值。在前面的章节你已经见过其中的一些了,比如,版本库的用户名和email地址。不像文件数据和其他版本库的元数据,在把一个版本库克隆(clone)或者复制到另一个版本库的时候配置设置是不跟着转移的。相反,Git对每个网站、每个用户和每个版本库的配置和设置信息都进行管理与检查。
在版本库中,Git维护两个主要的数据结构:对象库(object store)和索引(index)。所有这些版本库数据存放在工作目录根目录下一个名为.git的隐藏子目录中。
对象库在复制操作的时候能进行有效复制,这也是用来支持完全分布式VCS的一种技术。索引是暂时的信息,对版本库来说是私有的,并且可以在需要的时候按需求进行创建和修改。
接下来的两节将对对象库和索引进行更详细的描述。
4.1.2 Git对象类型
对象库是Git版本库实现的心脏。它包含你的原始数据文件和所有日志消息、作者信息、日期,以及其他用来重建项目任意版本或分支的信息。
Git放在对象库里的对象只有4种类型:块(blob)、目录树(tree)、提交(commit)和标签(tag)。这4种原子对象构成Git高层数据结构的基础。
块(blob)
文件的每一个版本表示为一个块(blob)。blob是“二进制大对象”(binary large object)的缩写,是计算机领域的常用术语,用来指代某些可以包含任意数据的变量或文件,同时其内部结构会被程序忽略。一个blob被视为一个黑盒。一个blob保存一个文件的数据,但不包含任何关于这个文件的元数据,甚至连文件名也没有。
目录树(tree)
一个目录树(tree)对象代表一层目录信息。它记录blob标识符、路径名和在一个目录里所有文件的一些元数据。它也可以递归引用其他目录树或子树对象,从而建立一个包含文件和子目录的完整层次结构。
提交(commit)
一个提交(commit)对象保存版本库中每一次变化的元数据,包括作者、提交者、提交日期和日志消息。每一个提交对象指向一个目录树对象,这个目录树对象在一张完整的快照中捕获提交时版本库的状态。最初的提交或者根提交(root commit)是没有父提交的。大多数提交都有一个父提交,虽然本书后面(第9章)会介绍一个提交如何引用多个父提交。
标签(tag)
一个标签对象分配一个任意的且人类可读的名字给一个特定对象,通常是一个提交对象。虽然9da581d910c9c4ac93557ca4859e767f5caf5169指的是一个确切且定义好的提交,但是一个更熟悉的标签名(如Ver-1.0-Alpha)可能会更有意义!
随着时间的推移,所有信息在对象库中会变化和增长,项目的编辑、添加和删除都会被跟踪和建模。为了有效地利用磁盘空间和网络带宽,Git把对象压缩并存储在打包文件(pack file)里,这些文件也在对象库里。
4.1.3 索引
索引是一个临时的、动态的二进制文件,它描述整个版本库的目录结构。更具体地说,索引捕获项目在某个时刻的整体结构的一个版本。项目的状态可以用一个提交和一棵目录树表示,它可以来自项目历史中的任意时刻,或者它可以是你正在开发的未来状态。
Git的关键特色之一就是它允许你用有条理的、定义好的步骤来改变索引的内容。索引使得开发的推进与提交的变更之间能够分离开来。
下面是它的工作原理。作为开发人员,你通过执行Git命令在索引中暂存(stage)变更。变更通常是添加、删除或者编辑某个文件或某些文件。索引会记录和保存那些变更,保障它们的安全直到你准备好提交了。还可以删除或替换索引中的变更。因此,索引支持一个由你主导的从复杂的版本库状态到一个可推测的更好状态的逐步过渡。
在第9章中,你会看到索引在合并(merge),允许管理、检查和同时操作同一个文件的多个版本中起到的重要作用。
4.1.4 可寻址内容名称
Git对象库被组织及实现成一个内容寻址的存储系统。具体而言,对象库中的每个对象都有一个唯一的名称,这个名称是向对象的内容应用SHA1得到的SHA1散列值。因为一个对象的完整内容决定了这个散列值,并且认为这个散列值能有效并唯一地对应特定的内容,所以SHA1散列值用来做对象数据库中对象的名字和索引是完全充分的。文件的任何微小变化都会导致SHA1散列值的改变,使得文件的新版本被单独编入索引。
SHA1的值是一个160位的数,通常表示为一个40位的十六进制数,比如,9da581d910c9c4ac93557ca4859e767f5caf5169。有时候,在显示期间,SHA1值被简化成一个较小的、唯一的前缀。Git用户所说的SHA1、散列码和对象ID都是指同一个东西。
全局唯一标识符
SHA散列计算的一个重要特性是不管内容在哪里,它对同样的内容始终产生同样的ID。换言之,在不同目录里甚至不同机器中的相同文件内容产生的SHA1哈希ID是完全相同的。因此,文件的SHA1散列ID是一种有效的全局唯一标识符。
这里有一个强大的推论,在互联网上,文件或者任意大小的blob都可以通过仅比较它们的SHA1标识符来判断是否相同。
4.1.5 Git追踪内容
理解Git不仅仅是一个VCS是很重要的,Git同时还是一个内容追踪系统(content tracking system)。这种区别尽管很微小,但是指导了Git的很多设计,并且也许这就是处理内部数据操作相对容易的关键原因。然而,因为这也可能是对新手来讲最难把握的概念之一,所以做一些论述是值得的。
Git的内容追踪主要表现为两种关键的方式,这两种方式与大多数其他①修订版本控制系统都不一样。
首先,Git的对象库基于其对象内容的散列计算的值,而不是基于用户原始文件布局的文件名或目录名设置。因此,当Git放置一个文件到对象库中的时候,它基于数据的散列值而不是文件名。事实上,Git并不追踪那些与文件次相关的文件名或者目录名。再次强调,Git追踪的是内容而不是文件。
如果两个文件的内容完全一样,无论是否在相同的目录,Git在对象库里只保存一份blob形式的内容副本。Git仅根据文件内容来计算每一个文件的散列码,如果文件有相同的SHA1值,它们的内容就是相同的,然后将这个blob对象放到对象库里,并以SHA1值作为索引。项目中的这两个文件,不管它们在用户的目录结构中处于什么位置,都使用那个相同的对象指代其内容。
如果这些文件中的一个发生了变化,Git会为它计算一个新的SHA1值,识别出它现在是一个不同的blob对象,然后把这个新的blob加到对象库里。原来的blob在对象库里保持不变,为没有变化的文件所使用。
其次,当文件从一个版本变到下一个版本的时候,Git的内部数据库有效地存储每个文件的每个版本,而不是它们的差异。因为Git使用一个文件的全部内容的散列值作为文件名,所以它必须对每个文件的完整副本进行操作。Git不能将工作或者对象库条目建立在文件内容的一部分或者文件的两个版本之间的差异上。
文件拥有修订版本和从一个版本到另一个版本的步进,用户的典型看法是这种文件简直是个工艺品。Git用不同散列值的blob之间的区别来计算这个历史,而不是直接存储一个文件名和一系列差异。这似乎有些奇怪,但这个特性让Git在执行某些任务的时候非常轻松。
4.1.6 路径名与内容
跟很多其他VCS一样,Git需要维护一个明确的文件列表来组成版本库的内容。然而,这个需求并不需要Git的列表基于文件名。实际上,Git把文件名视为一段区别于文件内容的数据。这样,Git就把索引从传统数据库的数据中分离出来了。看看表4-1会很有帮助,它粗略地比较了Git和其他类似的系统。
文件名和目录名来自底层的文件系统,但是Git并不真正关心这些名字。Git仅仅记录每个路径名,并且确保能通过它的内容精确地重建文件和目录,这些是由散列值来索引的。
Git的物理数据布局并不模仿用户的文件目录结构。相反,它有一个完全不同的结构却可以重建用户的原始布局。在考虑其自身的内部操作和存储方面,Git的内部结构是一种更高效的数据结构。
当Git需要创建一个工作目录时,它对文件系统说:“嘿!我这有这样大的一个blob数据,应该放在路径名为path/to/directory/file的地方。你能理解吗?”文件系统回复说:“啊,是啊,我认出那个字符串是一组子目录名,并且我知道把你的blob数据放在哪里!谢谢!”
4.1.7 打包文件
一个聪明的读者也许已经有了关于Git的数据模型及其单独文件存储的挥之不去的问题:直接存储每个文件每个版本的完整内容是否太低效率了?即使它是压缩的,把相同文件的不同版本的全部内容都存储的效率是否太低了?如果你只添加一行到文件里,Git是不是要存储两个版本的全部内容?
幸运的是,答案是“不是,不完全是!”
相反,Git使用了一种叫做 打包文件(pack file) 的更有效的存储机制。要创建一个打包文件,Git首先定位内容非常相似的全部文件,然后为它们之一存储整个内容。之后计算相似文件之间的差异并且只存储差异。例如,如果你只是更改或者添加文件中的一行,Git可能会存储新版本的全部内容,然后记录那一行更改作为差异,并存储在包里。
存储一个文件的整个版本并存储用来构造其他版本的相似文件的差异并不是一个新伎俩。这个机制已经被其他VCS(如RCS)用了好几十年了,它们的方法本质上是相同的。
然而,Git文件打包得非常巧妙。因为Git是由内容驱动的,所以它并不真正关心它计算出来的两个文件之间的差异是否属于同一个文件的两个版本。这就是说,Git可以在版本库里的任何地方取出两个文件并计算差异,只要它认为它们足够相似来产生良好的数据压缩。因此,Git有一套相当复杂的算法来定位和匹配版本库中潜在的全局候选差异。此外,Git可以构造一系列差异文件,从一个文件的一个版本到第二个,第三个,等等。
Git还维护打包文件表示中每个完整文件(包括完整内容的文件和通过差异重建出来的文件)的原始blob的SHA1值。这给定位包内对象的索引机制提供了基础。
打包文件跟对象库中其他对象存储在一起。它们也用于网络中版本库的高效数据传输。