🔬 Git 底层原理
理解 Git 的底层原理,能让你在遇到奇怪问题时不再"盲猜",而是能准确判断发生了什么。本章深入探讨 Git 的数据模型和存储机制。
📂 .git 目录结构详解
初见 .git 目录
ls -la .git/
# HEAD ← 当前所在分支的引用(指向 refs/heads/main 或直接 hash)
# config ← 仓库级配置(git config --local 写的地方)
# description ← 仅 GitWeb 使用,其他场景忽略
# hooks/ ← 客户端/服务端钩子脚本目录
# index ← 暂存区(Staging Area)的二进制文件
# info/ ← 仓库额外信息(exclude 文件等)
# logs/ ← reflog 数据目录
# objects/ ← Git 数据库核心!所有数据都存在这里
# refs/ ← 引用目录(分支、tag、远程跟踪分支)
# COMMIT_EDITMSG ← 最后一次 commit message(供 --amend 使用)
objects/ 目录——Git 的心脏
ls .git/objects/
# 00/ 1a/ 2f/ ... ← 每个目录名是 hash 的前 2 位
# info/ ← 额外的 objects 元数据
# pack/ ← Packfile 存储目录(压缩后的 objects)
Git 把所有数据(文件内容、目录结构、提交信息)都存为 object,用 40 位 SHA-1 hash 作为唯一 ID。
Git 项目启动于 2005 年,当时 SHA-1 是标准选择。Git 2.29+ 已开始支持 SHA-256,但生态兼容性仍是问题。实际中 SHA-1 碰撞攻击对 Git 的威胁极低(需要控制文件内容并触发 Git 存储,成本极高)。
🧱 Blob / Tree / Commit 三大对象
Git 有 4 种对象类型,其中最重要的是 Blob、Tree、Commit(第 4 种是 Tag 对象)。
Blob 对象——文件内容
Blob(Binary Large OBject)存储文件的内容(不含文件名、不含权限)。
# 手动创建一个 blob 对象
echo "Hello, Git" | git hash-object -w --stdin
# a1b2c3d4... ← 这是 blob 的 hash
# 查看 blob 内容
git cat-file -p a1b2c3d4
# Hello, Git
| 特性 | 说明 |
|---|---|
| 存储内容 | 文件数据(不含文件名) |
| Hash 计算 | blob <size>\0<content> |
| 去重 | 相同内容 → 相同 hash → 只存一份 |
Tree 对象——目录结构
Tree 存储目录快照:它记录了当前目录下有哪些文件(Blob)和子目录(Tree),以及它们的权限和文件名。
# 查看一个 tree 对象
git cat-file -p HEAD^{tree}
# 100644 blob a1b2c3d4... README.md
# 100644 blob e5f6g7h8... src/index.ts
# 040000 tree i9j0k1l2... src/utils
Tree 结构示意:
Commit (abc123)
└── Tree (def456) ← 根目录
├── README.md (blob: ghi789)
└── src/ (tree: jkl012)
├── index.ts (blob: mno345)
└── utils/ (tree: prs678)
└── helper.ts (blob: tuv901)
Commit 对象——提交快照
Commit 对象记录了一次提交的所有元数据。
# 查看一个 commit 对象
git cat-file -p HEAD
# tree def4567890abcdef1234567890abcdef12345678 ← 指向根 tree
# parent abc1234567890abcdef1234567890abcdef12345 ← 父提交(首次提交无 parent)
# author Alice <alice@example.com> 1717000000 +0800 ← 作者(写代码的人)
# committer Bob <bob@example.com> 1717000000 +0800 ← 提交者(执行 git commit 的人)
#
# fix: handle token expiration ← 提交 message
| 字段 | 含义 |
|---|---|
tree | 本次提交对应的根目录 Tree hash |
parent | 父提交 hash(合并提交有多个 parent) |
author | 原作者(姓名、邮箱、时间戳) |
committer | 提交者(rebase 后 author 不变,committer 变) |
| 消息体 | Commit message |
- author = 写代码的人(不变,即使被 rebase / cherry-pick)
- committer = 执行
git commit的人(rebase 后会变)
可以通过 git log --format=fuller 查看两者。
三对象关系图
Commit (v2)
├── tree → Tree (根目录快照)
├── parent → Commit (v1)
├── author: Alice <alice@...>
└── message: "fix: bug"
Tree (根目录快照)
├── "README.md" → Blob (内容 hash)
└── "src/" → Tree (src 目录)
└── "index.ts" → Blob (内容 hash)
📦 Packfile 与垃圾回收
为什么需要 Packfile?
随着提交增多,.git/objects/ 里会有成千上万个独立文件(每个 blob 一个文件),导致:
- 磁盘占用大——同一个文件修改 100 次,存 100 份完整内容
- 访问慢——随机 I/O 多,克隆时间长
Packfile 把多个 objects 压缩成一个文件,用增量存储(只存差异)大幅减少体积。
Packfile 结构
ls .git/objects/pack/
# pack-abc1234.pack ← 压缩数据文件
# pack-abc1234.idx ← 索引文件(快速查找)
| 文件 | 作用 |
|---|---|
.pack | 压缩后的 objects 数据(增量存储) |
.idx | 索引,支持 O(1) 查找某个 hash 在 pack 中的位置 |
垃圾回收(GC)
# 手动触发 GC(Git 会自动在合适时机调用)
git gc
# 更激进的 GC(会 unpack 再 repack,耗时更长)
git gc --aggressive
# 查看 objects 统计
git count-objects -v
# count: 1234 ← 未打包的 objects 数量
# size: 45678 ← 未打包的 objects 体积(KB)
# pack: 56 ← pack 文件数量
# size-pack: 123456 ← pack 文件总体积(KB)
git gc --aggressive 不要频繁跑--aggressive 会重新压缩所有 objects,耗时很长(大仓库可能数小时)。日常只需 git gc(增量 repack),仅在磁盘空间紧张时才用 --aggressive。
🔗 引用与符号引用
引用(Ref)——指向 Commit 的指针
引用是一个文本文件,里面存着一个 commit hash。
cat .git/refs/heads/main
# abc1234567890abcdef1234567890abcdef12345678 ← 这就是 "main 分支"的本质
cat .git/refs/remotes/origin/main
# def4567890abcdef1234567890abcdef12345678 ← 远程跟踪分支
| 引用类型 | 路径 | 含义 |
|---|---|---|
| 分支 | refs/heads/<name> | 本地分支,可移动(git commit 会自动更新) |
| 远程跟踪分支 | refs/remotes/origin/<name> | 远程分支的本地缓存(git fetch 更新) |
| Tag | refs/tags/<name> | 标签,通常不可移动 |
符号引用(Symbolic Ref)——指向另一个引用
HEAD 是最典型的符号引用,它指向当前分支的引用(而不是直接指向 commit hash)。
cat .git/HEAD
# ref: refs/heads/main ← 符号引用:HEAD 指向 main 分支
# 如果处于 "detached HEAD" 状态:
# abc1234... ← 直接指向 commit hash,不在任何分支上
HEAD 的三种状态
| 状态 | HEAD 内容 | 说明 |
|---|---|---|
| 正常(在分支上) | ref: refs/heads/main | git commit 会移动 main |
| Detached HEAD | abc1234... (直接 hash) | git commit 会创建"无分支的提交"(可用 reflog 找回) |
| 未 born(新仓库) | ref: refs/heads/main 但 main 不存在 | 第一次 git commit 后自动创建 |
HEAD^、HEAD~2 等语法的本质这些语法操作的是 DAG(有向无环图)中的 commit 节点:
HEAD^= 第一个 parentHEAD^2= 第二个 parent(仅合并提交有)HEAD~2= 向前 2 代(沿着第一个 parent 链)
它们最终都会被解析为某个 commit hash,git 再拿着 hash 去找对应的 object。
📝 总结:Git 的数据模型
工作时:你操作的是 Working Directory(文件)
暂存时:git add → 计算 hash → 写入 objects/(Blob)
提交时:git commit → 创建 Tree 对象 → 创建 Commit 对象 → 移动 HEAD 引用
存储时:objects/ 里的松散文件 → gc 后压缩为 pack 文件
引用时:refs/heads/main → commit hash;HEAD → refs/heads/main
理解这套模型后,git reset --hard、git reflog、git fsck 等"高级"命令就不再是黑魔法,而是可推导的操作。
📝 下一步
- 📝 Git 速查表 —— 所有常用命令一览
- 回到 🚀 Git 基础入门 —— 复习基础概念