在我们日常使用 Git 时,通常的操作是:
git add
命令,将这段代码添加到暂存区中git commit
和 git push
命令,将 本地 Git 版本库中的提交同步到服务器中的版本库中Git 在中间做了什么,它如何存储不同的文件和内容,以及如何区分不同分支下的文件版本呢?日常操作对这些自动的操作都是无感的。
但是如果哪天一旦上述操作中出现了错误,需要找回自己的代码时,如果不懂 Git 其内部存储原理,是没法找回的,因此为了避免这种情况,就有必要去了解其内部的存储——Git 对象的原理。
我们知道,Git 是一个内容寻址文件系统,其核心部分是一个键值对数据库。
当我们向 Git 仓库中插入任意类型的内容时,它会返回一个唯一的键。我们可以通过该键在任意时刻再次取回插入的内容。
比如我们初始化 GitDemo
,发现 Git 对 objects 目录进行了初始化,并创建了 pack 和 info 子目录,但均为空:
$ git init GitDemo
Initialized empty Git repository in D:/GitDemo/.git/
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
然后创建一个 readme.txt 文本,执行 git add
后会发现在 .git/objects
中新增了一个文件夹 89
和文件 dab47ae90ebdfee4e6cb3d64708cd73e9c5472
,
$ echo 'read me please' > readme.txt
$ git add readme.txt
$ find .git/objects -type f
.git/objects/89/dab47ae90ebdfee4e6cb3d64708cd73e9c5472
查看其文件内容,类型和大小:
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
read me please
$ git cat-file -t 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
blob
$ git cat-file -s 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
15
这个键值为 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
的对象就是 Git 对象中的 blob 对象。而且 Git 中所有的对象都存储在 .git/objects
目录(也叫做对象库)中。
这个键值是一个 SHA-1 的哈希值,由 40 个十六进制的数字组成。它是通过一个将待存储的数据外加一个头部信息一起做 SHA 算法运算而得到的校验和。40 个十六进制数字就相当于 160 比特,当用 SHA-1 对不同对象进行区分和识别时,冲突的概率就会极低,不用存储文件的具体类型,用 blob 和 SHA-1 就足以分辨不同文件内容了。
下面来看看 Git 对象的类型:
Blob(Binary Large Object,二进制大对象)是Git中的一种对象类型,用来指代某些可以包含任意数据的变量或文件。它是Git对文件内容的一种抽象表示。每个文件在Git仓库中都被表示为一个独立的Blob对象。Blob对象保存了文件的原始二进制数据,无论文件是文本文件还是二进制文件,Git都以Blob对象的形式存储它们。
比如在上一节中的 readme.txt 文本,在 Git 中就是以 blob 对象存储的:
$ git cat-file -t 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
blob
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
read me please
比如我们修改 readme.txt 文本,会发现有两个 blob 对象存储 readme.txt 的两个版本:
//新增一行文本: reading
$ vi readme.txt
$ git add readme.txt
//原来版本的readme.txt内容还存在:
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
//新版本的readme.txt内容
$ git cat-file -p b0530c9
read me please
reading
Blob对象在Git中的存储方式是使用对象哈希值来进行索引和存储。具体的存储方式如下:
git add
时,Git 就会提取该内容,然后将内容进行 SHA-1 哈希计算,得到一个40个十六进制字符的哈希值。这个哈希值就是Blob对象的唯一标识符,也就是我们上节提到的键。.git/objects
目录下)。89
,文件名为 dab47ae90ebdfee4e6cb3d64708cd73e9c5472
。).git/objects/pack
中Tree
对象是Git中的一种对象类型,用于表示文件和目录的组织结构。每当向Git仓库中添加一个目录时,Git会创建一个新的Tree对象来表示该目录的结构。Tree对象包含了目录中的文件和子目录的元数据,以及它们对应的Blob或Tree对象的哈希值。
比如我们接着在 GitDemo 仓库中添加目录 lib
和文件 readme2.txt 并提交后,当前目录为:
│ readme.txt
│
└─lib
readme2.txt
在 git 中的存储如下:
$ git cat-file -p master^{tree}
040000 tree dbff68a947c7cc60653ff64260b372a405939ae2 lib
100644 blob b0530c9b7360a8cea0e4af86475cac70a2985138 readme.txt
master^{tree}
语法表示 master 分支上最新的提交所指向的树对象。lib
子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:
$ git cat-file -p dbff68a947c7cc
//模式 对象类型 对象的SHA-1值 文件名
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 readme2.txt
从上面可以发现,一个 Tree 对象包含一条或多条树对象记录。每条记录含有一个指向数据对象或者子目录对象的 SHA-1 指针,以及相应的模式、类型、对象名(SHA-1 值)、文件名信息:
如果记录中的类型为 Blob,表示该项是一个文件;如果该类型为 Tree,表示该项是一个子目录。
和 blob 对象的存储方式类似,Tree对象在Git中的存储方式是使用对象哈希值来进行索引和存储。当执行 git add
时 Git 内部的操作有:
Commit对象是Git中的一种对象类型,用于记录代码仓库中的提交操作(也就是执行 git commit
命令)。每个Commit对象代表一个特定的提交操作,包含了提交的元数据和指向代码快照的引用。通过Commit对象,Git能够跟踪代码修改的历史,并实现版本控制和代码回溯等功能。
如下图所示,每个 Commit 对象就是一个 version 版本,Commit 对象通过指向代码快照(也就是一个 Tree 对象)的引用,记录了代码仓库在某个特定时间点的状态。
我们再次引用上面 Tree 对象中的 GitDemo 仓库案例,此时仓库中有如下文件和目录:
│ readme.txt
│
└─lib
readme2.txt
可以通过 git cat-file master
命令查看此时的 commit 对象:
//master就是一个指向commit对象的指针, 其内部存储Commit对象的SHA-1值
$ cat .git/refs/heads/master
2b2af66549827bd6a466fe43081f406c2a12900b
$ git cat-file -p 2b2af665498
tree 2503e9e0c4f774fc5ce298f4972f0e6d3a800d6f
parent 7b34a1e750918570ed610ee1f228e83b43a1192e
author wangJw <wangJw@163.com> 1705458723 +0800
committer wangJw <wangJw@163.com> 1705458723 +0800
second commit
从上面可知,一个 commit 对象由这样几个部分组成:
加上 commit 对象和 master 指针,可以完善在 tree 对象中的图:
当执行 git commit
命令提交代码时,commit 对象随之创建,Git 的内部操作有:
.git/objects
目录下。存储时,Git将Commit对象的内容写入一个临时文件,并将该文件的路径与哈希值相关联。Tag对象是Git中的一种对象类型,用于给特定的提交打上标记。Tag对象的主要作用是标记代码仓库中的特定提交或里程碑。它可以用于记录发布版本、重要的里程碑、稳定的代码快照等。git 标签分为两种类型:轻量标签和附注标签。
轻量标签是指向提交对象的引用:
//创建轻量级标签
$ git tag firstTag
//查看标签:
$ git tag
firstTag
当创建了 firstTag 后,会在.git/refs/tags 目录下创建一个名为 firstTag 的文件,其内容指向当前的 commit 对象的 SHA-1 值
$ cat .git/refs/tags/firstTag
2b2af66549827bd6a466fe43081f406c2a12900b
//轻量标签指向提交对象的引用
$ git cat-file -t firstTag
commit
$ git cat-file -p firstTag
tree 2503e9e0c4f774fc5ce298f4972f0e6d3a800d6f
parent 7b34a1e750918570ed610ee1f228e83b43a1192e
author wangJw <wangJw@163.com> 1705458723 +0800
committer wangJw <wangJw@163.com> 1705458723 +0800
second commit
我们发现,轻量标签 firstTag 中的内容只有一个指向提交对象的 SHA-1 值。没有其他内容,因此无法知道何人,什么时间创建的标签。在团队开发中很容易发生混淆,因此可以用另外一种打标签的方式:附注标签
附注标签则是仓库中的一个独立对象,使用带参数 -a
或 -m <msg>
的 git tag
命令:
//创建一个空提交:
$ git commit --allow-empty -m "empty commit for tagTest"
[master 8a4678f] empty commit for tagTest
//创建一个附注标签
$ git tag -m "secondTag" secondTag
//查看所有标签
$ git tag
firstTag
secondTag
这个时候再来看看 .git/refs/tags
中的 secondTag
标签内容:
//查看该标签的类型
$ git cat-file -t secondTag
tag
//再来看看secondTag标签的内容
$ git cat-file -p secondTag
object 8a4678fae181c16c6f4ff0e6a618991128d86da2
type commit
tag secondTag
tagger wangJw <wangJw@163.com> 1705480524 +0800
secondTag
主要由这样几个部分组成:
我们再来看看 Tag 对象的存储方式
当执行带有 -a
或 -m <msg>
的 git tag
命令时,Git 就会由如下操作:
.git/objects
目录下。存储时,Git将Tag对象的内容写入一个临时文件,并将该文件的路径与哈希值相关联。SHA-1(Secure Hash Algorithm 1)是一种用于生成哈希值的加密算法。该算法将任意长度的输入经过散列运算转换为固定长度的输出。这个固定长度的输出就叫做对应输入内容的数字摘要或者哈希值。
那么对于 Git 对象中的 SHA-1 哈希值是如何生成的?
在 《Pro Git 2nd》这本书提到,SHA-1 哈希值是通过将待存储的数据+一个头部信息(header)一起做 SHA-1 校验运算而得到的。
而在头部信息由这些部分组成:
Git 会将上述的头部信息和文件原始数据拼接,来计算出 SHA-1 校验和。在 Linux 中有 sha1sum 命令可以生成 SHA1 哈希值,下面来验证一下我们生成的 SHA1 哈希值和 Git 是不是相同的:
//当前目录结构
│ a.txt
│
└─b
c.txt
先来看看 blob 对象,也就是 a.txt 对应的文件内容的 SHA1 哈希值生成过程,
//a.txt中的内容为:
$ cat a.txt
123
//字符数为3
$ git cat-file blob HEAD:a.txt | wc -c
3
其头部信息为 blob 3\000
在文件内容上加上头部信息,然后对新文件内容执行 SHA-1 哈希算法:
$ (printf "blob 3\000"; git cat-file blob HEAD:a.txt) | sha1sum
d800886d9c86731ae5c4a62b0b77c437015e00d2 *-
查看在 Git 仓库中是否找到该 SHA-1 值对应的 blob 对象
$ git cat-file -p d80088
123
说明执行 sha1 算法和 Git 操作算法得到的结果一致,验证了 Git 中 SHA-1 哈希值的生成过程
此时在提交链最末端的 commit 对象内容是:
$ git cat-file commit master
tree 46bda27c4834d428a388841808fdaa7ca15a7bc1
parent 61b04b17412e1d9639db2a6b1b4e83319473a14a
author wangJW <1w@163.com> 1679818066 +0800
committer wangJW <1w@163.com> 1679818066 +0800
second commit
根据头部信息的组成,需要知道 commit 中的字符数:
$ git cat-file commit HEAD | wc -c
218
然后加上空格以及空字符串:commit 218\000
,然后与 commit 对象内容拼接,将拼接后的内容计算 SHA1 校验和:
$ (printf "commit 218\000"; git cat-file commit HEAD) | sha1sum
2514fb61430ad5beea4f80e2548f1fbdfd97d74d *-
再来看看 HEAD 文件中对应的 Commit 对象以及其内容是不是与上面的 SHA1 相符:
$ cat .git/HEAD
2514fb61430ad5beea4f80e2548f1fbdfd97d74d
$ git cat-file -p 2514fb6
tree b79d07773ea2d47125f1e7078bbc8113a74a2fa7
parent 61b04b17412e1d9639db2a6b1b4e83319473a14a
author wangJW <1w@163.com> 1705493204 +0800
committer wangJW <1w@163.com> 1705493204 +0800
second commit
从结果可知,说明 Git 内部就是采用头部信息+内容利用 SHA1 算法得到的哈希值。
再来看看 tree 对象
直接拿上面 commit 对象中的 tree 对象来做实验,首先查看 tree 对象中的内容和其中的字节数
$ git cat-file -p b79d0777
100644 blob d800886d9c86731ae5c4a62b0b77c437015e00d2 a.txt
040000 tree ceb3bfbba0a2f151a88628549113aa5c1be65bf5 b
//此时就是对应HEAD指针指向的树
$ git cat-file tree HEAD^{tree} | wc -c
61
然后根据头部信息+tree 对象内容信息计算 SHA-1 值:
$ (printf "tree 61\000"; git cat-file tree HEAD^{tree}) | sha1sum
b79d07773ea2d47125f1e7078bbc8113a74a2fa7 *-
发现此时计算出的 SHA-1 值和 commit 对象所指向的值完全相同,再次验证 SHA-1 生成方式。最后再来看看 tag 对象
首先创建一个 tag 对象:
$ git tag -m "firstTag" firstTag
//创建成功
$ git tag
firstTag
获取这个 tag 对象的字节数,并执行 SHA1 哈希算法
$ git cat-file tag firstTag | wc -c
136
$ (printf "tag 136\000"; git cat-file tag firstTag) | sha1sum
d0c8f7e57f23b368152094bf3e57e70b3569cb13 *-
从 tag 对象的执行结果说明,SHA1 哈希值生成方式正确。
从前面查看 blob 对象内容时提到过,在 Git 中的对象存储在 Git 仓库的 .git/objects
目录下。
在下列情况中,会触发 Git 存储对象的操作:
git add
:在执行 git add 命令暂存某个文件 时,Git 将会将文件的内容转换为一个 Blob 对象,并将该对象存储在本地对象数据库中。这个操作将文件添加到暂存区(Staging Area),为接下来的提交做准备。git commit
:执行 git commit 命令时,Git 首先会创建一个新的 Commit 对象。这个 Commit 对象包含了提交的元数据信息,如作者、提交时间、提交信息等。同时,Git 会创建一个对应的根目录的 Tree 对象,记录了当前提交时仓库中所有文件的快照。最后,Git 将这个 Commit 对象存储在本地对象数据库中,并将当前分支指向该 Commit 对象,表示当前的工作状态。git tag
:执行创建附注标注命令时,Git 会创建一个 Tag 对象,该对象包含标签的元数据信息,并指向一个特定的 Commit 对象。这个 Tag 对象会被存储在本地对象数据库中,以便后续引用。git merge
:执行 git merge 命令时,Git 会创建一个新的 Commit 对象,该对象包含合并的元数据信息,并引用两个或多个合并的分支的 Commit 对象。Git 会将这个新的 Commit 对象存储在本地对象数据库中,并将当前分支指向该新的 Commit 对象。本文通过 .git
目录角度解析 Git对象
.git/objects
目录下,其中对象 ID 值前两位作为目录名,后 38 位作为文件名《Git 权威指南》