Git 内部原理

《Pro Git》读书笔记

从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统

底层命令和高层命令

由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统,所以它还包含了一部分用于完成底层工作的命令。这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或藉由脚本调用,来完成工作。这部分命令一般被称作“底层(plumbing)”命令,而那些更友好的命令则被称作“高层(porcelain)”命令。

当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。这个目录包含了几乎所有 Git 存储和操作的对象。如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。

目录结构如下

1
2
3
4
5
6
7
8
9
$ ls -F1
index # 保存暂存区信息
HEAD # 指示目前被检出的分支
config* # 包含项目特有的配置项
description # 仅供GitWeb使用
hooks/ # 包含hook钩子脚本
info/ # 包含全局性排除(global exclude)文件
objects/ # 存储所有的数据内容
refs/ # 存储指向数据(分支)的提交对象的指针

Git 对象

Git 是一个内容寻址文件系统。Git 的核心部分是一个简单的键值对数据库(key-value data store)。你可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。

树对象

树对象(tree object),它能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项数据对象则大致上对应了 inodes 或文件内容。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息

通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。

提交对象

提交对象的格式很简单:它先指定一个顶层树对象,代表当前项目快照;然后是作者/提交者信息(依据你的user.name 和 user.email 配置来设定,外加一个时间戳);留空一行,最后是提交注释

每次我们运行 git add 和 git commit 命令时, Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。

enter description here

对象存储

在存储内容时,会有个头部信息一并被保存。让我们略花些时间来看看 Git 是如何存储其对象的。

所有的 Git 对象均以这种方式存储,区别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

Git 引用

为了能遍历那段历史从而找到所有相关对象,你仍须记住 1a410e 是最后一个提交。我们需要一个文件来保存 SHA-1 值,并给文件起一个简单的名字,然后用这个名字指针来替代原始的 SHA-1 值。在 Git 里,这样的文件被称为“引用(references,或缩写为 refs)”;你可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件。

Git 分支的本质:一个指向某一系列提交之首的指针或引用

HEAD引用

HEAD 文件是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,意味着它并不像普通引用那样包含一个 SHA-1 值——它是一个指向其他引用的指针

当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段

标签引用

前文我们刚讨论过 Git 的三种主要对象类型,事实上还有第四种。标签对象(tag object)非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

存在两种类型的标签:附注标签和轻量标签。轻量标签的全部内容——一个固定的引用。一个附注标签则更复杂一些。若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。

远程引用

第三种引用类型是远程引用(remote reference)。如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下

远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然可以git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。

包文件

Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。

包文件包含了刚才从文件系统中移除的所有对象的内容。索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。

同样有趣的地方在于,最新版本完整保存了文件内容,而原始的版本反而是以差异方式保存的——这是因为大部分情况下需要快速访问文件的最新版本。

引用规格

我们已经使用过一些诸如远程分支到本地引用的简单映射方式,但这种映射可以更复杂。

添加一个远程版本库

1
2
3
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*

引用规格的格式由一个可选的 + 号和紧随其后的 \:\ 组成,其中 \ 是一个模式(pattern),代表远程版本库中的引用;\ 是那些远程引用在本地所对应的位置。+ 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用

引用规格推送

如果 QA 团队想把他们的 master 分支推送到远程服务器的 qa/master 分支上,可以运行:

1
$ git push origin master:refs/heads/qa/master

如果他们希望 Git 每次运行 git push origin 时都像上面这样推送,可以在他们的配置文件中添加一条 push值:

1
2
3
4
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
push = refs/heads/master:refs/heads/qa/master

删除引用

你还可以借助类似下面的命令通过引用规格从远程服务器上删除引用:

1
$ git push origin :topic

因为引用规格(的格式)是 \:\,所以上述命令把 \ 留空,意味着把远程版本库的 topic 分支
定义为空值,也就是删除它。

传输协议

Git 可以通过两种主要的方式在版本库之间传输数据:“哑(dumb)”协议和“智能(smart)”协议。

哑协议

如果你正在架设一个基于 HTTP 协议的只读版本库,一般而言这种情况下使用的就是哑协议。

智能协议

智能协议是更常用的传送数据的方法,但它需要在服务端运行一个进程,而这也是 Git 的智能之处——它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。总共有两组进程用于传输数据,它们分别负责上传和下载数据。

维护与数据恢复

维护

Git 会不定时地自动运行一个叫做 “auto gc” 的命令。大多数时候,这个命令并不会产生效果。然而,如果有太多松散对象(不在包文件中的对象)或者太多包文件,Git 会运行一个完整的 git gc 命令。“gc” 代表垃圾回收,这个命令会做以下事情:收集所有松散对象并将它们放置到包文件中,将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。

数据恢复

在你使用 Git 的时候,你可能会意外丢失一次提交。通常这是因为你强制删除了正在工作的分支,但是最后却发现你还需要这个分支;亦或者硬重置了一个分支,放弃了你想要的提交。如果这些事情已经发生,该如何找回你的提交呢?

最方便,也是最常用的方法,是使用一个名叫 git reflog 的工具。当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。每一次你提交或改变分支,引用日志都会被更新。