Git 原理解析
目录
- 概述
- [Git 的核心设计](#Git 的核心设计)
- [Git 的四种核心对象](#Git 的四种核心对象)
- 对象关系图
- [从 git add 到 git commit 的底层流程](#从 git add 到 git commit 的底层流程)
- [.git 目录核心结构](#.git 目录核心结构)
- 分支的本质
- 引用和符号引用
- [Git 对象存储机制](#Git 对象存储机制)
- 哈希算法与完整性
- 实际示例演示
- 总结
概述
Git 不仅仅是一个版本控制系统,它的核心是一个内容寻址的文件系统。理解 Git 的内部机制,能够帮助我们更好地使用 Git,并在遇到问题时知道如何解决。
核心特点
- 内容寻址:通过内容计算哈希值,相同内容产生相同哈希
- 不可变性:对象一旦创建,内容不可修改
- 完整性:通过哈希值验证数据完整性
- 高效性:相同内容只存储一份,通过引用共享
Git 的核心设计
内容寻址的文件系统
Git 的核心是一个键值对数据库 (key-value store),其工作流程如下:
┌─────────────────────────────────────┐
│ Git 内容寻址流程 │
├─────────────────────────────────────┤
│ 1. 计算哈希 (SHA-1) │
│ 2. 存储内容 (.git/objects/) │
│ 3. 通过哈希寻址 │
└─────────────────────────────────────┘
工作流程:
-
计算哈希:对一段内容(如文件、目录结构、提交信息)附加一个头部信息,计算出一个唯一的 SHA-1 哈希值(40位十六进制字符串)
-
存储内容 :将内容压缩后,以其 SHA-1 值的前两位为目录名、后38位为文件名,存储在
.git/objects/目录下 -
通过哈希寻址:之后,Git 便通过这个 SHA-1 哈希值来读取或引用该内容
存储结构:
.git/objects/
├── ab/
│ └── c123def456... (哈希值: abc123def456...)
├── cd/
│ └── e789f012... (哈希值: cde789f012...)
└── ...
键值对数据库
Git 本质上是一个键值对数据库:
- 键 (Key):SHA-1 哈希值
- 值 (Value):对象内容(blob、tree、commit、tag)
特点:
- ✅ 相同内容产生相同哈希,只存储一份
- ✅ 内容一旦存储,不可修改(不可变性)
- ✅ 通过哈希值可以验证内容完整性
- ✅ 高效的内容去重
Git 的四种核心对象
Git 的对象主要分为四类:blob、tree、commit 和 tag。其中前三者构成了版本控制的基础。
Blob:文件内容
作用 :仅存储文件内容,不包含文件名、权限等任何元数据。
特点:
- 内容相同,SHA-1 值就相同
- Git 只存储一份副本(内容去重)
- 不包含文件名信息
示例:
bash
# 将 "hello" 存入对象库,返回其 SHA-1 值
echo 'hello' | git hash-object -w --stdin
# 输出: ce013625030ba8dba906f756967f9e9ca394464a
# 查看对象类型
git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a
# 输出: blob
# 查看对象内容
git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
# 输出: hello
存储位置:
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
内容去重示例:
bash
# 创建两个内容相同的文件
echo "hello" > file1.txt
echo "hello" > file2.txt
# 添加到 Git
git add file1.txt file2.txt
# 查看对象,发现只存储了一份
git cat-file --batch-check --batch-all-objects | grep blob
# 两个文件指向同一个 blob 对象
Tree:目录结构
作用 :代表项目中的目录,记录了该目录下所有文件和子目录的列表。
内容:每条记录包含:
- 文件模式(如
100644普通文件,100755可执行文件,040000目录) - 对象类型(
blob或tree) - SHA-1 值
- 文件名
示例 :一个 tree 对象的内容可能如下:
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
这代表一个包含 README、Rakefile 文件和 lib 子目录的目录。
查看 tree 对象:
bash
# 查看根目录的 tree
git cat-file -p HEAD^{tree}
# 查看指定 tree 对象
git cat-file -p <tree-hash>
# 查看 tree 的类型
git cat-file -t <tree-hash>
文件模式说明:
| 模式 | 说明 | 示例 |
|---|---|---|
100644 |
普通文件 | README.md |
100755 |
可执行文件 | script.sh |
040000 |
目录 | src/ |
120000 |
符号链接 | link |
Commit:版本快照
作用 :代表项目在某个时间点的完整快照,并记录了提交元信息。
内容:
- 指向根目录
tree对象的 SHA-1 值 - 一个或多个父
commit的 SHA-1 值(首次提交无父提交,合并提交有多个父提交) - 作者、提交者信息及时间戳
- 提交信息(commit message)
示例 :一个 commit 对象的内容可能如下:
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
parent fdf4fc3344e67ab068f836878b6c4951e3b15f3d
author Alice <alice@example.com> 1617187200 +0800
committer Alice <alice@example.com> 1617187200 +0800
First commit
查看 commit 对象:
bash
# 查看 commit 对象
git cat-file -p <commit-hash>
# 查看 commit 的类型
git cat-file -t <commit-hash>
# 查看最新提交
git cat-file -p HEAD
提交链:
所有 commit 通过 parent 指针串联起来,形成一条可追溯的历史链:
C1 ← C2 ← C3 ← C4
合并提交:
合并提交有多个父提交:
C2 ← C3
/ \
C1 C4 (合并提交)
\ /
C5 ← C6
Tag:里程碑标签
作用 :为某个特定的 commit 对象(或其他对象)创建一个永久的、人类可读的别名 ,通常用于标记发布版本(如 v1.0.0)。
类型:
-
轻量标签 (Lightweight Tag):
- 直接指向
commit的指针 - 不包含额外信息
- 直接指向
-
附注标签 (Annotated Tag):
- 一个独立的
tag对象 - 包含打标签者信息、日期和消息
- 一个独立的
创建标签:
bash
# 创建轻量标签
git tag v1.0.0
# 创建附注标签
git tag -a v1.0.0 -m "Release version 1.0.0"
# 查看标签
git show v1.0.0
查看 tag 对象:
bash
# 查看 tag 对象内容
git cat-file -p v1.0.0
# 查看 tag 类型
git cat-file -t v1.0.0
对象关系图
一次 git commit 后,这几个对象的关系如下:
[Commit]
|
v
[Tree] (根目录)
/ | \
v v v
[Blob] ... [Tree] ... (子目录和文件)
(文件内容)
关系说明:
- Commit 指向项目根目录 的
tree - Tree 指向其包含的文件
blob和子目录tree - Blob 仅存储文件内容
完整示例:
Commit: abc123...
|
v
Tree: def456... (根目录)
|
+-- 100644 blob ghi789... README.md
|
+-- 100644 blob jkl012... main.py
|
+-- 040000 tree mno345... src/
|
+-- 100644 blob pqr678... utils.py
从 git add 到 git commit 的底层流程
1. git add:生成 Blob & 更新暂存区
流程:
- 读取工作区文件内容
- 生成
blob对象并存入.git/objects - 更新暂存区 (Index) ,使其指向这个新生成的
blob对象
暂存区 (Index):
- 位置:
.git/index - 格式:二进制文件
- 内容:记录了下一次提交的内容快照
- 作用:暂存待提交的文件
查看暂存区:
bash
# 查看暂存区内容
git ls-files --stage
# 查看暂存区的 tree
git write-tree
git cat-file -p $(git write-tree)
2. git commit:生成 Tree & Commit
流程:
-
生成 Tree :根据暂存区的内容,为当前目录及所有子目录生成新的
tree对象 -
生成 Commit :创建一个
commit对象,它指向根目录的tree,并指向上一个commit(形成历史链) -
更新分支 :将当前分支(如
main)的引用更新为这个新commit的 SHA-1 值
详细步骤:
bash
# 1. 生成 tree 对象(基于暂存区)
tree_hash=$(git write-tree)
# 2. 创建 commit 对象
commit_hash=$(echo "First commit" | git commit-tree $tree_hash)
# 3. 更新分支引用
git update-ref refs/heads/main $commit_hash
.git 目录核心结构
.git 目录是 Git 仓库的核心,包含所有版本控制信息:
.git/
├── objects/ # 对象存储目录
│ ├── ab/ # 哈希值前两位作为目录名
│ │ └── c123... # 哈希值后38位作为文件名
│ └── ...
├── refs/ # 引用目录
│ ├── heads/ # 分支引用
│ │ ├── main # main 分支指向的 commit
│ │ └── dev # dev 分支指向的 commit
│ └── tags/ # 标签引用
│ └── v1.0.0 # 标签指向的 commit
├── HEAD # 当前分支的符号引用
├── index # 暂存区(二进制文件)
├── config # 仓库配置
├── hooks/ # Git 钩子脚本
└── ...
objects/ 目录
存放所有 blob、tree、commit、tag 对象。
存储方式:
- 使用 SHA-1 哈希值的前两位作为目录名
- 后38位作为文件名
- 内容经过压缩(zlib)
查看对象:
bash
# 列出所有对象
find .git/objects -type f
# 查看对象类型和内容
git cat-file -t <hash> # 类型
git cat-file -p <hash> # 内容
git cat-file -s <hash> # 大小
refs/ 目录
存放各种引用(分支、标签等),如 refs/heads/main 指向 main 分支的最新 commit。
分支引用:
.git/refs/heads/main
内容: abc123def456... (commit 的 SHA-1 值)
标签引用:
.git/refs/tags/v1.0.0
内容: abc123def456... (commit 的 SHA-1 值)
查看引用:
bash
# 查看分支引用
cat .git/refs/heads/main
# 查看所有引用
git show-ref
# 查看分支指向的 commit
git rev-parse main
HEAD 文件
一个符号引用,指向当前检出的分支(如 ref: refs/heads/main)。
查看 HEAD:
bash
# 查看 HEAD 内容
cat .git/HEAD
# 输出: ref: refs/heads/main
# 查看 HEAD 指向的 commit
git rev-parse HEAD
分离 HEAD:
当检出到某个具体的 commit 时,HEAD 直接指向 commit:
bash
git checkout abc123
cat .git/HEAD
# 输出: abc123def456...
index 文件
二进制文件,即暂存区,记录了下一次提交的内容快照。
查看暂存区:
bash
# 查看暂存区内容
git ls-files --stage
# 查看暂存区的统计信息
git diff --cached --stat
分支的本质
Git 的分支本质上是一个指向某个 commit 的轻量级指针文件。
分支的存储
例如,refs/heads/main 文件里存储的就是 main 分支最新 commit 的 SHA-1 值:
bash
cat .git/refs/heads/main
# 输出: abc123def456789...
分支操作的本质
新建分支:
- 创建一个新文件(如
refs/heads/dev) - 内容指向当前
commit
提交:
- 在当前分支上提交时,Git 更新该分支文件
- 使其指向新的
commit
切换分支:
HEAD文件的内容被修改,指向新的分支引用- 工作区文件随之更新
分支关系图:
C1 ← C2 ← C3
↑ ↑
main dev
分支的优势
由于 commit 对象之间通过 parent 指针相连,分支的创建、合并、切换都非常高效,因为它们只是在移动指针。
创建分支:只需创建一个文件,指向当前 commit(几乎瞬间完成)
切换分支:只需修改 HEAD 和工作区文件(非常快速)
合并分支:只需创建一个新的 commit,指向两个父 commit
引用和符号引用
引用 (Reference)
引用是一个指向 commit 的指针,存储在 .git/refs/ 目录下。
类型:
- 分支引用:
refs/heads/<branch> - 标签引用:
refs/tags/<tag> - 远程引用:
refs/remotes/<remote>/<branch>
查看引用:
bash
# 查看所有引用
git show-ref
# 查看分支引用
git show-ref --heads
# 查看标签引用
git show-ref --tags
符号引用 (Symbolic Reference)
符号引用是一个指向另一个引用的引用,如 HEAD。
HEAD 的两种状态:
-
指向分支(正常状态):
HEAD → refs/heads/main → commit -
直接指向 commit(分离 HEAD):
HEAD → commit
查看符号引用:
bash
# 查看 HEAD 指向
git symbolic-ref HEAD
# 查看所有符号引用
find .git -type f -name "HEAD" -o -name "*HEAD*"
Git 对象存储机制
对象压缩
Git 使用 zlib 压缩算法存储对象,节省存储空间。
压缩效果:
- 文本文件:通常压缩率 50-70%
- 二进制文件:压缩率较低
对象打包
为了进一步提高效率,Git 会将多个对象打包成 pack 文件。
打包机制:
- 位置:
.git/objects/pack/ - 格式:
.pack文件(打包数据)+.idx文件(索引) - 触发:
git gc(垃圾回收)时自动打包
查看打包文件:
bash
# 查看 pack 文件
ls .git/objects/pack/
# 查看 pack 文件信息
git verify-pack -v .git/objects/pack/*.idx
对象去重
相同内容的文件只存储一份,通过引用共享。
去重示例:
bash
# 创建两个内容相同的文件
echo "hello" > file1.txt
echo "hello" > file2.txt
# 添加到 Git
git add file1.txt file2.txt
# 查看对象,发现只存储了一份
git cat-file --batch-check --batch-all-objects | grep blob
哈希算法与完整性
SHA-1 哈希
Git 使用 SHA-1 算法计算对象的哈希值。
特点:
- 40 位十六进制字符串
- 相同内容产生相同哈希
- 内容稍有改动,哈希值完全不同
- 不可逆(无法从哈希值反推内容)
计算示例:
bash
# 计算内容的哈希值
echo "hello" | git hash-object --stdin
# 输出: ce013625030ba8dba906f756967f9e9ca394464a
完整性验证
Git 通过哈希值验证对象完整性:
bash
# 验证对象完整性
git fsck
# 验证特定对象
git cat-file -t <hash>
git cat-file -p <hash>
损坏检测:
如果对象文件损坏,Git 会检测到哈希值不匹配,报告错误。
实际示例演示
示例 1:手动创建对象
bash
# 1. 创建 blob 对象
echo "Hello, Git!" | git hash-object -w --stdin
# 输出: a5c19667710254f4f5137305c3b1092e62643261
# 2. 查看 blob 对象
git cat-file -p a5c19667710254f4f5137305c3b1092e62643261
# 输出: Hello, Git!
# 3. 创建 tree 对象(需要先有 blob)
git update-index --add --cacheinfo 100644 \
a5c19667710254f4f5137305c3b1092e62643261 hello.txt
# 4. 写入 tree 对象
tree_hash=$(git write-tree)
echo $tree_hash
# 5. 创建 commit 对象
commit_hash=$(echo "Initial commit" | \
git commit-tree $tree_hash)
echo $commit_hash
# 6. 更新分支引用
git update-ref refs/heads/main $commit_hash
示例 2:查看对象关系
bash
# 查看最新提交
git cat-file -p HEAD
# 查看提交指向的 tree
git cat-file -p HEAD^{tree}
# 查看 tree 中的文件
git ls-tree HEAD
# 查看文件的 blob
git cat-file -p HEAD:README.md
示例 3:探索对象存储
bash
# 查看所有对象
find .git/objects -type f | head -10
# 查看对象类型分布
git cat-file --batch-check --batch-all-objects | \
awk '{print $2}' | sort | uniq -c
# 查看对象大小
git cat-file --batch-check --batch-all-objects | \
awk '{sum+=$3} END {print sum}'
总结
核心概念总结
- Blob :存文件内容
- Tree :存目录结构(文件名、子目录等)
- Commit :存项目快照 及历史 (
tree+parent+ 元信息) - 分支 :一个指向特定
commit的指针
Git 的设计优势
- 内容寻址:通过内容计算哈希,相同内容只存储一份
- 不可变性:对象一旦创建,内容不可修改
- 完整性:通过哈希值验证数据完整性
- 高效性:分支操作只是移动指针,非常快速
理解原理的价值
理解了这套对象模型,你就能明白:
reset、revert、merge等高级操作背后的原理- 为什么 Git 如此高效
- 如何解决复杂的问题
- 如何优化 Git 仓库