Wiki LogoWiki - The Power of Many

Week 01: Git 对象模型与存储引擎

深入剖析 Git 的内容寻址存储引擎, 理解 Blob, Tree, Commit, Tag 对象的内部结构与持久化机制.

1. Git 的本质: 内容寻址文件系统

1.1 什么是内容寻址

Git 的底层是一个内容寻址文件系统 (Content-Addressable Filesystem). 这意味着数据的存储位置由其内容的哈希值决定, 而非文件路径或名称.

传统文件系统 vs 内容寻址

特性传统文件系统内容寻址系统
寻址方式路径 (/home/user/file.txt)内容哈希 (a1b2c3d4...)
重命名改变路径内容不变, 哈希不变
内容相同的文件存储多份只存储一份
完整性验证需要额外机制内置 (哈希即校验)

1.2 为什么选择内容寻址

  1. 去重: 相同内容只存储一次, 节省空间.
  2. 完整性: 哈希值本身就是校验和, 任何损坏都会被检测到.
  3. 分布式友好: 不同仓库之间可以通过哈希值确认是否拥有相同的对象.
  4. 不可变性: 对象一旦创建, 内容就不会改变 (改变内容 = 创建新对象).

1.3 数学模型: Git 作为有向无环图 (DAG)

除了内容寻址, Git 的另一个数学本质是 DAG (Directed Acyclic Graph).

  • Directed (有向): 提交指向父提交, 形成时间箭头 (反向).
  • Acyclic (无环): 你不能成为自己的祖先.
  • Graph (图): 由节点 (Commit) 和边 (Parent Pointer) 组成.

DAG 的意义:

  1. 可达性 (Reachability): Git 通过图的遍历算法 (如 Graph Coloring) 来计算 "哪些对象需要推送到服务器" 或 "哪些对象是垃圾可以回收".
  2. 合并基底 (Merge Base): 两个分支的共同祖先, 就是 DAG 中它们的第一条重合路径.
  3. 拓扑排序: 保证了历史的因果关系.

2. SHA-1 哈希算法

2.1 哈希基础

Git 使用 SHA-1 (Secure Hash Algorithm 1) 算法计算对象的哈希值:

  • 输入: 任意长度的数据
  • 输出: 固定 160 位 (40 个十六进制字符) 的哈希值
  • 特性: 单向、确定性、雪崩效应
# 计算字符串的 SHA-1
echo -n "hello" | sha1sum
# aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

# Git 计算对象哈希 (会添加 header)
echo -n "hello" | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

2.2 Git 对象的哈希计算

Git 在计算哈希前会添加一个 header:

{type} {size}\0{content}

例如, 存储字符串 "hello" 作为 blob:

blob 5\0hello

计算方式:

# 手动计算
echo -e "blob 5\0hello" | sha1sum
# ce013625030ba8dba906f756967f9e9ca394464a

# 等价于
echo -n "hello" | git hash-object --stdin

2.3 SHA-256 迁移

由于 SHA-1 已被证明存在碰撞攻击, Git 正在逐步迁移到 SHA-256:

# 创建使用 SHA-256 的仓库 (Git 2.29+)
git init --object-format=sha256

3. 四大核心对象

Git 的所有数据都存储为四种类型的对象:

3.1 Blob (二进制大对象)

作用: 存储文件的纯内容, 不包含文件名、路径或权限.

结构

blob {size}\0{content}

特点:

  • 两个内容完全相同的文件, 无论名称或路径, 共享同一个 Blob.
  • Blob 是 Git 去重的基础.

查看 Blob

# 查看对象类型
git cat-file -t ce01362
# blob

# 查看对象内容
git cat-file -p ce01362
# hello

# 查看对象大小
git cat-file -s ce01362
# 5

3.2 Tree (树对象)

作用: 存储目录结构, 将文件名、权限与 Blob 关联.

结构

tree {size}\0
{mode} {filename}\0{20-byte SHA-1}
{mode} {filename}\0{20-byte SHA-1}
...

mode 权限值:

Mode含义
100644普通文件
100755可执行文件
120000符号链接
040000目录 (子 Tree)

示例

git cat-file -p HEAD^{tree}
# 100644 blob a1b2c3d4...  README.md
# 100755 blob e5f6g7h8...  build.sh
# 040000 tree i9j0k1l2...  src

Tree 是递归结构: 一个 Tree 可以包含其他 Tree (子目录).

3.3 Commit (提交对象)

作用: 记录项目的一次快照, 包含元数据.

结构

tree {tree-sha1}
parent {parent-sha1}
parent {parent-sha1}  (合并提交可能有多个 parent)
author {name} <{email}> {timestamp} {timezone}
committer {name} <{email}> {timestamp} {timezone}

{commit message}

示例

git cat-file -p HEAD
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent 1234567890abcdef...
# author Alice <alice@example.com> 1705380000 +0800
# committer Alice <alice@example.com> 1705380000 +0800
#
# Initial commit

Commit 形成链表: 每个 Commit 指向其父 Commit, 形成历史链.

3.4 Tag (标签对象)

作用: 为特定 Commit 创建一个有意义的名称.

两种标签:

  • 轻量标签 (Lightweight): 仅是指向 Commit 的引用, 不创建 Tag 对象.
  • 附注标签 (Annotated): 创建 Tag 对象, 包含元数据和签名.

附注标签结构

object {commit-sha1}
type commit
tag v1.0.0
tagger Alice <alice@example.com> 1705380000 +0800

Release version 1.0.0

创建标签

# 轻量标签
git tag v1.0.0

