用了这么久 Git,你是不是好奇:git add、git commit、git push 背后到底发生了什么?文件是怎么存储的?分支为什么切换这么快?
本章就像“Git 解剖课”,带你钻到 Git 的“五脏六腑”里看一看!我们会用最简单的语言 + 实操代码,从底层对象、引用、打包文件讲到数据恢复,让初学者也能搞懂 Git 的核心原理。理解这些后,你不仅能更灵活地使用 Git,遇到问题还能轻松排查,再也不会被“神秘报错”吓住~
Tree 对象
Commit 对象
SHA- 1 哈希
Git 本质是一个“内容寻址文件系统”,所有数据都以“对象”形式存储,每个对象用唯一的 SHA- 1 哈希值标识(40 位字符串)。核心有三种对象,它们共同构成了 Git 的版本管理基础:
1. Blob 对象 – 存储文件内容 📄
Blob(二进制大对象)专门存储文件的原始内容,不包含文件名、权限等元信息。可以理解为“文件内容的快照”。
mkdir git-internal-test && cd git-internal-test
git init# 2. 存储一段文本到 Git 数据库(生成 Blob 对象)
# -w:写入对象到数据库;–stdin:从标准输入读取内容
echo “Hello Git Internals!” | git hash-object -w –stdin
# 输出示例:7f8f296d49c33832a44e882a59d09c266a9059c2(这是 Blob 的 SHA- 1 哈希)
# 3. 查看 Blob 对象的内容
# git cat-file -p 哈希值:查看对象内容(-p=pretty print)
git cat-file -p 7f8f296d49c33832a44e882a59d09c266a9059c2
# 输出:Hello Git Internals!
# 4. 查看对象类型(确认是 Blob)
git cat-file -t 7f8f296d49c33832a44e882a59d09c266a9059c2
# 输出:blob
# 5. 存储文件到 Git 数据库(同样生成 Blob)
echo “Version 1 of test.txt” > test.txt
git hash-object -w test.txt
# 输出示例:83baae61804e65cc73a7201a7252750c76066a30
2. Tree 对象 – 存储目录结构 📂
Tree 对象相当于“目录索引”,存储文件名、文件权限,以及对应的 Blob 对象哈希(或子 Tree 对象哈希)。可以理解为“文件夹的快照”。
# –add:添加文件;–cacheinfo:从 Git 数据库添加(而非工作区文件)
# 格式:git update-index –add –cacheinfo 权限 Blob 哈希 文件名
git update-index –add –cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt# 2. 从暂存区生成 Tree 对象(记录目录结构)
git write-tree
# 输出示例:d8329fc1cc938780ffdd9f94e0d364e0ea74f579(Tree 的 SHA- 1 哈希)
# 3. 查看 Tree 对象内容(看到文件名、权限、对应的 Blob 哈希)
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
# 输出:100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
# 100644:普通文件权限;100755:可执行文件;040000:目录(子 Tree)
# 4. 新增子目录,生成嵌套 Tree
mkdir subdir
echo “Subdir file” > subdir/subfile.txt
git hash-object -w subdir/subfile.txt # 生成 subfile.txt 的 Blob
git update-index –add –cacheinfo 100644 [subfile 的 Blob 哈希] subdir/subfile.txt
git write-tree # 生成包含子目录的 Tree 对象
类比理解 :Tree 对象就像一张“文件夹清单”,上面写着“test.txt(权限 644)对应 Blob 哈希 xxx”“subdir(目录)对应 Tree 哈希 yyy”,清晰记录了目录结构。
3. Commit 对象 – 存储版本信息 📝
Commit 对象是“版本的元数据容器”,记录了:当前版本的顶层 Tree 对象(整个项目的目录快照)、父 Commit(上一个版本)、作者 / 提交者信息、提交时间、提交信息。
# echo 提交信息 | git commit-tree Tree 哈希
echo “First commit: add test.txt” | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
# 输出示例:fdf4fc3344e67ab068f836878b6c4951e3b15f3d(Commit 的 SHA- 1 哈希)# 2. 查看 Commit 对象内容
git cat-file -p fdf4fc3344e67ab068f836878b6c4951e3b15f3d
# 输出结构:
# tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579(对应顶层 Tree)
# author Your Name <your.email@example.com> 1699999999 +0800(作者信息)
# committer Your Name <your.email@example.com> 1699999999 +0800(提交者信息)
#
# First commit: add test.txt(提交信息)
# 3. 创建第二个 Commit(指定父 Commit,形成版本链)
# 修改 test.txt,生成新的 Blob 和 Tree
echo “Version 2 of test.txt” > test.txt
git hash-object -w test.txt # 新 Blob 哈希
git update-index test.txt # 更新暂存区
new_tree=$(git write-tree) # 新 Tree 哈希
# 创建 Commit 时用 -p 指定父 Commit 哈希
echo “Second commit: update test.txt to version 2” | git commit-tree $new_tree -p fdf4fc3344e67ab068f836878b6c4951e3b15f3d
三者关系总结
Commit → Tree → Blob:一个 Commit 指向一个顶层 Tree(项目根目录),Tree 指向子 Tree 或 Blob,最终通过 Blob 获取文件内容。这就是 Git 版本存储的核心逻辑!
HEAD 指针
分支引用
标签引用
你可能好奇:分支名(如 master)、标签名(如 v1.0)为什么能直接对应到某个版本?答案是“引用”——引用就是一个“指针文件”,存储着 Commit 的 SHA- 1 哈希,让你不用记复杂的哈希值,用简单的名字就能访问版本。
1. 分支引用 – 可移动的指针 🔄
分支本质是一个“可移动的引用”,永远指向该分支的最新 Commit。当你提交新代码时,分支引用会自动移动到新的 Commit。
ls .git/refs
# 输出:heads tags remotes(heads= 分支,tags= 标签,remotes= 远程分支)# 2. 手动创建一个分支引用(模拟 git branch 命令)
# 格式:echo “Commit 哈希 ” > .git/refs/heads/ 分支名
echo “fdf4fc3344e67ab068f836878b6c4951e3b15f3d” > .git/refs/heads/my-branch
# 3. 现在可以用分支名访问 Commit
git log my-branch –oneline
# 输出:fdf4fc3 First commit: add test.txt
# 4. 用 git update-ref 创建分支(更安全的方式,避免手动编辑文件)
git update-ref refs/heads/another-branch [第二个 Commit 的哈希]
# 5. 切换分支(本质是修改 HEAD 指针)
git checkout my-branch
# 查看 HEAD 指向:cat .git/HEAD
# 输出:ref: refs/heads/my-branch
2. HEAD 指针 – 当前版本的“风向标”🧭
HEAD 是一个“特殊引用”,指向你当前正在工作的分支(或直接指向某个 Commit,即“分离头指针”状态)。Git 通过 HEAD 知道“当前工作区对应的是哪个版本”。
cat .git/HEAD
# 输出:ref: refs/heads/my-branch(指向分支引用)# 2. 切换到“分离头指针”状态(直接指向 Commit)
git checkout fdf4fc3344e67ab068f836878b6c4951e3b15f3d
cat .git/HEAD
# 输出:fdf4fc3344e67ab068f836878b6c4951e3b15f3d(直接是 Commit 哈希)
# 3. 用 git symbolic-ref 操作 HEAD(安全修改)
git symbolic-ref HEAD refs/heads/another-branch
cat .git/HEAD
# 输出:ref: refs/heads/another-branch
3. 标签引用 – 固定版本的“书签”🔖
标签(tag)是“固定的引用”,指向某个特定的 Commit,不会随提交移动。适合标记版本(如 v1.0、v2.1)。
git update-ref refs/tags/v1.0 fdf4fc3344e67ab068f836878b6c4951e3b15f3d# 2. 创建带注释的标签(生成 Tag 对象,包含标签信息、作者、日期)
git tag -a v1.1 [第二个 Commit 的哈希] -m “Version 1.1 with test.txt updated”
# 3. 查看标签对应的 Commit
git show v1.0
# 输出:显示 v1.0 指向的 Commit 信息
# 4. 查看 Tag 对象(带注释标签会生成 Tag 对象)
git cat-file -t [标签的哈希]
# 输出:tag
git cat-file -p [标签的哈希]
# 输出:包含标签名、标签者、日期、标签信息、指向的 Commit 哈希
打包文件(packfile)
git gc
增量存储
1. 松散对象 vs 打包文件
之前生成的 Blob、Tree、Commit 对象都是“松散对象”(每个对象一个文件,存储在.git/objects/ 哈希前两位 / 哈希后 38 位)。当松散对象过多时,Git 会自动(或手动)将它们打包成“打包文件(packfile)”,采用增量存储优化空间。
find .git/objects -type f
# 输出示例:
# .git/objects/7f/8f296d49c33832a44e882a59d09c266a9059c2(Blob)
# .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579(Tree)
# .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d(Commit)# 2. 手动触发 Git 垃圾回收(生成打包文件)
git gc
# 输出:Counting objects: XXX, done. Compressing objects: 100% (XXX/XXX), done. …
# 3. 查看打包文件(.git/objects/pack 目录下)
ls .git/objects/pack
# 输出:pack-xxx.idx pack-xxx.pack(idx= 索引文件,pack= 数据文件)
# 4. 查看打包文件中的对象(验证对象被打包)
git verify-pack -v .git/objects/pack/pack-xxx.idx
# 输出包含所有打包的对象(Blob、Tree、Commit)及其大小
2. 增量存储 – 打包文件的核心优化
打包文件的关键优化:对于内容相似的文件,Git 只存储“增量差异”(即后一个版本相对于前一个版本的变化),而不是完整内容,极大节省空间。
示例 :test.txt 从“Version 1”修改为“Version 2”,打包后 Git 不会存储两个完整的 Blob,而是存储“Version 1”的完整内容 +“Version 2”相对于“Version 1”的差异(仅修改了“1”→“2”)。
3. git gc 命令 – 手动优化仓库
git gc(garbage collect,垃圾回收)的作用:
- 将松散对象打包成打包文件,节省空间
- 合并多个打包文件为一个,提高效率
- 删除不可达的“悬空对象”(如未被任何 Commit 引用的 Blob)
git gc # 普通垃圾回收
git gc –auto # 自动判断是否需要回收(Git 内部常用)
git gc –prune=now # 立即删除过期的悬空对象(默认保留 2 周)
悬空对象
git fsck
git prune
删除大文件
1. 引用日志(reflog)- 恢复误删的分支 /Commit 🕰️
Git 会记录 HEAD 和分支的每一次移动(如提交、切换分支、重置),存储在“引用日志”中。即使误删分支或 hard reset 丢失 Commit,也能通过 reflog 找回。
# 假设当前有两个 Commit,HEAD 在第二个 Commit
git log –oneline # 查看 Commit:abc123(新)、fdf4fc3(旧)
git reset –hard fdf4fc3 # 强制重置到旧版本,丢失 abc123# 2. 查看引用日志,找到丢失的 Commit
git reflog # 显示 HEAD 的所有移动记录
# 输出示例:
# fdf4fc3 (HEAD -> my-branch) HEAD@{0}: reset: moving to fdf4fc3
# abc123 HEAD@{1}: commit: Second commit: update test.txt to version 2(丢失的 Commit)
# 3. 恢复丢失的 Commit(创建分支指向它)
git branch recover-lost abc123
git checkout recover-lost
# 现在找回了丢失的版本!
# 4. 详细查看 reflog 对应的 Commit 信息
git log -g # 等同于 git log –reflog,显示 reflog 的 Commit 详情
2. 悬空对象与 git fsck – 查找丢失的对象 🔍
“悬空对象”是指没有被任何 Commit、Tree 或引用指向的对象(如未提交的 Blob、误删分支后的 Commit)。git fsck(file system check)可以检查仓库完整性,找到悬空对象。
echo “Dangling blob content” | git hash-object -w –stdin
# 输出:xyz789(Blob 哈希)# 2. 查找悬空对象
git fsck –full
# 输出示例:
# dangling blob xyz789(悬空 Blob)
# dangling commit abc123(如果之前的丢失 Commit 未被恢复)
# 3. 恢复悬空 Commit(创建分支指向它)
git branch recover-dangling abc123
3. 删除仓库中的大文件(彻底清理)🗑️
如果不小心提交了大文件(如 2GB 的安装包),即使后来删除,Git 历史中仍会保留该文件,导致仓库体积过大。需要彻底删除大文件的所有历史引用。
# 查看所有 Blob 的大小,排序后找最大的
git rev-list –objects –all | git cat-file –batch-check=’%(objecttype) %(objectname) %(objectsize) %(rest)’ | grep ‘^blob’ | sort -k3 -n -r | head -5
# 输出示例:blob 123456 2097152000 largefile.zip(2GB 大文件)# 2. 用 filter-branch 删除大文件的所有历史引用
# –index-filter:操作暂存区;–cached:从暂存区删除;–ignore-unmatch:忽略不存在的文件
git filter-branch –index-filter ‘git rm –cached –ignore-unmatch largefile.zip’ — –all
# 3. 删除旧的引用和悬空对象
rm -rf .git/refs/original/ # 删除 filter-branch 生成的原始引用
git reflog expire –expire=now –all # 过期所有 reflog
git gc –prune=now # 垃圾回收,删除悬空对象
# 4. 验证大文件已删除(仓库体积变小)
git count-objects -v # 查看仓库大小
# 输出:size-pack: XXX(单位 KB,明显变小)
smart 协议
HTTP(S) 传输
SSH 传输
packfile 传输
1. 两种核心传输协议
Git 传输数据(clone、fetch、push)时主要使用两种协议,各有适用场景:
(1)Dumb 协议 – 简单的“静态文件传输”📤
基于 HTTP(S),服务器端无需运行 Git 进程,只需提供.git 目录下的文件(如松散对象、打包文件),客户端通过 HTTP GET 请求下载。优点:简单易配置;缺点:仅支持读操作(clone、fetch),不支持 push,效率低。
适用场景:搭建只读的公共仓库(如开源项目的镜像),无需复杂配置。
(2)Smart 协议 – 智能的“双向传输”🤖
Git 专用协议,支持读写(push、fetch),服务器端运行 Git 进程(如 git-receive-pack、git-upload-pack),会先协商双方的对象差异,只传输需要的对象(打包成 packfile),效率高。
适用场景:日常开发(clone、fetch、push),支持双向交互,是目前最常用的协议。
2. 传输流程简化(以 git clone 为例)
- 客户端连接远程服务器(如 SSH、HTTP),发送“获取仓库信息”请求
- 服务器返回仓库的引用(分支、标签对应的 Commit 哈希)和能力(如支持 packfile、增量传输)
- 客户端对比本地已有的对象(首次 clone 则无),告诉服务器“需要哪些对象”
- 服务器将需要的对象打包成 packfile(增量优化),发送给客户端
- 客户端接收 packfile,解压并存储到本地.git/objects 目录,完成克隆
GIT_TRACE_PACKET=true git clone https://github.com/git/git.git
# 输出传输过程中的数据包交互,可看到协商、打包、传输的细节
- 核心对象:Blob(文件内容)、Tree(目录结构)、Commit(版本元数据),通过 SHA- 1 哈希唯一标识,构成 Git 的版本存储基础
- 引用系统:分支、标签、HEAD 都是“指针”,指向 Commit 或对象,让用户无需记忆复杂哈希
- 存储优化:松散对象→打包文件(git gc),增量存储相似文件,极大节省空间
- 数据恢复:reflog 记录 HEAD 移动,git fsck 查找悬空对象,可找回误删的分支和 Commit
- 传输协议:smart 协议为主,协商差异后传输 packfile,高效支持双向交互
要不要我帮你整理一份 Git 内部原理速查手册 ?包含核心对象操作、引用管理、数据恢复、仓库优化的常用命令和原理图解,让你遇到底层问题时能快速查阅,彻底吃透 Git 的工作机制~