Appearance
Git 对象模型
Git 的底层是一个简单的键值存储数据库(content-addressable filesystem)。理解 Git 的对象模型,能帮助你真正明白 Git 是如何存储和管理数据的。
Git 对象概述
Git 用四种对象类型来存储所有数据:
| 对象类型 | 作用 |
|---|---|
| Blob | 存储文件内容 |
| Tree | 存储目录结构 |
| Commit | 存储提交快照 |
| Tag | 存储标签信息 |
所有对象都存储在 .git/objects/ 目录下,以 SHA-1 哈希值命名。
Blob 对象(文件内容)
Blob(Binary Large Object)存储文件的内容,不包含文件名、权限等元数据。
内容:"Hello, Git!\n"
SHA-1:ce013625030ba8dba906f756967f9e9ca394464a
存储位置:.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a关键特性:
- 相同内容的文件只存储一份(内容寻址)
- 不记录文件名,文件名由 Tree 对象维护
- 即使文件被重命名,只要内容不变,Blob 对象不变
bash
# 创建一个 Blob 对象
echo "Hello, Git!" | git hash-object --stdin -w
# 输出:8ab686eafeb1f44702738c8b0f24f2567c36da6d
# 查看 Blob 内容
git cat-file -p 8ab686eafeb1f44702738c8b0f24f2567c36da6d
# Hello, Git!Tree 对象(目录结构)
Tree 对象对应文件系统中的目录,记录该目录下包含的文件(Blob)和子目录(Tree)。
Tree: 项目根目录
├── 100644 blob abc123 README.md
├── 100644 blob def456 package.json
└── 040000 tree ghi789 src/
└── 100644 blob jkl012 main.jsTree 的结构:
模式 类型 SHA-1哈希 文件名
100644 blob abc123... README.md
100755 blob def456... run.sh (可执行文件)
040000 tree ghi789... src/ (子目录)文件模式说明:
| 模式 | 含义 |
|---|---|
100644 | 普通文件 |
100755 | 可执行文件 |
100664 | 群组可写文件 |
120000 | 符号链接 |
040000 | 子目录(Tree) |
160000 | Git 子模块 |
bash
# 查看 Tree 对象内容
git ls-tree HEAD
# 100644 blob abc123... README.md
# 040000 tree ghi789... src
# 递归显示所有文件
git ls-tree -r HEADCommit 对象(提交快照)
Commit 对象是 Git 历史的核心,它记录了:
- tree:指向项目根目录的 Tree 对象(整个项目快照)
- parent:指向上一个提交(父提交)的 SHA-1
- author:作者信息(姓名、邮箱、时间戳)
- committer:提交者信息(可能与 author 不同)
- message:提交信息
Commit 对象内容示例:
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent a1b2c3d4e5f6789012345678901234567890abcd
author Zhang San <zhang@example.com> 1704067200 +0800
committer Zhang San <zhang@example.com> 1704067200 +0800
feat: 添加用户登录功能
实现了基于 JWT 的用户认证流程bash
# 查看提交对象
git cat-file -p HEAD
# tree 4b825dc...
# parent abc123...
# author ...
# committer ...
#
# feat: 添加用户登录功能提交链(历史图):
C3 ──parent──→ C2 ──parent──→ C1(初始提交,无 parent)
↑ ↑ ↑
tree T3 tree T2 tree T1Tag 对象(标签)
Tag 分为两种:
轻量标签(Lightweight Tag)
轻量标签只是一个指向某个 Commit 的引用文件,不创建独立的 Tag 对象。
bash
git tag v1.0.0
# 创建一个轻量标签,就是 .git/refs/tags/v1.0.0 文件,内容是 commit SHA-1附注标签(Annotated Tag)
附注标签会创建一个独立的 Tag 对象,包含额外信息:
bash
git tag -a v1.0.0 -m "Release version 1.0.0"Tag 对象内容:
object a1b2c3d4...(指向的 Commit SHA-1)
type commit
tag v1.0.0
tagger Zhang San <zhang@example.com> 1704067200 +0800
Release version 1.0.0SHA-1 哈希与内容寻址
SHA-1 哈希
Git 使用 SHA-1 算法为每个对象计算 40 位十六进制哈希值。哈希值由对象内容决定:
哈希输入 = 对象类型 + 空格 + 内容长度 + \0 + 内容
例如 Blob:
"blob 13\0Hello, Git!\n"
SHA-1 = ce013625030ba8dba906f756967f9e9ca394464a内容寻址的意义:
- 相同内容 → 相同哈希(自动去重)
- 不同内容 → 不同哈希(无冲突)
- 哈希可作为完整性校验
注意:Git 正在逐步迁移到 SHA-256,以提高安全性。但 SHA-1 对 Git 对象存储来说,实际冲突风险极低。
对象存储路径
SHA-1 的前 2 位作为目录名,后 38 位作为文件名:
SHA-1: ce013625030ba8dba906f756967f9e9ca394464a
路径: .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a这样可以避免单个目录文件过多的性能问题。
对象之间的引用关系
Commit C1
├── tree: T1
│ ├── blob: README.md (b1)
│ └── tree: src/ (T2)
│ └── blob: main.js (b2)
└── parent: (无,初始提交)
Commit C2
├── tree: T3
│ ├── blob: README.md (b1) ← 未修改,复用同一 Blob
│ ├── blob: LICENSE (b3) ← 新文件
│ └── tree: src/ (T2) ← 未修改,复用同一 Tree
└── parent: C1关键洞察:
- Git 不存储文件差异,每次提交都是完整快照
- 通过哈希去重,相同内容不会重复存储
- 快照模型让分支切换、历史查看极其高效
实践验证
bash
# 初始化仓库并创建文件
git init test-repo && cd test-repo
echo "Hello" > README.md
git add README.md
git commit -m "Initial commit"
# 查看所有对象
find .git/objects -type f
# 查看 HEAD 指向的提交
git cat-file -p HEAD
# 查看提交对应的 Tree
git cat-file -p HEAD^{tree}
# 查看文件内容(Blob)
git cat-file -p HEAD:README.md总结
| 对象 | 存储内容 | 指向 |
|---|---|---|
| Blob | 文件内容 | — |
| Tree | 目录结构(文件名+权限+SHA-1) | Blob、Tree |
| Commit | 提交信息 | Tree(根目录)、Commit(父提交) |
| Tag | 标签信息 | Commit(通常) |
理解 Git 对象模型后,你会发现 Git 的很多"神奇"操作其实都有清晰的原理:分支只是一个指向 Commit 的文件,合并是找共同祖先,rebase 是重新创建 Commit 对象……