Files
Obsidian/博客/编程与工具/Git内部原理详解.md
T

59 KiB
Raw Blame History

Git内部原理详解

作者:刘航宇 更新日期2026年4月26日 预计阅读时间50分钟


第一章:引言——为什么要了解内部原理?

大多数开发者每天都在使用Git,但很少有人真正理解它的工作原理。你可能已经熟练掌握了addcommitpushpull等基本命令,能够处理日常的版本控制需求。但是,当遇到以下情况时,你是否会感到困惑?

  • 为什么git reset --hard可以撤销提交,却可能导致工作区代码丢失?
  • git rebasegit merge到底有什么区别?什么时候该用哪个?
  • 为什么相同的文件在Git中只存储一份?
  • git checkoutgit 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 initgit 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打包文件(稍后介绍)

这种存储方式有两个重要作用:

  1. 避免单目录文件过多:数万个对象如果都放在一个目录下,会导致文件系统性能下降
  2. 内容寻址:通过哈希值可以快速定位对象

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图示2HEAD与分支的关系

┌─────────────────────────────────────────────────────┐
│  .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对象

BlobBinary 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图示3Blob对象存储机制

┌─────────────────────────────────────────────────────┐
│  文件: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图示4Tree对象结构

┌─────────────────────────────────────────────────────┐
│  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图示5Commit对象

┌─────────────────────────────────────────────────────┐
│  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-1Secure 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图示7SHA-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-1SHA1("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图示11commit的物理过程

┌─────────────────────────────────────────────────────┐
│  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图示12Packfile中的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图示13checkout 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图示14reset三种模式对比

┌─────────────────────────────────────────────────────┐
│  初始状态                                            │
│                                                      │
│  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图示15merge 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图示16stash原理

┌─────────────────────────────────────────────────────┐
│  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的核心是一个内容寻址的文件系统。 一旦理解了这个,一切都会变得清晰。


全文完