☰
SHA-1 与 blob:Git 为什么用对象内容生成 40 位 ID
2026/10/1 2:42:56 网站建设 项目流程

SHA-1 与 blob:Git 为什么用对象内容生成 40 位 ID

这一篇讲 Git 对象系统的第一块:一个普通文件为什么能变成一个稳定的对象 ID。

如果你执行:

mgit hash-object a.txt

为什么会得到一个 40 位十六进制字符串?

这个字符串到底是文件路径的哈希、文件内容的哈希,还是 Git 内部对象的哈希?

答案是:它是 Git 规范对象字节的 SHA-1。对 blob 来说,参与哈希的字节是:

blob SIZE\0CONTENT

所以hello\n对应的不是:

sha1(“hello\n”)

而是:

sha1(“blob 6\0hello\n”)

这个细节很关键。Git 的对象 ID 绑定的是“带类型和长度的对象内容”,不是文件路径,也不是裸文件内容。

一次真实运行

从空仓库开始:

mkdir /tmp/mgit-article-01
cd /tmp/mgit-article-01
mgit init
printf “hello\n” > a.txt
mgit hash-object a.txt
mgit hash-object -w a.txt
mgit cat-file -t HASH
mgit cat-file -p HASH
find .git/objects -type f | sort
git cat-file -t HASH
git cat-file -p HASH

一次实际输出是:

Initialized empty Git repository in ./.git
ce013625030ba8dba906f756967f9e9ca394464a
ce013625030ba8dba906f756967f9e9ca394464a
blob
hello
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
blob
hello

这里有三个观察点。

第一,mgit hash-object a.txt和mgit hash-object -w a.txt得到同一个 hash:

ce013625030ba8dba906f756967f9e9ca394464a

这说明-w只控制是否写入对象库,不改变对象 ID 的计算方式。

第二,写入后的对象路径是:

.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

Git loose object 的路径规则是把 40 位 hash 拆成两段:前 2 位作为目录名,后 38 位作为文件名。

第三,真实 Git 可以读这个对象:

git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a
git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a

输出是:

blob
hello

这说明 mini-git 写出的 loose object 符合真实 Git 的对象格式。

对象 ID 到底怎么计算

命令层会先读取文件内容,然后调用对象层计算 hash。核心逻辑可以压缩成这样:

读取文件内容
-> 拼出对象头:blob SIZE\0
-> 拼上文件内容
-> 对整段规范对象字节做 SHA-1

对hello\n来说,文件内容长度是 6 个字节,所以对象头是:

blob 6\0

最终参与哈希的是:

blob 6\0hello\n

这也解释了为什么 blob 不保存文件名。

object_hash的输入只有对象类型、内容长度和内容本身,没有路径。文件叫a.txt还是b.txt,只要内容都是hello\n,blob hash 就一样。

文件名、目录层级、可执行权限这些信息不属于 blob,它们由 tree 对象保存。后面讲 tree 的时候,这一点会更明显。

SHA-1 在 Git 里承担什么职责

这里不要把重点放偏。我们不是在讲“如何从零推导 SHA-1 的 80 轮压缩函数”,而是在讲 SHA-1 在 Git 对象系统里的角色。

它主要承担四件事:

  • 把对象规范字节映射成固定长度 ID。
  • 相同对象得到相同 ID。
  • 内容改变会导致 ID 改变。
  • 读取对象时可以重新计算 hash 做完整性校验。

所以这篇文章讲 SHA-1,真正要理解的是 Git 的内容寻址模型。

也就是说,Git 不是先给文件分配一个随机 ID,再把内容塞进去;它是先拿对象内容算 ID,再用这个 ID 存储和寻找对象。

写入 loose object 时发生了什么

只执行:

mgit hash-object a.txt

只会计算 hash。

加上-w后:

mgit hash-object -w a.txt

才会写入对象库。

写入流程可以理解成:

计算对象 hash
-> 根据 hash 得到对象路径
-> 拼完整对象数据:header + content
-> zlib 压缩
-> 写入 .git/objects/xx/yyyy…

比如:

ce013625030ba8dba906f756967f9e9ca394464a

会被存成:

.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

这里还有一个自然去重的效果:对象 ID 由内容决定,同一个对象已经存在时,不需要重复写。

压缩不影响对象 ID

loose object 文件里存的是 zlib 压缩后的数据,但 hash 不是对压缩结果算的。

顺序是:

先对 header + content 算 hash
再把 header + content 压缩写入磁盘

这点很重要。

压缩只是物理存储方式,对象 ID 来自逻辑对象内容。后面对象从 loose object 进入 packfile,存储方式会变,但对象 ID 不会因为“换了存储方式”而变化。

cat-file 如何反向验证

cat-file的意义是把对象读回来:

hash
-> object path
-> 读取压缩文件
-> zlib 解压
-> 解析 “type size\0”
-> 输出类型或内容

所以:

mgit cat-file -t ce013625030ba8dba906f756967f9e9ca394464a

输出:

blob

而:

mgit cat-file -p ce013625030ba8dba906f756967f9e9ca394464a

输出:

hello

真实 Git 也能读出同样结果,说明 mini-git 写入的对象格式是兼容的。

这一篇只需要抓住的边界

mini-git 这里保留了 Git 对象系统最核心的东西:

  • 对象头。
  • SHA-1 对象 ID。
  • loose object 路径。
  • zlib 压缩。
  • 类型和内容解析。

但它还不是完整 Git 对象数据库实现。

后面还会遇到 packfile、delta、对象遍历、引用可达性等机制。本文只关心第一个问题:一个文件内容如何变成一个可寻址的 blob 对象。

另外,SHA-1 本身已经不适合作为现代安全哈希来抵御有意构造的碰撞攻击。这里讨论的是 Git 的对象寻址模型;真实 Git 生态里也有 SHA-256 仓库格式等演进。这个系列当前阶段只需要先理解对象 ID 的抽象。

面试里可以这样讲

如果被问到“Git 的 blob 和对象 ID 是什么”,可以这样回答:

Git 的 blob 对象只保存文件内容,不保存文件名。对象 ID 来自规范对象字节的哈希,对 blob 来说是blob SIZE\0CONTENT。因此同样内容会得到同样 blob ID,文件名、目录结构和权限由 tree 对象保存。loose object 会按 hash 的前 2 位分目录,剩余 38 位做文件名,并以 zlib 压缩后的形式存储。

如果继续追问“为什么 Git 可以校验对象完整性”,可以这样答:

因为对象 ID 是对象规范内容的哈希。读取对象后,Git 可以解压并重新计算type size\0content的哈希,如果结果和请求的对象 ID 不一致,就说明对象损坏或不匹配。

这两个答案都能回到实验验证:

  • mgit hash-object -w写对象。
  • mgit cat-file和真实git cat-file读对象。
  • .git/objects/ce/...证明 loose object 的路径规则。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询