转载

基本的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的内容追踪主要表现为两种关键的方式,这两种方式与大多数其他{![Monotone、Mercurial、OpenCMS和Venti是一些值得注意的例外。—原注]}修订版本控制系统都不一样。

首先,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和其他类似的系统。

表4-1 数据库对比

系 统 索 引 机 制 数 据 存 储
传统数据库 索引顺序存取方法(ISAM) 数据记录
UNIX文件系统 目录( /path/to/file 数据块
Git .git/objects/hash 、树对象内容 blob对象、树对象

文件名和目录名来自底层的文件系统,但是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值。这给定位包内对象的索引机制提供了基础。

打包文件跟对象库中其他对象存储在一起。它们也用于网络中版本库的高效数据传输。

4.2 对象库图示

让我们看看Git的对象之间是如何协作来形成完整系统的。

blob对象是数据结构的“底端”;它什么也不引用而且只被树对象引用。在接下来的图里,每个blob由一个矩形表示。

树对象指向若干blob对象,也可能指向其他树对象。许多不同的提交对象可能指向任何给定的树对象。每个树对象由一个三角形表示。

一个圆圈表示一个提交对象。一个提交对象指向一个特定的树对象,并且这个树对象是由提交对象引入版本库的。

每个标签由一个平行四边形表示。每个标签可以指向最多一个提交对象。

分支不是一个基本的Git对象,但是它在命名提交对象的时候起到了至关重要的作用。把每个分支画成一个圆角矩形。

图 4-1 展示了所有部分如何协作。这张图显示了一个版本库在添加了两个文件的初始提交后的状态。两个文件都在顶级目录中。同时它们的master分支和一个叫V1.0的标签都指向ID为1492的提交对象。

基本的Git概念
图4-1 Git对象

现在,让我们使事情变得复杂一点。保留原来的两个文件不变,添加一个包含一个文件的新子目录。对象库就如图4-2所示。

基本的Git概念

图4-2 二次提交后的Git对象.png

就像前一张图里,新提交对象添加了一个关联的树对象来表示目录和文件结构的总状态。在这里,它是ID为cafed00d的树对象。

因为顶级目录被添加的新子目录改变了,顶级树对象的内容也跟着改变了,所以 Git引进了一个新的树对象:cafed00d。

然而,blob对象dead23和feeb1e在从第一次到第二次提交的时候没有发生变化。Git意识到ID没有变化,所以可以被新的cafed00d树对象直接引用和共享。

请注意提交对象之间箭头的方向。父提交在时间上来得更早。因此,在Git的实现里,每个提交对象指回它的一个或多个父提交。很多人对此感到困惑,因为版本库的状态通常画成反方向:数据流从父提交流向子提交。

第6章扩展了这些图来展示版本库的历史是如何建立和被不同命令操作的。

4.3 Git在工作时的概念

带着一些原则,来看看所有这些概念和组件是如何在版本库里结合在一起的。让我们创建一个新的版本库,并更详细地检查内部文件和对象库。

4.3.1 进入.git目录

首先,使用git init来初始化一个空的版本库,然后运行find来看看都创建了什么文件。

$ mkdir /tmp/hello $ cd /tmp/hello $ git init Initialized empty Git repository in /tmp/hello/.git/ # 列出当前目录中的所有文件 $ find . . ./.git ./.git/hooks ./.git/hooks/commit-msg.sample ./.git/hooks/applypatch-msg.sample ./.git/hooks/pre-applypatch.sample ./.git/hooks/post-commit.sample ./.git/hooks/pre-rebase.sample ./.git/hooks/post-receive.sample ./.git/hooks/prepare-commit-msg.sample ./.git/hooks/post-update.sample ./.git/hooks/pre-commit.sample ./.git/hooks/update.sample ./.git/refs ./.git/refs/heads ./.git/refs/tags ./.git/config ./.git/objects ./.git/objects/pack ./.git/objects/info ./.git/description ./.git/HEAD ./.git/branches ./.git/info ./.git/info/exclude

可以看到,.git 目录包含很多内容。这些文件是基于模板目录显示的,根据需要可以进行调整。根据使用的Git的版本,实际列表可能看起来会有一点不同。例如,旧版本的Git不对 .git/hooks 文件使用 .sample 后缀。

在一般情况下,不需要查看或者操作.git目录下的文件。认为这些“隐藏”的文件是Git 底层(plumbing)或者配置的一部分。Git有一小部分底层命令来处理这些隐藏的文件,但是你很少会用到它们。

最初,除了几个占位符之外, .git/objects 目录(用来存放所有Git对象的目录)是空的。

$ find .git/objects .git/objects .git/objects/pack .git/objects/info

现在,让我们来小心地创建一个简单的对象。

$ echo "hello world" > hello.txt $ git add hello.txt

如果输入的“hello world”跟这里一样(没有改变间距和大小写),那么objects目录应该如下所示:

$ find .git/objects .git/objects .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info

所有这一切看起来很神秘。其实不然,下面各节会慢慢解释原因。

4.3.2 对象、散列和blob

当为 hello.t__xt 创建一个对象的时候,Git并不关心 hello.txt 的文件名。Git只关心文件里面的内容:表示“hello world”的12个字节和换行符(跟之前创建的blob一样)。Git对这个blob执行一些操作,计算它的SHA1散列值,把散列值的十六进制表示作为文件名它放进对象库中。

如何知道一个SHA1散列值是唯一的?

两个不同blob产生相同SHA1散列值的机会十分渺茫。当这种情况发生的时候,称为一次碰撞。然而,一次SHA1碰撞的可能性太低,你可以放心地认为它不会干扰我们对Git的使用。

SHA1是“安全散列加密”算法。直到现在,没有任何已知的方法(除了运气之外)可以让一个用户刻意造成一次碰撞。但是碰撞会随机发生吗?让我们来看看。

对于160位数,你有2 160 或者大约10 48 (1后面跟48个0)种可能的SHA1散列值。这个数是极其巨大的。即使你雇用一万亿人来每秒产生一万亿个新的唯一blob对象,持续一万亿年,你也只有10 43 个blob对象。

如果你散列了2 80 个随机blob,可能会发生一次碰撞。

不相信我们的话,就去读读Bruce Schneier的书吧{![《应用密码学》的作者、美国密码学学者、信息安全专家与作家。—译者注]}。

在这种情况下散列值是3b18e512dba79e4c8300dd08aeb37f8e728b8dad。160位的SHA1散列值对应 20 个字节,这需要 40 个字节的十六进制来显示,因此这内容另存为 .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad 。Git在前两个数字后面插入一个“/”以提高文件系统效率(如果你把太多的文件放在同一个目录中,一些文件系统会变慢;使SHA1的第一个字节成为一个目录是一个很简单的办法,可以为所有均匀分布的可能对象创建一个固定的、256路分区的命名空间)。

为了展示Git真的没有对文件的内容做很多事情(它还是同样的内容“hello world”),可以在任何时间使用散列值把它从对象库里提取出来。

$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello world

提示

Git也知道手动输入40个字符是很不切实际的,因此它提供了一个命令通过对象的唯一前缀来查找对象的散列值。

$ git rev-parse 3b18e512d 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

4.3.3 文件和树

既然“hello world”那个blob已经安置在对象库里了,那么它的文件名又发生了什么事呢?如果不能通过文件名找到文件Git就太没用了。

正如前面提到的,Git通过另一种叫做目录树(tree)的对象来跟踪文件的路径名。当使用git add命令时,Git会给添加的每个文件的内容创建一个对象,但它并不会马上为树创建一个对象。相反,索引更新了。索引位于 .git/index 中,它跟踪文件的路径名和相应的blob。每次执行命令(比如,git add、git rm或者git mv)的时候,Git会用新的路径名和blob信息来更新索引。

任何时候,都可以从当前索引创建一个树对象,只要通过底层的git write-tree命令来捕获索引当前信息的快照就可以了。

目前,该索引只包含一个文件, hello.txt.

$ git ls-files -s 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0      hello.txt

在这里你可以看到文件的关联, hello.txt 与3b18e4...的blob。

接下来,让我们捕获索引状态并把它保存到一个树对象里。

$ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 $ find .git/objects .git/objects .git/objects/68 .git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60 .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info

现在有两个对象:3b18e5的“hello world”对象和一个新的68aba6树对象。可以看到,SHA1对象名完全对应 .git/objects 下的子目录和文件名。

但是树是什么样子的呢?因为它是一个对象,就像blob一样,所以可以用底层命令来查看它。

$ git cat-file -p 68aba6 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad     hello.txt

对象的内容应该很容易解释。第一个数100644,是对象的文件属性的八进制表示,用过UNIX的chmod命令的人应该对这个很熟悉了。这里,3b18e5是 hello world 的blob的对象名, hello.txt 是与该blob关联的名字。

当执行git ls-file -s的时候,很容易就可以看到树对象已经捕获了索引中的信息。

4.3.4 对Git使用SHA1的一点说明

在更详细地讲解树对象的内容之前,让我们先来看看SHA1散列的一个重要特性。

$ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 $ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 $ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60

每次对相同的索引计算一个树对象,它们的SHA1散列值仍是完全一样的。Git并不需要重新创建一个新的树对象。如果你在计算机前按照这些步骤操作,你应该看到完全一样的SHA1散列值,跟本书所刊印的一样。

这样看来,散列函数在数学意义上是一个真正的函数:对于一个给定的输入,它总产生相同的输出。这样的散列函数有时也称为摘要,用来强调它就像散列对象的摘要一样。当然,任何散列函数(即使是低级的奇偶校验位)也有这个属性。

这是非常重要的。例如,如果你创建了跟其他开发人员相同的内容,无论你俩在何时何地工作,相同的散列值就足以证明全部内容是一致的。事实上,Git确实将它们视为一致的。

但是等一下—SHA1散列是唯一的吗?难道万亿人每秒产生的万亿个blob永远不会产生一次碰撞吗?这在Git新手中是一个常见的疑惑。因此,请仔细阅读,因为如果你能理解这种区别,那么本章的其他内容就很简单了。

在这种情况下,相同的SHA1散列值并不算碰撞。只有两个不同的对象产生一个相同的散列值时才算碰撞。在这里,你创建了相同内容的两个单独实例,相同的内容始终有相同的散列值。

Git 依赖于 SHA1 散列函数的另一个后果:你是如何得到称为 68aba62e560c0ebc3396 e8ae9335232cd93a3f60的树的并不重要。如果你得到了它,你就可以非常有信心地说,它跟本书的另一个读者的树对象是一样的。Bob通过合并Jennie的提交A、提交B和Sergey的提交C来创建这个树,而你是从Sue得到提交A,然后从Lakshmi那里更新提交B和提交C的合并。结果都是一样的,这有利于分布式开发。

如果要求你查看对象68aba62e560c0ebc3396e8ae9335232cd93a3f60,并且你能找到这样的一个对象,同时因为SHA1是一个加密散列算法,因此你就可以确信你找的对象跟散列创建时的那个对象的数据是相同的。

反过来也是如此:如果你在你的对象库里没找到具有特定散列值的对象,那么你就可以肯定你没有持有那个对象的副本。总之,你可以判断你的对象库是否有一个特定的的对象,即使你对它(可能非常大)的内容一无所知。因此,散列就好似对象的可靠标签或名称。

但是Git也依赖于比那个结论更强的东西。考虑最近的一次提交(或者它关联的树对象)。因为它包含其父提交以及树的散列,反过来又通过递归整个数据结构包含其所有子树和blob的散列,因此可归结为它通过原始提交的散列值唯一标识整个数据结构在提交时的状态。

最后,我们在上一段中的声明可以推出散列函数的强大应用:它提供了一种有效的方法来比较两个对象,甚至是两个非常大而复杂的数据结构{![对这个数据结构更详细的描述见6.3.2节。—原注]},而且并不需要完全传输。

4.3.5 树层次结构

只有单个文件的信息是很好管理的,就像上一节所讲的一样,但项目包含复杂而且深层嵌套的目录结构,并且会随着时间的推移而重构和移动。通过创建一个新的子目录,该目录包含 hello.txt 的一个完全相同的副本,让我们看看Git是如何处理这个问题的。

$ pwd /tmp/hello $ mkdir subdir $ cp hello.txt subdir/ $ git add subdir/hello.txt $ git write-tree 492413269336d21fac079d4a4672e55d5d2147ac $ git cat-file -p 4924132693 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    hello.txt 040000 tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60    subdir

新的顶级树包含两个条目:原始的 hello.txt 以及新的 子目录 ,子目录是 树 而不是 blob 。

注意到不寻常之处了吗?仔细看 subdir 的对象名。是你的老朋友,68aba62e560c0 ebc3396e8ae9335232cd93a3f60!

刚刚发生了什么? subdir 的新树只包含一个文件 hello.txt ,该文件跟旧的“hello world”内容相同。所以 subdir 树跟以前的顶级树是完全一样的!当然它就有跟之前一样的SHA1对象名了。

让我们来看看 .git/objects 目录,看看最近的更改有哪些影响。

$ find .git/objects .git/objects .git/objects/49 .git/objects/49/2413269336d21fac079d4a4672e55d5d2147ac .git/objects/68 .git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60 .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info

这只有三个唯一的对象:一个包含“hello world”的blob;一棵包含 hello.txt 的树,文件里是“hello world”加一个换行;还有第一棵树旁边包含 hello.txt 的另一个索引的另一棵树。

4.3.6 提交

讨论的下一主题是提交(commit)。现在 hello.txt 已经通过git add命令添加了,树对象也通过git write-tree命令生成了,可以像这样用底层命令那样创建提交对象。

$ echo -n "Commit a file that says hello/n" /     | git commit-tree 492413269336d21fac079d4a4672e55d5d2147ac 3ede4622cc241bcb09683af36360e7413b9ddf6c

结果如下所示。

$ git cat-file -p 3ede462 tree 492413269336d21fac079d4a4672e55d5d2147ac author Jon Loeliger <jdl@example.com> 1220233277 -0500 committer Jon Loeliger <jdl@example.com> 1220233277 -0500 Commit a file that says hello

如果你在计算机上按步骤操作,你可能会发现你生成的提交对象跟书上的名字不一样。如果你已经理解了目前为止的一切内容,那原因就很明显了:这是不同的提交。提交包含你的名字和创建提交的时间,尽管这区别很微小,但依然是不同的。另一方面,你的提交确实有相同的树。这就是提交对象与它们的树对象分开的原因:不同的提交经常指向同一棵树。当这种情况发生时,Git能足够聪明地只传输新的提交对象,这是非常小的,而不是很可能很大的树和blob对象。

在实际生活中,你可以(并且应该)跳过底层的git write-tree和git commit-tree步骤,并只使用git commit命令。成为一个完全快乐的Git用户,你不需要记住那些底层命令。

一个基本的提交对象是相当简单的,这是成为一个真正的RCS需要的最后组成部分。提交对象可能是最简单的一个,包含:

  • 标识关联文件的树对象的名称;
  • 创作新版本的人(作者)的名字和创作的时间;
  • 把新版本放到版本库的人(提交者)的名字和提交的时间;
  • 对本次修订原因的说明(提交消息)。

默认情况下,作者和提交者是同一个人,也有一些情况下,他们是不同的。

基本的Git概念
图像说明文字

提示

可以使用git show --pretty=fuller命令来查看给定提交的其他细节。

尽管提交对象跟树对象用的结构是完全不同的,但是它也存储在图结构中。当你做一个新提交时,你可以给它一个或多个父提交。通过继承链来回溯,可以查看项目历史。第6章会给出关于提交和提交图的更详细描述。

4.3.7 标签

最后,Git还管理的一个对象就是标签。尽管Git只实现了一种标签对象,但是有两种基本的标签类型,通常称为 轻量级的(lightweight)和带附注的(annotated)。

轻量级标签只是一个提交对象的引用,通常被版本库视为是私有的。这些标签并不在版本库里创建永久对象。带标注的标签则更加充实,并且会创建一个对象。它包含你提供的一条消息,并且可以根据RFC 4880来使用GnuPG密钥进行数字签名。

Git在命名一个提交的时候对轻量级的标签和带标注的标签同等对待。不过,默认情况下,很多Git命令只对带标注的标签起作用,因为它们被认为是“永久”的对象。

可以通过git tag命令来创建一个带有提交信息、带附注且未签名的标签:

$ git tag -m "Tag version 1.0" V1.0 3ede462

可以通过git cat-file -p命令来查看标签对象,但是标签对象的SHA1值是什么呢?为了找到它,使用4.3.2节的提示。

$ git rev-parse V1.0 6b608c1093943939ae78348117dd18b1ba151c6a $ git cat-file -p 6b608c object 3ede4622cc241bcb09683af36360e7413b9ddf6c type commit tag V1.0 tagger Jon Loeliger <jdl@example.com> Sun Oct 26 17:07:15 2008 -0500 Tag version 1.0

除了日志消息和作者信息之外,标签指向提交对象3ede462。通常情况下,Git通过某些分支来给特定的提交命名标签。请注意,这种行为跟其他VCS有明显的不同。

Git通常给指向树对象的提交对象打标签,这个树对象包含版本库中文件和目录的整个层次结构的总状态。

回想一下图4-1,V1.0标签指向提交1492—依次指向跨越多个文件的树(8675309)。因此,这个标签同时适用于该树的所有文件。

这跟CVS不同,例如,对每个单独的文件应用标签,然后依赖所有打过标签的文件来重建一个完整的标记修订。并且CVS允许你移动单独文件的标签,而Git则需要在标签移动到的地方做一个新的提交,囊括该文件的状态变化。

正文到此结束
Loading...