Git 内部原理
理解 Git 的内部原理能让你更好地使用 Git,也能在遇到问题时更快地定位和解决。本章将深入介绍 Git 的底层实现机制。
核心概念:内容寻址文件系统
Git 本质上是一个内容寻址文件系统(Content-Addressable Filesystem),在此基础上构建了版本控制系统的用户界面。
这意味着什么呢?简单来说,Git 的核心是一个键值对数据存储。你可以向 Git 仓库插入任意类型的内容,Git 会返回一个唯一的键,你之后可以用这个键来检索该内容。
.git 目录结构
当你运行 git init 时,Git 会创建一个 .git 目录,几乎所有 Git 存储和操作的内容都位于这个目录中:
$ ls -F1 .git
config # 项目配置
description # GitWeb 使用(可忽略)
HEAD # 指向当前分支
hooks/ # 钩子脚本
index # 暂存区信息
info/ # 全局排除文件
objects/ # 对象数据库
refs/ # 引用(分支、标签等)
最重要的四个部分:
- objects/:存储所有内容数据
- refs/:存储指向数据(分支、标签、远程等)的指针
- HEAD:指向当前检出的分支
- index:存储暂存区信息
Git 对象模型
Git 有四种基本对象类型:blob、tree、commit 和 tag。理解这些对象是掌握 Git 内部原理的关键。
Blob 对象(文件内容)
Blob(Binary Large Object)存储文件的内容,但不包含文件名。它是 Git 中最基础的对象类型。
# 使用底层命令创建 blob 对象
$ echo 'Hello, Git!' | git hash-object -w --stdin
8ab686eafeb1f44702738c8b0f24f2567c36da6d
# 查看对象内容
$ git cat-file -p 8ab686eafeb1f44702738c8b0f24f2567c36da6d
Hello, Git!
# 查看对象类型
$ git cat-file -t 8ab686eafeb1f44702738c8b0f24f2567c36da6d
blob
git hash-object 命令的 -w 选项表示将对象写入数据库,--stdin 表示从标准输入读取内容。
对象存储路径:
$ find .git/objects -type f
.git/objects/8a/b686eafeb1f44702738c8b0f24f2567c36da6d
注意:Git 将 40 位 SHA-1 哈希值的前 2 位作为子目录名,后 38 位作为文件名。这样做有两个好处:
- 避免单个目录下文件过多,影响文件系统性能
- 便于通过前缀快速定位对象
Tree 对象(目录结构)
Tree 对象解决了一个问题:blob 只存储内容,不存储文件名和目录结构。Tree 对象相当于 Unix 中的目录条目,它包含指向 blob 或其他 tree 的引用,并记录文件名和权限。
# 查看某个提交对应的 tree
$ git cat-file -p HEAD^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README.md
100644 blob 8f94139338f9404f26296befa88755fc2598c289 package.json
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 src
Tree 对象中的每一行包含:
- 模式:
100644(普通文件)、100755(可执行文件)、120000(符号链接)、040000(目录) - 类型:blob 或 tree
- SHA-1 哈希:指向的对象
- 名称:文件名或目录名
Commit 对象(提交)
Commit 对象将 tree 对象与提交信息、作者信息、时间戳和父提交关联起来:
$ git cat-file -p HEAD
tree cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
parent 8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b
author 张三 <[email protected]> 1715000000 +0800
committer 张三 <[email protected]> 1715000000 +0800
添加用户登录功能
Commit 对象的结构:
- tree:指向顶层目录的 tree 对象
- parent:父提交(可以有多个,如合并提交)
- author:作者信息(谁写的代码)
- committer:提交者信息(谁提交的代码,可能与作者不同)
- 空行
- 提交信息
Tag 对象(标签)
附注标签(annotated tag)会创建 tag 对象:
$ git cat-file -p v1.0.0
object cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
type commit
tag v1.0.0
tagger 张三 <[email protected]> 1715000000 +0800
版本 1.0.0 发布
Tag 对象包含:
- object:指向的对象(通常是 commit)
- type:对象类型
- tag:标签名
- tagger:打标签的人
- 标签信息
对象关系图
对象存储格式
Git 对象的存储格式如下:
- 构造头部:
对象类型 内容长度\0 - 拼接头部和内容
- 计算 SHA-1 哈希值
- 使用 zlib 压缩
- 存储到
.git/objects/目录
例如,存储 "Hello" 这个字符串:
blob 5\0Hello
然后计算这个字符串的 SHA-1 值并压缩存储。
Git 引用(Refs)
直接使用 SHA-1 哈希值来引用提交非常不便。Git 引用(references 或 refs)是存储 SHA-1 值的文件,给哈希值起了一个可读的名字。
分支引用
分支本质上就是一个指向某个提交的可变指针:
# 查看分支引用
$ cat .git/refs/heads/main
cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
# 等价于
$ git rev-parse main
cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
当你创建新分支时,Git 只是在 .git/refs/heads/ 目录下创建一个新文件:
$ git branch feature-login
$ cat .git/refs/heads/feature-login
cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
HEAD 引用
HEAD 是一个特殊的引用,指向当前所在的位置:
$ cat .git/HEAD
ref: refs/heads/main
当你在 main 分支时,HEAD 指向 refs/heads/main。当你切换分支时,HEAD 文件的内容会更新。
在"分离 HEAD"状态下,HEAD 直接指向某个提交:
$ git checkout abc123
$ cat .git/HEAD
abc123def456789...
标签引用
标签分为两种:
- 轻量标签:与分支引用类似,只是存储在
refs/tags/目录 - 附注标签:创建 tag 对象,引用指向 tag 对象
# 轻量标签
$ cat .git/refs/tags/v1.0.0-light
cfda3bf7c8b6a1e9b8a5e8c7d6f5a4b3c2d1e0f9
# 附注标签(指向 tag 对象)
$ cat .git/refs/tags/v1.0.0
a1b2c3d4e5f6... # tag 对象的哈希
远程引用
远程分支引用存储在 refs/remotes/ 目录:
$ cat .git/refs/remotes/origin/main
abc123def456789...
远程引用与分支引用的区别在于:远程引用是只读的,Git 不会自动更新它们,只有在你执行 git fetch 或 git pull 时才会更新。
引用规范(Refspec)
Refspec 定义了本地分支与远程分支的映射关系:
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
格式:[+]源引用:目标引用
+表示强制更新(即使不是快进)refs/heads/*匹配远程所有分支refs/remotes/origin/*映射到本地的远程跟踪分支
Pack 文件
当你有大量小对象时,Git 会将它们打包成 pack 文件以提高存储效率。
松散对象 vs 打包对象
松散对象:每个对象作为单独的文件存储在 .git/objects/ 目录下。
打包对象:多个对象打包到一个 .pack 文件中,并有一个对应的 .idx 索引文件。
打包机制
Git 在以下情况会触发打包:
- 手动执行
git gc - 推送到远程时
- 仓库中松散对象过多时
打包的核心思想是增量存储:Git 会找出相似的对象,只存储它们的差异(delta):
# 手动触发打包
$ git gc
# 查看打包结果
$ ls .git/objects/pack/
idx-abc123.idx # 索引文件
pack-abc123.pack # 打包文件
Pack 文件的优势
- 节省空间:相似对象只存储差异
- 减少文件数量:大量小文件变成一个大文件
- 网络传输高效:推送时只传输 pack 文件
查看 Pack 文件内容
# 查看某个 pack 文件包含的对象
$ git verify-pack -v .git/objects/pack/pack-abc123.idx
# 输出示例
non delta: 1 chain
abc123 blob 120 85 1
def456 blob 125 80 2
ghi789 tree 350 200 3
...
Pack 的增量存储原理
Git 在打包时会分析对象之间的相似性。对于相似的对象,Git 会选择一个作为"基础对象",其他对象只存储与基础对象的差异。
增量存储示例
假设你有两个版本的文件:
版本1:Hello, World!
版本2:Hello, Git!
如果直接存储两个 blob:
blob 1: "Hello, World!" → 存储完整内容
blob 2: "Hello, Git!" → 存储完整内容
使用增量存储:
blob 1: "Hello, World!" → 存储完整内容(作为基础对象)
blob 2: 基础对象=blob 1, 差异="将 'World' 替换为 'Git'"
这种存储方式特别适合:
- 文件的多个版本
- 相似的代码文件
- 二进制文件的多个版本
数据完整性与安全
SHA-1 哈希
Git 使用 SHA-1 哈希来标识对象,这提供了两个重要保证:
- 内容寻址:相同的内容总是产生相同的哈希值
- 完整性校验:任何对内容的修改都会产生不同的哈希值
虽然 SHA-1 在理论上存在碰撞风险,但在 Git 的使用场景中,这种风险可以忽略不计。Git 社区也在逐步迁移到更安全的 SHA-256。
对象验证
# 验证仓库中所有对象的完整性
$ git fsck
# 输出示例
Checking object directories: 100% (256/256), done.
Checking objects: 100% (1234/1234), done.
git fsck 会检查:
- 对象是否损坏
- 引用是否有效
- 是否有悬空对象(dangling objects)
恢复丢失的数据
使用 reflog
即使你误删了分支或执行了 git reset --hard,数据可能仍然存在。reflog 记录了 HEAD 的所有变化:
# 查看 reflog
$ git reflog
# 输出示例
abc1234 HEAD@{0}: reset: moving to HEAD~1
def5678 HEAD@{1}: commit: 添加重要功能
ghi9012 HEAD@{2}: checkout: moving from feature to main
...
# 恢复到某个操作之前的状态
$ git checkout def5678
使用 fsck 找回悬空对象
# 查找悬空对象
$ git fsck --lost-found
# 输出示例
dangling commit abc123def456...
dangling blob xyz789...
# 查看悬空提交的内容
$ git show abc123def456
# 恢复悬空提交
$ git branch recovered-branch abc123def456
从 pack 文件恢复
即使 HEAD 和 reflog 都丢失了,对象仍然存在于 pack 文件中:
# 列出 pack 文件中的所有对象
$ git verify-pack -v .git/objects/pack/*.idx | grep commit
# 恢复某个提交
$ git cat-file -p <commit-hash>
传输协议
本地协议
当远程仓库在本地文件系统时:
$ git clone /path/to/repo.git
$ git clone file:///path/to/repo.git
使用 file:// 前缀会触发 Git 传输协议,效率略低但更安全。
HTTP 协议
Git 支持两种 HTTP 协议:
- 哑协议(Dumb HTTP):简单的文件传输,效率低
- 智能协议(Smart HTTP):支持协商和增量传输,推荐使用
# 智能协议会使用 /info/refs?service=git-upload-pack 端点
$ git clone https://github.com/user/repo.git
SSH 协议
SSH 协议提供加密和认证:
$ git clone user@server:/path/to/repo.git
$ git clone ssh://user@server/path/to/repo.git
Git 协议
Git 协议速度最快,但没有认证机制:
$ git clone git://server/path/to/repo.git
通常只用于只读的公开仓库。
命令分类:高层命令与底层命令
Git 命令分为两类:
高层命令(Porcelain)
日常使用的用户友好命令:
git add、git commit、git pushgit branch、git checkout、git mergegit log、git status、git diff
底层命令(Plumbing)
用于脚本和底层操作的命令:
| 命令 | 功能 |
|---|---|
git hash-object | 计算对象的哈希值 |
git cat-file | 查看对象内容 |
git update-index | 更新暂存区 |
git write-tree | 将暂存区写入 tree 对象 |
git commit-tree | 创建 commit 对象 |
git read-tree | 读取 tree 到暂存区 |
git ls-files | 列出暂存区文件 |
git update-ref | 更新引用 |
底层命令示例——手动创建一次提交:
# 1. 创建 blob 对象
$ echo "Hello, World" | git hash-object -w --stdin
# 2. 写入暂存区
$ git update-index --add --cacheinfo 100644 <hash> hello.txt
# 3. 创建 tree 对象
$ git write-tree
# 4. 创建 commit 对象
$ echo "Initial commit" | git commit-tree <tree-hash>
# 5. 更新分支引用
$ git update-ref refs/heads/main <commit-hash>
小结
本章我们学习了 Git 的内部原理:
- 内容寻址文件系统:Git 的核心设计
- 四种对象类型:blob(文件内容)、tree(目录结构)、commit(提交)、tag(标签)
- 引用系统:分支、标签、HEAD 都是指向对象的引用
- Pack 文件:增量存储机制,节省空间和传输带宽
- 数据恢复:reflog、fsck 和 pack 文件都可以用来恢复丢失的数据
- 传输协议:本地、HTTP、SSH、Git 四种协议的特点
- 底层命令:直接操作 Git 内部结构的命令
理解这些原理能帮助你更好地使用 Git,也能在遇到问题时更有效地解决。