git 数据结构探究之blob

git有三种数据结构:blob、tree、commit。
今天先探究blob,另外两个其实差不多。

准备工作

实验环境:

windows 10,Git Bash

在任意空文件夹右键,点击 Git Bash

git_bash

输入下面的命令,初始化空文件夹为仓库:

$ git init

注:在GitBash中,右键充当了Ctrl C/V 。

之后可以看到一个名为 .git 的隐藏文件夹。

kongfile

有这个隐藏文件夹的文件夹就称为仓库
删除它就又变为普通文件夹。

查看隐藏文件夹目录结构

tree -a

tree
hook目录可以先手动删除,这样目录树会显示的更简洁。
删除hook文件夹后的目录结构:

$ tree -a
.
`-- .git
    |-- HEAD
    |-- config
    |-- description
    |-- info
    |   `-- exclude
    |-- objects
    |   |-- info
    |   `-- pack
    `-- refs
        |-- heads
        `-- tags

8 directories, 4 files

注:
tree命令可能需要单独安装:下载链接
找到Binaries,点击后面的zip进行下载。
download_tree

下载完成后,使用解压工具解压zip文件,并进入解压后的bin目录。
将tree.exe复制到C:\Program Files\Git\usr\bin 目录下即可。

探究一:什么是blob?

我们要探究的数据结构就存储在 .git/objects 目录下,
可用find命令递归展开查看。
-type 参数显示指定类型的内容,f表示文件类型。

$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

目前我们还没有写入任何文件,
可以看出objects目录下只有两个空文件夹info和pack。

blob

blob文件本质是一个压缩文件,当我们将文件git add进入缓存区时,git会对文件内容进行少量描述性添加,然后分别进行SHA-1压缩两种运算,
SHA1运算的结果是一串40位的字符,取前两位作为文件夹名称,后38位作为压缩后的文件名。

接下来,我们手搓一个blob格式的文件:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
  1. git hash-object命令会对我们输入的内容进行描述性添加,然后进行SHA1运算,
  2. -w选项:对内容进行压缩运算并保存结果,
  3. --stdin命令:指明内容来自输入端。

(注意我们提到的这个描述性添加,后面会专门解说。)

可以通过find命令查看我们创造的blob文件的完整路径:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

通过tree命令观察目录结构:

$ tree -a
.
`-- .git
    |-- HEAD
    |-- config
    |-- description
    |-- info
    |   `-- exclude
    |-- objects
    |   |-- d6
    |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
    |   |-- info
    |   `-- pack
    `-- refs
        |-- heads
        `-- tags

9 directories, 5 files

可以看到,在.git/objects目录下,多了一个d6目录,该目录中还有一个以38个字符命名的文件。
而其他则没有变化。

接下来可以用git cat-file来查看我们创造的这个文件的格式或者它压缩前原本的内容。

$ git cat-file -t d670
blob
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
  1. -t选项用来显示类型
  2. -p选项用来显示压缩前的内容
  3. 文件名既可以输入SHA1运算后的全部40位字符,也可以只取SHA1运算的前几位(一般要大于等于4位)。

git cat-file命令本质是一个解压缩命令,
那么它是如何识别文件类型呢?
原理就在于我们上面提到的少量描述性添加

为了探究具体添加了什么,这次我们直接解压blob文件:

$ openssl zlib -d < .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 
blob 13 test content

输出的blob表示文件类型
13表示文件内容所占字符,但‘test content’算上空格才12个字符,为何是13?
我们可以将解压结果写入文件,然后用二进制编辑器查看一下:

$ openssl zlib -d <.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 >test.bin

注:文件名随便取,可以叫test.bin,也可以叫temp.md。

之后用HxD打开test.bin文件

ascii_lr
可以看到,结尾处有一个16进制编码为0A的字符,它对应的ASCII是换行符LR(Line Feed)。
换行符是一种动作效果,没有办法直接显示,所以二进制编辑器用“.”来代替。
而我们的终端或者普通编辑器则什么也不显示,
这也就解释了为何是13个字符。

注:
00对应NULL,表示空字符。
20对应空格。

现在我们对解压出的文件内容进行SHA1运算,看看哈希值是否和我们用git hash-file计算的一样:

$ openssl sha1 < test.bin
SHA1(stdin)= d670460b4b4aece5915caf5c68d12f560a9fe3e4

结论:

blob文件就是对我们的源文件内容进行描述性添加,然后对内容进行压缩运算的结果。
所以blob类型就是一种压缩文件。

