跳到主要内容

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 有四种基本对象类型:blobtreecommittag。理解这些对象是掌握 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 位作为文件名。这样做有两个好处:

  1. 避免单个目录下文件过多,影响文件系统性能
  2. 便于通过前缀快速定位对象

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 对象的存储格式如下:

  1. 构造头部:对象类型 内容长度\0
  2. 拼接头部和内容
  3. 计算 SHA-1 哈希值
  4. 使用 zlib 压缩
  5. 存储到 .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...

标签引用

标签分为两种:

  1. 轻量标签:与分支引用类似,只是存储在 refs/tags/ 目录
  2. 附注标签:创建 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 fetchgit 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 在以下情况会触发打包:

  1. 手动执行 git gc
  2. 推送到远程时
  3. 仓库中松散对象过多时

打包的核心思想是增量存储:Git 会找出相似的对象,只存储它们的差异(delta):

# 手动触发打包
$ git gc

# 查看打包结果
$ ls .git/objects/pack/
idx-abc123.idx # 索引文件
pack-abc123.pack # 打包文件

Pack 文件的优势

  1. 节省空间:相似对象只存储差异
  2. 减少文件数量:大量小文件变成一个大文件
  3. 网络传输高效:推送时只传输 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 哈希来标识对象,这提供了两个重要保证:

  1. 内容寻址:相同的内容总是产生相同的哈希值
  2. 完整性校验:任何对内容的修改都会产生不同的哈希值

虽然 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 协议:

  1. 哑协议(Dumb HTTP):简单的文件传输,效率低
  2. 智能协议(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 addgit commitgit push
  • git branchgit checkoutgit merge
  • git loggit statusgit 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 的内部原理:

  1. 内容寻址文件系统:Git 的核心设计
  2. 四种对象类型:blob(文件内容)、tree(目录结构)、commit(提交)、tag(标签)
  3. 引用系统:分支、标签、HEAD 都是指向对象的引用
  4. Pack 文件:增量存储机制,节省空间和传输带宽
  5. 数据恢复:reflog、fsck 和 pack 文件都可以用来恢复丢失的数据
  6. 传输协议:本地、HTTP、SSH、Git 四种协议的特点
  7. 底层命令:直接操作 Git 内部结构的命令

理解这些原理能帮助你更好地使用 Git,也能在遇到问题时更有效地解决。

参考资料