跳到主要内容

🔬 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。

💡 为什么是 SHA-1 而不是 SHA-256?

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 vs. committer 的区别
  • 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 一个文件),导致:

  1. 磁盘占用大——同一个文件修改 100 次,存 100 份完整内容
  2. 访问慢——随机 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 更新)
Tagrefs/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/maingit commit 会移动 main
Detached HEADabc1234... (直接 hash)git commit 会创建"无分支的提交"(可用 reflog 找回)
未 born(新仓库)ref: refs/heads/main 但 main 不存在第一次 git commit 后自动创建
💡 理解 HEAD^HEAD~2 等语法的本质

这些语法操作的是 DAG(有向无环图)中的 commit 节点

  • HEAD^ = 第一个 parent
  • HEAD^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 --hardgit refloggit fsck 等"高级"命令就不再是黑魔法,而是可推导的操作。

📝 下一步