探究二:当我们修改后,blob会发生什么?

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
warning: in the working copy of 'test.txt', LF will be replaced by CRLF the next time Git touches it
83baae61804e65cc73a7201a7252750c76066a30

忽略warning,并不影响我们实验。

txt_version1

查看目录树结构:

$ tree -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- 83
|   |   |   `-- baae61804e65cc73a7201a7252750c76066a30
|   |   |-- d6
|   |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
`-- test.txt

10 directories, 7 files

可以看到.git/objects目录下,多了一个83文件夹。
以及,我们的主目录下还有一个test.txt文件。

修改test.txt文件

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

txt_version2
文件内容已经改变。
我们再次查看目录树:

$ tree -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- 1f
|   |   |   `-- 7a7a472abf3dd9643fd615f6da379c4acb3e3a
|   |   |-- 83
|   |   |   `-- baae61804e65cc73a7201a7252750c76066a30
|   |   |-- d6
|   |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
`-- test.txt

11 directories, 8 files

可以看到.git/objects目录下又多了一个1f文件夹。

列出三个blob文件:

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

可知各个文件夹中文件的内容分别是:
1f是version 2
83是version 1
d6是test content

验证一下:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30
version 1
$ git cat-file -t 83baae61804e65cc73a7201a7252750c76066a30
blob
$ git cat-file -p 1f7a
version 2

结论:

blob是对文件内容的完整压缩记录,
相当于我们对源文件每git add一次,就会产生一个对应的blob。
哪怕我们只修改一个字符,也会产生一个新的完整文件压缩版blob。
容量爆炸指日可待!

所以我们应避免以下三点:

  1. 频繁产生blob
  2. 将无用文件生成blob
  3. 大文件生成blob

对应解决方案:

  1. 减少git add操作
  2. 撰写.gitignore文件,这样每次git add就会忽略指定文件
  3. 大文件应该有特殊的操作,待研究。

这样应该就可以减缓容量爆炸问题。

探究三:内容相同会发生什么?

我们新建一个文件test2.txt,它的内容也是“version 2”。

$ echo 'version 2' > test2.txt
$ git hash-object -w test2.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

查看目录树结构:

$ tree -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- 1f
|   |   |   `-- 7a7a472abf3dd9643fd615f6da379c4acb3e3a
|   |   |-- 83
|   |   |   `-- baae61804e65cc73a7201a7252750c76066a30
|   |   |-- d6
|   |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
|-- test.txt
`-- test2.txt

主目录下增加了test2.txt文件,
但blob并没有增加。
结论:说明不同文件但相同内容共享同一个blob。

探究四:哈希值前两位相同会发生什么?

我们要构造两个不同文件,让他们哈希值前两位相同,我找到的是数字“7”和“36”。

$ echo '7' | git hash-object -w --stdin
7f8f011eb73d6043d2e6db9d2c101195ae2801f2

观察目录树结构:

$ tree -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- 1f
|   |   |   `-- 7a7a472abf3dd9643fd615f6da379c4acb3e3a
|   |   |-- 7f
|   |   |   `-- 8f011eb73d6043d2e6db9d2c101195ae2801f2
|   |   |-- 83
|   |   |   `-- baae61804e65cc73a7201a7252750c76066a30
|   |   |-- d6
|   |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
|-- test.txt
`-- test2.txt

12 directories, 10 files

发现多了一个7f文件夹。
构造另一个:

$ echo '36' | git hash-object -w --stdin
7facc89938bbc5635e3d36ffa56b4c85e9b07db8

观察目录树结构:

$ tree -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- 1f
|   |   |   `-- 7a7a472abf3dd9643fd615f6da379c4acb3e3a
|   |   |-- 7f
|   |   |   |-- 8f011eb73d6043d2e6db9d2c101195ae2801f2
|   |   |   `-- acc89938bbc5635e3d36ffa56b4c85e9b07db8
|   |   |-- 83
|   |   |   `-- baae61804e65cc73a7201a7252750c76066a30
|   |   |-- d6
|   |   |   `-- 70460b4b4aece5915caf5c68d12f560a9fe3e4
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
|-- test.txt
`-- test2.txt

12 directories, 11 files

可以看到,目录并没有增多,但7f目录下多了一个文件。

结论:这其实是一种文件整理方式,可能是为了方便检索。待以后继续探究。

参考链接:

git官方教程
Git底层原理与分析模型
[deepseek && 豆包]

posted @ 2025-07-30 22:24  leader4  阅读(26)  评论(0)    收藏  举报