59 KiB
Git内部原理详解
作者:刘航宇 更新日期:2026年4月26日 预计阅读时间:50分钟
第一章:引言——为什么要了解内部原理?
大多数开发者每天都在使用Git,但很少有人真正理解它的工作原理。你可能已经熟练掌握了add、commit、push、pull等基本命令,能够处理日常的版本控制需求。但是,当遇到以下情况时,你是否会感到困惑?
- 为什么
git reset --hard可以撤销提交,却可能导致工作区代码丢失? git rebase和git merge到底有什么区别?什么时候该用哪个?- 为什么相同的文件在Git中只存储一份?
git checkout和git switch命令有什么不同?
要回答这些问题,仅靠记住命令是不够的。我们需要深入Git的内部,理解它的核心设计思想。
理解Git内部原理的价值:
✅ 问题诊断:当Git行为异常时,你能快速定位原因
✅ 高级操作:理解rebase、cherry-pick等命令的底层机制
✅ 安全操作:避免误操作导致数据丢失,知道如何恢复
✅ 性能优化:理解packfile机制,明白何时Git会自动优化存储
✅ 调试能力:使用git cat-file、git ls-tree等底层命令调试
Git本质上是一个内容寻址的文件系统,理解这个核心概念,你就能理解Git的一切行为。在本文中,我们将从.git目录结构开始,深入剖析Git的四大核心对象(Blob、Tree、Commit、Tag),解析SHA-1哈希算法的工作原理,探讨引用(Refs)机制,最后深入了解一些高级命令的底层实现。
让我们开始这场Git内部世界的探索之旅。
第二章:.git目录结构——Git的家底
当你执行git init或git clone时,Git会在项目根目录创建一个隐藏的.git目录。这个目录就是Git的心脏,包含了版本控制所需的一切数据。理解.git目录的结构,是理解Git内部原理的第一步。
2.1 目录全览
让我们先看看一个典型Git仓库的.git目录结构:
.git/
├── HEAD # 当前分支指针
├── config # 仓库配置
├── description # 仓库描述
├── index # 暂存区(Staging Area)
├── objects/ # 所有Git对象存储(核心!)
│ ├── pack/ # 打包后的对象文件
│ └── info/ # 对象信息
├── refs/ # 所有引用
│ ├── heads/ # 本地分支
│ ├── tags/ # 标签
│ └── remotes/ # 远程追踪分支
├── info/ # 额外信息
└── hooks/ # Git钩子脚本
ASCII图示1:.git目录结构
┌─────────────────────────────────────────────────────┐
│ .git/ │
├─────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────┐ │
│ │ HEAD │ │
│ │ ref: refs/heads/main │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ config │ │ index │ │ description │ │
│ └──────────┘ └──────────┘ └────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ objects/ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │ │
│ │ │ 12/ │ │ ab/ │ │ pack/ │ │ │
│ │ │ (对象) │ │ (对象) │ │ (打包文件) │ │ │
│ │ └─────────┘ └─────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ refs/ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │
│ │ │ heads/ │ │ tags/ │ │ remotes/ │ │ │
│ │ │ (分支) │ │ (标签) │ │ (远程) │ │ │
│ │ └──────────┘ └──────────┘ └───────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ hooks/ │ │
│ │ (pre-commit, post-commit等钩子) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
2.2 objects目录详解
objects/目录是Git对象数据库的核心。Git的所有数据——文件内容、目录结构、提交记录——都以对象的形式存储在这里。
Git使用SHA-1哈希值的前两位作为目录名,其余38位作为文件名:
.git/objects/
├── 12/ # 哈希前两位:12
│ └── 3456789abcdef... # 哈希其余38位
├── ab/
│ └── cdef123456789... # 另一个对象
└── pack/ # Packfile打包文件(稍后介绍)
这种存储方式有两个重要作用:
- 避免单目录文件过多:数万个对象如果都放在一个目录下,会导致文件系统性能下降
- 内容寻址:通过哈希值可以快速定位对象
2.3 refs目录详解
refs/目录存储了Git的引用——指向Commit对象的指针。与对象的不可变性不同,引用是可以随时更新的。
.git/refs/
├── heads/ # 本地分支,每个分支一个文件
│ ├── main
│ └── develop
├── tags/ # 标签
│ └── v1.0.0
└── remotes/ # 远程追踪分支
└── origin/
└── main
每个引用文件的内容非常简单——只是一行SHA-1哈希值:
# 查看某个分支指向的commit
cat .git/refs/heads/main
# 输出:abc123def456789...(40位哈希值)
2.4 HEAD指针
HEAD是一个特殊引用,指向当前分支的最新提交。它告诉了Git"你现在在哪个位置"。
# 查看HEAD内容
cat .git/HEAD
# 输出:ref: refs/heads/main (指向main分支)
# 或者如果是 detached HEAD 状态
# 输出:abc123def456... (直接指向某个commit)
ASCII图示2:HEAD与分支的关系
┌─────────────────────────────────────────────────────┐
│ .git/HEAD │
│ ┌─────────────────────────────────────────────┐ │
│ │ ref: refs/heads/main │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ │ 解析 │
│ ↓ │
│ .git/refs/heads/main │
│ ┌─────────────────────────────────────────────┐ │
│ │ abc123def456... │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ │ 指向 │
│ ↓ │
│ Commit对象: abc123def456... │
│ ┌─────────────────────────────────────────────┐ │
│ │ tree: 3d拿到1f6a7c... │ │
│ │ parent: def456ghi... │ │
│ │ author: 刘航宇 │ │
│ │ message: feat: 添加新功能 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
第三章:对象模型——Git的核心
Git的核心设计是一个内容寻址的对象数据库。Git使用四种类型的对象来存储所有数据:Blob、Tree、Commit和Tag。理解这四种对象及其关系,是理解Git内部原理的关键。
3.1 Blob对象
Blob(Binary Large Object) 是Git中最基础的对象类型,用于存储文件的完整内容。
重要特性:Blob对象不存储文件名,只存储文件的内容。这意味着即使两个文件的内容完全相同,Git也只会存储一份Blob对象(因为它们有相同的SHA-1哈希值)。
让我们通过实际操作来理解Blob对象:
# 创建演示仓库
mkdir git-internals-demo
cd git-internals-demo
git init
# 创建文件并查看其SHA-1哈希
echo "Hello World" > hello.txt
git hash-object hello.txt
# 输出:8ab686eafeb1f44702738c8b0f24a25619336c2c
# 使用git cat-file查看Blob对象
git cat-file -t 8ab686ea
# 输出:blob
git cat-file -p 8ab686ea
# 输出:Hello World
ASCII图示3:Blob对象存储机制
┌─────────────────────────────────────────────────────┐
│ 文件:hello.txt │
│ 内容:"Hello World" │
│ │
│ git hash-object │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ SHA-1 Hash: 8ab686eafeb1f44702738c8b0... │ │
│ │ 计算方式: SHA1("blob 12\0Hello World") │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ↓ 存储到.objects/ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ .git/objects/8a/b686eafeb1f44702738c8b0... │ │
│ │ │ │
│ │ 实际存储(zlib压缩): │ │
│ │ "blob 12\0Hello World" │ │
│ │ ────── ─ ───────────── │ │
│ │ 类型 大小 内容 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ⚠️ 重要特性:Blob对象不存储文件名! │
└─────────────────────────────────────────────────────┘
Blob对象的存储格式非常简单:blob <文件大小>\0<文件内容>。这个格式在进行压缩后存储在.git/objects/目录下。
3.2 Tree对象
如果说Blob对象存储了文件内容,那么Tree对象就是存储目录结构的对象。Tree对象包含指向Blob和其他Tree的引用,每个引用包括文件名、文件权限和对象的SHA-1哈希值。
让我们创建一个更复杂的演示:
# 创建多文件目录结构
mkdir src
echo "print('hello')" > src/main.py
echo "#!/bin/bash" > script.sh
# 添加到暂存区并查看
git add .
git write-tree
# 输出:3d拿到1f6a7c8b9e2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a
# 查看Tree对象内容
git cat-file -t 3d拿到1f6
# 输出:tree
git cat-file -p 3d拿到1f6
# 输出示例:
# 100644 blob a1b2c3d4e5f6g7h8... script.sh
# 040000 tree e5f6g7h8i9j0k1l2... src
ASCII图示4:Tree对象结构
┌─────────────────────────────────────────────────────┐
│ Tree对象结构 │
│ │
│ 项目目录结构: │
│ . │
│ ├── script.sh │
│ └── src/ │
│ └── main.py │
│ │
│ 对应的Tree对象: │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Tree: 3d拿到1f6a7c (根目录) │ │
│ │ ───────────────────────────────────────────│ │
│ │ 100644 blob script.sh → Blob对象 │ │
│ │ 040000 tree src → Tree对象 │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ │ 指向 │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Tree: e5f6g7h8 (src目录) │ │
│ │ ───────────────────────────────────────────│ │
│ │ 100644 blob main.py → Blob对象 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Tree对象中的文件权限采用Unix风格:
100644:普通文件040000:目录(Tree对象)100755:可执行文件
3.3 Commit对象
Commit对象是整个对象模型的核心。它将Blob和Tree对象串联起来,形成一个完整的项目快照,同时包含提交元数据。
# 创建提交
git commit -m "Initial commit"
# 查看最新commit的SHA
git log --format="%H" -1
# 输出:abc123def456789...
# 查看Commit对象内容
git cat-file -t abc123
# 输出:commit
git cat-file -p abc123
输出示例:
tree 3d拿到1f6a7c8b9e2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a
parent
author 刘航宇 <3364451258@qq.com> 1704067200 +0800
committer 刘航宇 <3364451258@qq.com> 1704067200 +0800
feat: 初始化项目
ASCII图示5:Commit对象
┌─────────────────────────────────────────────────────┐
│ Commit对象 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ commit abc123def456789... │ │
│ │ ────────────────────────────────────────── │ │
│ │ │ │
│ │ tree 3d拿到1f6a7c... ← 指向项目快照的Tree │ │
│ │ parent ← 父提交(首次提交则为空)│ │
│ │ │ │
│ │ author 刘航宇 <xxx@qq.com> │ │
│ │ committer 刘航宇 <xxx@qq.com> │ │
│ │ date: 2024-01-01 10:00:00 │ │
│ │ │ │
│ │ message: │ │
│ │ feat: 初始化项目 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
3.4 对象关系图
现在我们可以看到四种对象是如何联系在一起的:
ASCII图示6:完整对象关系
┌──────────────┐
│ Tag │
│ v1.0.0 │
│ abc789... │
└──────┬───────┘
│ points to
↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Commit │────▶│ Tree │────▶│ Blob │
│ abc123... │ │ 3d拿到1f... │ │ main.py │
│ (当前提交) │ │ (根目录) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
│
│ parent
↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Commit │────▶│ Tree │────▶│ Blob │
│ def456... │ │ 7e8i9j... │ │ script.sh │
│ (父提交) │ │ (src目录) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
这是一个有向无环图(DAG)——每个Commit指向它的父提交,而Tree和Blob形成了项目的内容树。
第四章:SHA-1哈希——Git的基石
4.1 SHA-1简介
SHA-1(Secure Hash Algorithm 1)是Git内部无处不在的核心组成部分。Git中的一切对象都由其内容的SHA-1哈希值来标识。这个40位的十六进制字符串看起来像这样:
8ab686eafeb1f44702738c8b0f24a25619336c2c
4.2 哈希计算原理
Git计算SHA-1哈希的公式是:
SHA1 = SHA1("blob <文件大小>\0<文件内容>")
注意这个格式——它以对象类型和大小开头,这就是为什么相同内容的不同文件会得到相同的哈希值(因为Git只关心内容本身)。
让我们验证这一点:
# 手动计算SHA-1
echo -n "blob 12\0Hello World" | sha1sum
# 输出:8ab686eafeb1f44702738c8b0f24a25619336c2c -
# Git计算对比
git hash-object hello.txt
# 输出:8ab686eafeb1f44702738c8b0f24a25619336c2c ✓
ASCII图示7:SHA-1哈希计算过程
┌─────────────────────────────────────────────────────┐
│ SHA-1 Hash计算过程 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 输入内容: │ │
│ │ "blob 12\0Hello World" │ │
│ │ ──── ── ───────────── │ │
│ │ 类型 大小 文件内容 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ SHA-1算法 │
│ (160位输出) │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 输出:40位十六进制 │ │
│ │ 8ab686ea febe1f44 702738c8 b0f24a25 619336c2c│ │
│ │ ──────── ───────── ───────── ───────── ─────│ │
│ │ 前2位 │ │ │ │ │ │
│ │ 作为目录 │ ──────┴────────┴────────┘ │ │
│ │ 名 └─────── 其余38位作为文件名 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 最终存储路径: │ │
│ │ .git/objects/8a/b686eafeb1f44702738c8b0... │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
4.3 哈希的特性与价值
1. 内容寻址——相同内容只存储一次
# 创建两个内容相同的文件
echo "same content" > file1.txt
echo "same content" > file2.txt
git add .
git status
# 注意:只会显示一个新的blob,因为Git检测到相同内容
2. 完整性保证——任何修改都会改变哈希
# 原始文件的哈希
git hash-object hello.txt
# 输出:8ab686ea...
# 修改文件
echo "Hello World!" > hello.txt
git hash-object hello.txt
# 输出:a1b2c3d4... (完全不同!)
# 这意味着Git可以检测到任何文件损坏
3. 引用稳定性——提交哈希由内容决定
同一个提交在不同机器上会有相同的哈希值(前提是作者信息也相同)。这保证了分布式协作的一致性。
4.4 SHA-1的实际应用
# 使用哈希前缀引用对象(只需唯一前缀)
git show 8ab686ea
git log --oneline
# abc123d feat: 添加新功能
# 比较两个提交
git diff abc123d..def456e
# 查看某个文件的历史
git log --follow -p -- filename
第五章:引用(Refs)机制
引用(Refs)是Git中指向Commit对象的指针。与存储在.git/objects/中的不可变对象不同,引用文件存储在.git/refs/中,是可以随时更新的。
5.1 HEAD指针
HEAD是一个特殊引用,指向当前分支的最新提交。它告诉Git"你现在工作在哪个提交上"。
# 查看HEAD指向
cat .git/HEAD
# 输出:ref: refs/heads/main
# 如果是 detached HEAD 状态
# 输出:abc123def456...(直接指向某个commit)
# 解析HEAD指向的commit
git rev-parse HEAD
# 输出:abc123def456789...
git rev-parse --symbolic-full-name HEAD
# 输出:refs/heads/main
5.2 分支引用
创建新分支本质上就是创建一个新文件:
# 创建分支
git branch feature
# 查看分支引用文件
cat .git/refs/heads/feature
# 输出:abc123def456...(与当前HEAD指向相同)
# 查看所有本地分支
git branch
# 输出:
# feature
# * main
# (*表示当前分支)
ASCII图示8:创建分支的实质
┌─────────────────────────────────────────────────────┐
│ git branch feature 执行前: │
│ │
│ .git/refs/heads/main │
│ ┌─────────────────────────────────────────────┐ │
│ │ abc123def456... │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ Commit abc123 │
│ │
├─────────────────────────────────────────────────────┤
│ git branch feature 执行后: │
│ │
│ .git/refs/heads/main │
│ ┌─────────────────────────────────────────────┐ │
│ │ abc123def456... │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ├── .git/refs/heads/feature │
│ │ ┌─────────────────────────────────┐ │
│ └──▶│ abc123def456... │ │
│ └─────────────────────────────────┘ │
│ │
│ ⚠️ 创建分支只是创建一个文件,指向当前commit! │
└─────────────────────────────────────────────────────┘
5.3 远程引用和标签
.git/refs/
├── heads/main # 本地分支:main
├── remotes/origin/main # 远程追踪:origin/main
└── tags/v1.0.0 # 标签:v1.0.0
# 查看标签引用
cat .git/refs/tags/v1.0.0
# 输出:789abc123...
# 标签分为两种类型
# 轻量标签:只是一个指向commit的引用
# 附注标签:是一个完整的Tag对象
# 查看标签对象
git cat-file -t v1.0.0
# 如果是附注标签,类型是"tag"
# 如果是轻量标签,类型是"commit"
ASCII图示9:引用层级
┌─────────────────────────────────────────────────────┐
│ 引用层级结构 │
│ │
│ .git/HEAD │
│ ┌─────────────────────────────────────────────┐ │
│ │ ref: refs/heads/main │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ↓ resolves to │
│ .git/refs/heads/main │
│ ┌─────────────────────────────────────────────┐ │
│ │ abc123def456... │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ↓ points to │
│ ┌─────────────────────────────────────────────┐ │
│ │ Commit: abc123def456... │ │
│ │ │ │
│ │ tree: 3d拿到1f6a7c... │ │
│ │ parent: def456ghi... │ │
│ │ message: feat: 添加新功能 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
第六章:工作区、暂存区、仓库的物理交互
6.1 三大区域定义
Git有三个核心工作区域:
- 工作区(Working Directory):你实际编辑文件的地方
- 暂存区(Staging Area / Index):准备提交的文件快照
- 仓库(Repository):存储所有历史记录的数据库
ASCII图示10:三大区域关系
┌─────────────────────────────────────────────────────┐
│ 你的电脑 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ │ │
│ │ 工作区 暂存区 仓库 │ │
│ │ Working Dir git add Index git │ │
│ │ ┌────────┐ ──────────▶ ┌────────┐ commit│ │
│ │ │hello.txt│ │ Index │ ────▶│ │
│ │ │main.py │ ◀────────── │.git/ │ │ │
│ │ │script │ git reset │index │ │ │
│ │ └────────┘ └────────┘ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └─────────┼─────────────────────┼────────────┘ │
│ │ │ │
│ └─────────────────────┘ │
│ │
│ 物理位置:项目文件夹 .git/index .git/ │
│ (文件) objects/ │
└─────────────────────────────────────────────────────┘
6.2 git add 的物理过程
当执行git add时,Git做了以下事情:
# 演示git add的内部过程
echo "new content" > test.txt
# 添加前:检查对象是否存在
find .git/objects -name "*test*" 2>/dev/null
# 无输出
# 执行add
git add test.txt
# 添加后:检查暂存区
git ls-files --stage test.txt
# 输出:100644 abc123def456... 0 test.txt
# ──── ──────────────── ─
# 权限 SHA-1哈希 暂存序号
物理过程分解:
1. 读取文件 "new content"
2. 计算SHA-1:SHA1("blob 12\0new content")
3. 创建压缩对象文件:.git/objects/ab/cdef123...
4. 更新 Index(.git/index 文件)
6.3 git commit 的物理过程
执行git commit时,Git做了更复杂的操作:
# 1. 根据Index创建Tree对象
# 2. 创建Commit对象
# 3. 更新分支引用
# 4. 更新HEAD指针
# 演示commit过程
git status
# 执行commit
git commit -m "Add test.txt"
# 查看新建的commit
git cat-file -p HEAD
# 输出:
# tree 3d拿到1f6a7c...
# parent abc123def...
# author 刘航宇 ...
# committer 刘航宇 ...
#
# Add test.txt
# 查看分支引用是否更新
cat .git/refs/heads/main
# 输出:新commit的SHA值
ASCII图示11:commit的物理过程
┌─────────────────────────────────────────────────────┐
│ git commit 的物理过程 │
│ │
│ Step 1: 根据Index创建Tree对象 │
│ ┌─────────────────────────────────────────────┐ │
│ │ Index (.git/index) │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ blob test.txt → abc123... │ │ │
│ │ │ blob hello.txt → 8ab686... │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ git write-tree │ │ │
│ │ ↓ │ │
│ │ .git/objects/ │ │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ Tree对象: 3d拿到1f6... │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ Step 2: 创建Commit对象 │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Commit对象: def456... │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ tree: 3d拿到1f6... │ │ │
│ │ │ parent: abc123... (上一提交) │ │ │
│ │ │ author/committer: 刘航宇 │ │ │
│ │ │ message: Add test.txt │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ Step 3: 更新分支引用 │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ refs/heads/main → def456... (新commit) │ │
│ │ HEAD → ref: refs/heads/main (保持指向) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
6.4 git status 的原理
git status通过比较三个位置来判断文件状态:
git status -s
# ?? = 未跟踪(Index中没有,工作区有)
# A = 新添加(Index中有,HEAD中没有)
# M = 已修改(Index与HEAD不一致)
# D = 已删除(Index有,HEAD无)
# R = 已重命名
状态判断逻辑:
┌─────────────────────────────────────────────────────┐
│ git status 判断逻辑 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ HEAD │ vs │ Index │ vs │ 工作区 │ │
│ │ (仓库) │ │ (暂存区) │ │ (文件) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ↓ │
│ 比较结果决定状态: │
│ │
│ 全部相同 → nothing to commit (干净) │
│ 只有工作区变 → M (修改未暂存) │
│ 工作区+暂存区变 → M (已暂存待提交) │
│ 新文件在工作区 → ?? (未跟踪) │
└─────────────────────────────────────────────────────┘
第七章:Packfile机制
7.1 为什么要打包?
Git创建对象时,每个对象都是独立存储的。这在项目较小时没问题,但随着项目增长:
- 大量小文件会占用很多磁盘空间
- 文件系统目录下的文件数量会影响性能
- 网络传输大量小对象效率低下
Packfile是Git解决这个问题的方式——它将多个对象打包成一个文件,使用Delta压缩来节省空间。
7.2 何时触发打包?
# 自动触发打包的情况:
# 1. 对象数量超过7500个
# 2. 运行 git gc 或 git gc --auto
# 3. 推送代码到远程仓库
# 4. 执行 git bundle
# 查看pack目录
ls .git/objects/pack/
# 初始为空,打包后会出现 .idx 和 .pack 文件
# 手动触发打包
git gc
# 输出示例:
# Counting objects: 100, done.
# Delta compression using 4 threads.
# Compressing objects: 100% (50/50), done.
# Writing objects: 100% (100/100), done.
7.3 Delta压缩原理
Packfile使用两种压缩策略:
1. 完整对象存储:每个新提交的基础版本 2. Delta压缩:只存储与基础版本的差异
ASCII图示12:Packfile中的Delta压缩
┌─────────────────────────────────────────────────────┐
│ 原始存储(未打包) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ v1.py │ │ v2.py │ │ v3.py │ │
│ │ 100KB │ │ 101KB │ │ 102KB │ │
│ │ 完整存储 │ │ 完整存储 │ │ 完整存储 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ 总计:303KB │
│ │
├─────────────────────────────────────────────────────┤
│ 打包后(Delta压缩) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ v1.py │ │ delta1 │ │ delta2 │ │
│ │ 100KB │ │ ~1KB │ │ ~1KB │ │
│ │ 完整存储 │ │ v1→v2 │ │ v2→v3 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ 总计:~102KB (节省约66%) │
│ │
│ 读取v3时:v1 + delta1 + delta2 = v3 │
└─────────────────────────────────────────────────────┘
7.4 Packfile的结构
每个packfile包含两个文件:
.git/objects/pack/
├── pack-abc123def...idx # 索引文件(快速查找)
└── pack-abc123def...pack # 数据文件(实际压缩数据)
# 查看packfile信息
git verify-pack -v .git/objects/pack/pack-abc123...pack
# 输出示例:
# abc123... blob 100 file.txt
# def456... tree 50 (root tree)
# ...
第八章:高级命令的底层原理
8.1 git checkout vs git switch
# git checkout <branch>(旧方式)
# 功能:切换分支 + 更新工作区 + 更新暂存区
# git switch <branch>(新方式,Git 2.23+)
# 功能:只切换分支
ASCII图示13:checkout vs switch
┌─────────────────────────────────────────────────────┐
│ git checkout feature │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 更新 HEAD → feature分支 │ │
│ │ 2. 更新 Index → feature的暂存区 │ │
│ │ 3. 更新工作区 → feature的文件内容 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ git switch feature │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 更新 HEAD → feature分支 │ │
│ │ 2. 不改变 Index 和工作区 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ⚠️ checkout会丢弃未提交的修改,switch更安全 │
└─────────────────────────────────────────────────────┘
8.2 git reset 的三种模式
git reset是Git中最强大的命令之一,但它也是最容易误用的命令。理解它的三种模式能帮助你安全地使用它。
| 模式 | --soft | --mixed | --hard |
|---|---|---|---|
| HEAD | 移动 | 移动 | 移动 |
| Index | 保持 | 重置 | 重置 |
| 工作区 | 保持 | 保持 | 重置 |
# 场景:假设你有以下提交历史
# A --- B --- C (main, HEAD)
# --soft:只移动HEAD,保留Index和工作区
git reset --soft HEAD~1
# 结果:
# A --- B --- C (HEAD)
# ↑
# main指向B,但C的修改还在Index和工作区
# --mixed(默认):移动HEAD,重置Index
git reset HEAD~1
# 结果:
# A --- B --- C (Index被重置为B的状态)
# ↑
# HEAD指向B,C的修改只在工作区
# --hard(危险!):移动HEAD,重置Index和工作区
git reset --hard HEAD~1
# 结果:
# A --- B (HEAD, main)
# C的修改完全丢失!
ASCII图示14:reset三种模式对比
┌─────────────────────────────────────────────────────┐
│ 初始状态 │
│ │
│ HEAD → Commit C (当前) │
│ Index → [C] │
│ 工作区 → [C的内容] │
│ │
├─────────────────────────────────────────────────────┤
│ git reset --soft HEAD~1 │
│ │
│ HEAD → Commit B (前一个) │
│ Index → [C] (保持) │
│ 工作区 → [C的内容] (保持) │
│ │
│ 效果:可以重新提交,相当于"撤销最后一次提交" │
│ │
├─────────────────────────────────────────────────────┤
│ git reset --mixed HEAD~1 (默认) │
│ │
│ HEAD → Commit B │
│ Index → [B] (重置为B的状态) │
│ 工作区 → [C的内容] (保持) │
│ │
│ 效果:保留修改在文件,解除暂存状态 │
│ │
├─────────────────────────────────────────────────────┤
│ git reset --hard HEAD~1 (危险) │
│ │
│ HEAD → Commit B │
│ Index → [B] (重置) │
│ 工作区 → [B的内容] (重置) ⚠️ │
│ │
│ 效果:C的修改完全丢失!无法恢复! │
└─────────────────────────────────────────────────────┘
8.3 git merge vs git rebase
这是Git中最容易被误解的一对命令。理解它们的历史管理方式,能帮助你选择合适的策略。
Merge(合并):
- 创建一个新的"合并提交",有两个父提交
- 保留完整的分支历史
- 不会改变现有提交
Rebase(变基):
- 将当前分支的提交"重新应用"到目标分支上
- 创建新的提交(SHA值改变)
- 产生更线性的历史
# 初始状态
# main: A --- B --- C
# └--- D --- E feature
# Merge
git checkout main
git merge feature
# 结果:
# main: A --- B --- C --------- F
# └--- D --- E --------
# ↖ merge commit
# feature: D --- E (保持不变)
# Rebase
git checkout feature
git rebase main
# 结果:
# main: A --- B --- C
# └--- D' --- E' feature
# (D和E被重新应用到C之上,创建了新的提交)
ASCII图示15:merge vs rebase
Merge(保留完整历史,包括合并点):
feature: D --- E
/ \
main: A --- B --- C - F
↖ F是合并提交,有两个父提交
Rebase(创建线性历史):
main: A --- B --- C
\
feature: D' --- E'
(D和E被"复制"应用到C之后,原来的D和E不再属于feature分支)
何时使用哪个?
✅ 使用 merge 的情况:
- 合并到 main/develop 等主分支
- 需要保留完整的分支历史
- 团队成员需要看到完整的开发轨迹
✅ 使用 rebase 的情况:
- 整理本地未推送的提交
- 保持分支历史线性
- pull --rebase 拉取远程更新
❌ 永远不要 rebase:
- 已经推送的公共分支
- 其他人正在工作的分支
- 会改变历史,导致协作混乱
8.4 git stash 的原理
git stash是Git的"临时储物柜"——它将当前工作区的修改保存起来,以便稍后恢复。
# 暂存当前修改
git stash
# 输出:Saved working directory and index state WIP on feature/xxx:
# 查看暂存列表
git stash list
# 输出:stash@{0}: WIP on feature/xxx: abc123 feat: 添加功能A
# 恢复暂存
git stash pop # 恢复并删除暂存
git stash apply # 恢复但保留暂存
ASCII图示16:stash原理
┌─────────────────────────────────────────────────────┐
│ git stash 执行过程 │
│ │
│ 状态1:工作区有修改 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ HEAD │ │ Index │ │ 工作区 │ │
│ │ [提交C] │ │ [C+修改] │ │ [C+修改] │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │
│ git stash │ │
│ ↓ │
│ 状态2:工作区恢复到HEAD状态 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ HEAD │ │ [提交C] │ │ [提交C] │ │
│ │ [提交C] │ │ (清空) │ │ (清空) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │
│ ↓ │
│ 创建stash对象(一个commit-like对象) │
│ ┌─────────────────────────────────────────────┐ │
│ │ stash@{0}: │ │
│ │ - Tree: 工作区+Index的快照 │ │
│ │ - Parent: HEAD commit │ │
│ │ - Message: WIP on feature/xxx... │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
第九章:总结
核心概念回顾
┌─────────────────────────────────────────────────────┐
│ Git内部原理核心知识图谱 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 对象模型 │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Blob │ │Tree │ │Commit│ │ Tag │ │ │
│ │ │文件 │ │目录 │ │快照 │ │版本 │ │ │
│ │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │
│ │ └─────────┴────────┴────────┘ │ │
│ └─────────────────┬───────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ SHA-1 哈希 │ │
│ │ 内容寻址 + 完整性保证 + 幂等性 │ │
│ └─────────────────┬───────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 引用机制 │ │
│ │ HEAD + 分支 + 标签 + 远程追踪 │ │
│ └─────────────────┬───────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 工作区域 │ │
│ │ 工作区 ←→ 暂存区 ←→ 仓库 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
理解Git内部原理的实践价值
✅ 问题诊断
- 理解 git status 输出的含义
- 理解冲突的本质和解决方法
- 知道 git reset --hard 的危险
✅ 高级操作
- 安全使用 git rebase
- 理解 git bisect 的工作方式
- 使用 git reflog 恢复误删的提交
✅ 性能优化
- 理解 git gc 的作用
- 知道何时Git会自动打包
- 优化仓库存储
✅ 安全操作
- 知道什么操作是安全的,什么是危险的
- 知道如何恢复误操作
- 避免数据丢失
继续学习
理解Git的内部原理后,你可以进一步探索:
📚 进阶主题
- Git钩子(Hooks):自动化工作流程
- 子模块(Submodules):管理多仓库依赖
- git bisect:自动化问题定位
- Git内部命令:git cat-file, git ls-tree, git merge-base
结语
Git的设计哲学是简单、强大、可信赖。理解它的内部原理,不仅能帮助你更好地使用这个工具,更能让你体会到优秀软件设计的魅力。
Blob、Tree、Commit、Tag这四种简单的对象类型,通过SHA-1哈希连接起来,就构成了一个强大的版本控制系统。这个设计如此优雅,以至于15年后的今天,Git仍然是版本控制领域的标准。
记住:Git的核心是一个内容寻址的文件系统。 一旦理解了这个,一切都会变得清晰。
全文完