Skip to content

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.js

Tree 的结构:

模式  类型   SHA-1哈希                              文件名
100644 blob  abc123...  README.md
100755 blob  def456...  run.sh        (可执行文件)
040000 tree  ghi789...  src/          (子目录)

文件模式说明:

模式含义
100644普通文件
100755可执行文件
100664群组可写文件
120000符号链接
040000子目录(Tree)
160000Git 子模块
bash
# 查看 Tree 对象内容
git ls-tree HEAD
# 100644 blob abc123...  README.md
# 040000 tree ghi789...  src

# 递归显示所有文件
git ls-tree -r HEAD

Commit 对象(提交快照)

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 T1

Tag 对象(标签)

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.0

SHA-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 对象……