git-03 Git原理
1. Git 的本质:内容寻址文件系统
Git 本质上是一个以内容寻址的文件系统,在此之上构建了版本控制功能。
所有数据以对象形式存储在 .git/objects/ 目录下,每个对象用其内容的 SHA-1 哈希值(40位十六进制)命名。
.git/
├── HEAD # 当前所在分支/提交
├── config # 仓库配置
├── index # 暂存区
├── objects/ # 所有 Git 对象
│ ├── info/
│ └── pack/
└── refs/ # 引用(分支、标签)
├── heads/ # 本地分支
├── remotes/ # 远程分支
└── tags/ # 标签
2. 四种对象类型
2.1 Blob(文件内容)
存储文件内容,不包含文件名。相同内容的文件只存储一份。
echo "hello git" | git hash-object -w --stdin
git cat-file -p 8d0e41
git cat-file -t 8d0e41
对象存储路径:.git/objects/8d/0e41234f24b6da...(前2位为目录名)
2.2 Tree(目录结构)
存储目录结构,包含文件名、权限及指向 blob/tree 的引用。
git cat-file -p HEAD^{tree}
格式:<mode> <type> <SHA-1>\t<name>
| mode | 说明 |
|---|---|
100644 |
普通文件 |
100755 |
可执行文件 |
120000 |
符号链接 |
040000 |
目录(tree) |
160000 |
Submodule(gitlink) |
2.3 Commit(提交)
存储一次提交的元数据:作者、时间戳、提交消息,以及指向 tree 和父提交的引用。
git cat-file -p HEAD
# tree 9b1a2f3... ← 指向根 tree
# parent a1b2c3... ← 父提交(merge 提交有两个 parent)
# author Liu <l@x.com> 1710000000 +0800
# committer Liu <l@x.com> 1710000000 +0800
#
# feat: add login feature
提交链:
commit C → commit B → commit A (初始提交,无 parent)
| | |
tree T3 tree T2 tree T1
2.4 Tag(标签对象)
附注标签(annotated tag)会创建 tag 对象,包含标签者、时间、消息及指向被标记对象的引用。
git cat-file -p v1.0.0
# object a1b2c3...
# type commit
# tag v1.0.0
# tagger Liu <l@x.com> 1710000000 +0800
#
# Release version 1.0.0
轻量标签只是一个引用(ref),不创建 tag 对象。
3. 对象存储结构
Git 存储对象的格式:
内容 = "<type> <size>\0<data>"
SHA1 = sha1(内容)
存储 = zlib压缩(内容)
路径 = .git/objects/<SHA1前2位>/<SHA1后38位>
python3 -c "
import zlib, sys
data = open('.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f', 'rb').read()
print(zlib.decompress(data))
"
4. 引用(References)
引用是指向 SHA-1 的具名指针,存储在 .git/refs/ 下。
cat .git/refs/heads/main
cat .git/HEAD
HEAD 的两种状态:
cat .git/HEAD
cat .git/HEAD
4.1 引用更新过程
git commit 执行时:
1. 将暂存区生成 tree 对象
2. 创建 commit 对象(包含 tree SHA + parent SHA + 元数据)
3. 更新 HEAD 指向的分支引用到新 commit SHA
5. 暂存区(Index)
.git/index 是一个二进制文件,记录了暂存区的状态:
- 文件路径
- 文件权限
- 对应 blob 的 SHA-1
- stat 信息(mtime、ctime、size 等,用于快速检测文件是否变更)
git ls-files --stage
git status 的工作原理:
- 比较工作区文件的 stat 与 index 中记录的 stat → 判断是否有未暂存修改
- 比较 index 中的 SHA-1 与 HEAD commit 中 tree 的 SHA-1 → 判断是否有已暂存修改
6. 分支的本质
分支只是一个文件,内容是 commit 的 SHA-1。
cat .git/refs/heads/main
echo "a1b2c3d4e5f6..." > .git/refs/heads/new-branch
这就是为什么 Git 分支创建如此廉价——只需写入40字节。
6.1 merge 的本质
Fast-forward merge: 直接移动分支指针,不创建新对象。
Three-way merge: 找到两分支的公共祖先,生成合并提交。
A──B──C (main)
/
base─
\
D──E──F (feature)
合并时:
- 找公共祖先 base
- 对比 base→C 的变更和 base→F 的变更
- 合并变更,冲突需手动解决
- 创建新提交 M(有两个 parent:C 和 F)
6.2 rebase 的本质
rebase 不是"移动提交",而是重新应用变更:
原始:
main: A──B──C
\
feature: D──E
rebase feature onto main:
1. 找到 feature 和 main 的公共祖先(B)
2. 把 feature 上 B 之后的提交(D、E)生成 patch
3. 在 main 的 C 上依次应用这些 patch,生成新提交 D'、E'
4. 移动 feature 分支指针到 E'
result:
main: A──B──C
\
feature: D'──E'
D’ 和 D 的内容相同,但 SHA-1 不同(parent 不同)。
7. Pack 文件
随着提交增多,松散对象(loose objects)数量会很多,Git 会自动将它们打包为 pack 文件以节省空间。
git gc
ls .git/objects/pack/
Pack 的压缩策略:
Git 不是存储每个版本的完整文件,而是存储差量(delta):
版本1(完整): blob 完整内容(base)
版本2: delta = 版本2 相对版本1 的差异
版本3: delta = 版本3 相对版本2 的差异
查找对象时通过 .idx 索引快速定位,再根据 delta 链重建完整内容。
git count-objects -vH
8. 底层命令(Plumbing Commands)
Git 分为两类命令:
- Porcelain(瓷器):用户友好的高级命令(
commit、merge、push) - Plumbing(管道):底层命令,供脚本调用
git hash-object -w file.txt # 写入 blob 对象,返回 SHA-1
git cat-file -t <sha> # 查看对象类型
git cat-file -p <sha> # 查看对象内容
git cat-file -s <sha> # 查看对象大小
git ls-tree HEAD # 查看 HEAD 的 tree
git write-tree # 将暂存区写成 tree 对象
git commit-tree <tree> -p <parent> -m "msg" # 手动创建 commit
git update-ref refs/heads/main <sha> # 更新引用
git symbolic-ref HEAD # 查看/设置符号引用
git ls-files --stage # 查看暂存区
git update-index --add file.txt # 手动添加到暂存区
git diff-tree -p HEAD # HEAD 的 tree diff
git diff-index HEAD # 暂存区与 HEAD 的 diff
手动创建一次提交(不用 git commit):
blob=$(echo "hello" | git hash-object -w --stdin)
git update-index --add --cacheinfo 100644,$blob,hello.txt
tree=$(git write-tree)
commit=$(git commit-tree $tree -m "manual commit")
git update-ref refs/heads/main $commit
9. 数据完整性
Git 用 SHA-1 保证数据完整性:
- 每个对象的名称就是其内容的哈希值
- 修改任何内容会产生不同的 SHA-1
- Commit 包含 tree 的 SHA-1,tree 包含 blob 的 SHA-1,形成哈希链
- 任何历史记录的篡改都会导致 SHA-1 不匹配,立即被检测到
git fsck
git fsck --full
10. 垃圾回收
某些操作会产生"悬挂对象"(dangling objects)——没有任何引用指向它们:
git reset --hard:旧提交变为悬挂git commit --amend:旧提交变为悬挂- 删除分支:该分支上未合并的提交变为悬挂
这些对象默认保留 90 天(通过 reflog 可恢复),之后被 git gc 清理。
git fsck --lost-found
git show a1b2c3
git reflog expire --expire=now --all
git gc --prune=now
11. 总结:一次 git commit 发生了什么
git commit -m "feat: add login"
1. 读取暂存区(.git/index)
↓
2. 为每个暂存的文件创建/复用 blob 对象
↓
3. 递归创建目录的 tree 对象
↓
4. 创建 commit 对象
- 包含根 tree 的 SHA-1
- 包含父提交的 SHA-1
- 包含作者/提交者信息
- 包含提交消息
↓
5. 更新 HEAD 指向的分支引用
.git/refs/heads/main → 新 commit SHA-1
整个过程:写对象(不可变) + 移动指针,这正是 Git 快速、安全、可回溯的根本原因。
xingliuhua