# 附注标签
git tag -a v1.0.0 -m "Release 1.0.0"

# 带 GPG 签名的标签
git tag -s v1.0.0 -m "Signed release"

4. .git 目录结构

.git 目录是 Git 仓库的核心, 存储所有版本控制信息.

.git/
├── HEAD                 # 当前分支指针
├── config               # 仓库级配置
├── description          # GitWeb 描述
├── hooks/               # 钩子脚本
├── index                # 暂存区 (Staging Area)
├── info/                # 仓库信息
│   └── exclude          # 本地 gitignore
├── logs/                # 引用日志 (reflog)
│   ├── HEAD
│   └── refs/
├── objects/             # 对象存储
│   ├── info/
│   ├── pack/            # 打包对象
│   └── {2-char}/{38-char}  # 松散对象
└── refs/                # 引用
    ├── heads/           # 本地分支
    ├── remotes/         # 远程分支
    └── tags/            # 标签

4.1 HEAD 文件

HEAD 是一个符号引用, 指向当前分支:

cat .git/HEAD
# ref: refs/heads/main

当处于分离 HEAD 状态时, HEAD 直接包含 Commit 哈希:

cat .git/HEAD
# a1b2c3d4e5f6...

4.2 refs 目录

引用是指向 Commit 的指针:

# 分支
cat .git/refs/heads/main
# a1b2c3d4e5f6...

# 远程跟踪分支
cat .git/refs/remotes/origin/main
# a1b2c3d4e5f6...

# 标签
cat .git/refs/tags/v1.0.0
# a1b2c3d4e5f6...

4.3 objects 目录

对象以两种形式存储:

松散对象 (Loose Objects)

  • 路径: .git/objects/{前2位}/{后38位}
  • 压缩: 使用 zlib 压缩
ls .git/objects/ce/
# 013625030ba8dba906f756967f9e9ca394464a

打包对象 (Packed Objects)

  • 路径: .git/objects/pack/
  • 格式: .pack (数据) + .idx (索引)
  • 用途: 节省空间, 优化传输

5. Packfile 与 Delta 压缩

5.1 为什么需要 Packfile

松散对象的问题:

  • 每个对象一个文件, 文件系统开销大
  • 相似内容的文件 (如同一文件的不同版本) 各自存储, 浪费空间

5.2 Delta 压缩

Packfile 使用 Delta 压缩: 只存储对象之间的差异.

Base Object: 完整的文件内容 (1000 bytes)
Delta 1: 相对于 Base 的差异 (50 bytes)
Delta 2: 相对于 Delta 1 的差异 (30 bytes)

5.3 触发打包

# 手动触发打包
git gc

# 自动触发 (当松散对象超过阈值)
git config gc.auto 256  # 默认值

5.4 查看 Packfile

# 列出 pack 文件
ls .git/objects/pack/

# 验证 pack 文件
git verify-pack -v .git/objects/pack/pack-{hash}.idx

6. Plumbing 命令实战

Git 命令分为两类:

  • Porcelain (瓷器): 用户友好的高层命令 (add, commit, push)
  • Plumbing (管道): 底层命令, 直接操作对象

6.1 创建对象

# 创建 Blob
echo "hello" | git hash-object -w --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

# 创建 Tree
git update-index --add --cacheinfo 100644 ce0136... hello.txt
git write-tree
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904

# 创建 Commit
echo "Initial commit" | git commit-tree 4b825dc -p HEAD
# a1b2c3d4e5f6...

6.2 读取对象

# 查看对象类型
git cat-file -t {hash}

# 查看对象内容
git cat-file -p {hash}

# 查看对象大小
git cat-file -s {hash}

6.3 手动构建提交

完整的底层提交流程:

# 1. 创建文件并写入对象库
echo "Hello, Git!" > hello.txt
BLOB_HASH=$(git hash-object -w hello.txt)

# 2. 更新索引
git update-index --add --cacheinfo 100644 $BLOB_HASH hello.txt

# 3. 创建 Tree
TREE_HASH=$(git write-tree)

# 4. 创建 Commit
COMMIT_HASH=$(echo "First commit" | git commit-tree $TREE_HASH)

# 5. 更新分支引用
git update-ref refs/heads/main $COMMIT_HASH

7. 练习

7.1 探索 .git/objects

  1. 创建一个新仓库并添加几个文件.
  2. 列出 .git/objects 中的所有对象.
  3. 使用 git cat-file 查看每个对象的类型和内容.

7.2 验证内容寻址

  1. 创建两个内容相同但名称不同的文件.
  2. 验证它们是否共享同一个 Blob.

7.3 手动创建提交

使用 Plumbing 命令手动创建一个完整的提交, 不使用 git addgit commit.


8. 思考题

  1. 为什么 Git 对象是不可变的?
  2. 如果两个文件内容相同但权限不同, 它们的 Blob 是否相同? Tree 呢?
  3. 为什么 Git 选择 SHA-1 而不是 MD5?
  4. Delta 压缩的 "Base Object" 是如何选择的?
  5. 如果 .git/objects 中的某个对象损坏了, Git 能检测到吗?

9. 本周小结

  • 内容寻址: Git 以内容的哈希值作为地址存储数据.
  • 四大对象: Blob (内容), Tree (目录), Commit (快照), Tag (标签).
  • 不可变性: 对象一旦创建就不会改变.
  • Packfile: 通过 Delta 压缩节省空间.
  • Plumbing 命令: 底层命令直接操作对象.

理解 Git 的对象模型, 就理解了 Git 的一切. 分支、合并、变基, 都只是在这些不可变对象之上移动指针.

On this page