<!-- title: 分支只是个指针：Git凭什么存快照不存差异 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=U8cSIqoc3DI -->

# 分支只是个指针：Git 凭什么存快照不存差异

## 导语

我们每天都在 `git add`、`git commit`、`git push`，但绝大多数开发者从未真正回答过一个要命的问题：**Git 到底往 `.git/` 目录里写了什么？** 很多人脑子里有一个根深蒂固的直觉——版本控制嘛，不就是记录每次改了哪几行，存一堆 diff（差异）吗？这套心智模型来自 CVS、SVN 时代的"增量存储"，看起来理所当然，却是理解 Git 最大的绊脚石。

Concept && Coding 频道（24.4 万订阅）这期 53 分钟的《Git Architecture | How Git works internally》之所以值得一看，正是因为它从底层把 Git 拆开给你看：Git 根本不是存 diff，它存的是**不可变的内容快照（snapshot）**；而所谓的"分支"，本质只是一个 41 字节的指针文件。一旦你理解了 Blob / Tree / Commit 这三种对象和它们组成的 Merkle DAG，那些过去靠死记硬背的命令——`reset` 和 `revert` 有啥区别、`rebase` 为啥能"重写历史"、`cherry-pick` 凭什么能单独搬一个提交——会瞬间变得不证自明。本文基于该视频的章节结构（01:30 Blob、10:01 Tree、27:47 Commit），结合 Git 源码与官方文档，把这个反直觉的内核彻底讲透。

> 说明：该视频发布于 2026-05-14，为频道深度讲解，部分内容为会员专享；想看公开同主题视频，可在 YouTube 搜索 *Jafer Sabir - Git Architecture Explained*（`z2AcRK0SYyM`）等大量免费讲解。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Git Architecture \| How Git works internally |
| 频道 | Concept && Coding — by Shrayansh（约 24.4 万订阅） |
| 时长 | 53 分 44 秒 |
| 发布日期 | 2026-05-14 |
| 核心主题 | Git 的内容寻址对象模型：Blob / Tree / Commit |

## 核心内容拆解

### 1. 真相：Git 是一个"内容寻址文件系统"

视频开篇就抛出了一个 Linus Torvalds 自己定调的观点：**Git 的底层并不是一个版本控制系统，而是一个愚蠢的内容追踪器（stupid content tracker），本质上是一个 key-value 数据存储。** 这不是修辞，而是字面意义上的架构事实。

所谓"内容寻址（content-addressable）"，指的是：你存进去的每一块数据，它的"地址"（key）就是它自己内容的哈希值。具体来说，Git 对一块内容计算 `SHA-1`（正在向 `SHA-256` 迁移），得到的 40 位十六进制字符串，既是这块内容的**身份证**，也是它在 `.git/objects/` 里的**存储地址**。内容变一个字节，哈希就彻底改变——这就从机制上保证了"不可篡改"。

我的延伸：这种"用内容的指纹当地址"的设计，其实是函数式编程里 **persistent data structure（持久化数据结构）** 的思想——旧版本永不修改，新版本通过共享未变部分来构建。Git 的几乎所有"魔法"（秒级分支、廉价合并、完整性能验证）都源于这一个底层选择。理解了这点，你就明白为什么 Git 能在本地完整保存整个项目历史而不臃肿：它大量复用未改动的对象。

### 2. 颠覆直觉的核心：Git 存的是快照，不是 diff

这是全片最关键的认知翻盘。视频强调：**每次 `git commit`，Git 存的不是"这次改了哪几行"，而是项目在那一刻的完整快照。** 这与传统 VCS 的 delta 存储完全相反。

那岂不是会膨胀？不会。因为 Git 用上一节说的共享机制：一个文件如果这一版没改，新的快照就**直接复用**旧的那个 blob 对象，而不会重新存一份。真正发生的事是：commit 指向一棵新的 tree，这棵 tree 里大部分叶子都指向和上一版相同的旧 blob，只有真正改过的文件指向新建的 blob。所以从存储看它是"增量"的，但从模型看它是"全量快照"的——**增量是存储优化，快照是逻辑真相**。

为什么这个区分重要？因为它解释了 Git 操作的速度和语义。`git diff` 不是去"读取存的 diff"，而是**临时算**两个快照之间的差异；`git checkout` 是把某个完整快照铺到工作区，而不是"把 diff 依次回放"。一旦你以为 Git 存的是 diff，就会错误地预期"回退要逐步撤销"，但实际上跳到任何一个历史快照都是 O(1) 级别的指针移动。

### 3. Blob 对象：内容与文件名的彻底分离

视频第 01:30 起讲 Blob，点出了一个最容易让人困惑的细节：**blob 只存文件内容，根本不存文件名。** 文件名是被放在 tree 里的。

一个 blob 的实际构造是这样的：Git 在你的文件内容前面加一个 header——`blob <字节数>\0`（`\0` 是空字节分隔符），然后对这整个串算 SHA-1，得到对象名，再用 zlib 压缩后写进 `.git/objects/`。注意全程没有文件名的影子。

这个设计带来一个强大的副作用：**自动去重**。假设你项目里有三个文件 `a.txt`、`b.txt`、`copy.txt`，内容完全一样，那么不管它们叫什么名字、在哪个目录，Git 只会存**一个** blob——因为内容相同，SHA-1 必然相同。我见过有人在仓库里误传一份 200MB 的重复日志，结果 `.git` 体积几乎没涨，就是这个原因。内容即地址，天然把"相同的东西"坍缩成一份。

### 4. Tree 对象：目录结构就是一棵树

blob 没有文件名，那文件名和目录层级在哪？答案在 **tree 对象**（视频 10:01）。tree 本质是一个目录的清单：它的每一行记录是一个三元组——`<mode> <文件名> <对象SHA-1>`，并且这条记录可以指向一个 blob（文件）或者另一个 tree（子目录）。

换句话说，**tree 把"文件名 + 目录结构"这层信息单独编码成一个对象**。一棵 root tree 完整描述了项目根目录那一刻的形态：递归地沿着子 tree 下去，就能还原出整棵文件树，每个叶子都是一个 blob。这正是"快照"的物化形态——commit 之所以能代表一个完整快照，就是因为它持有一棵 root tree 的 SHA-1。

我的补充：这里有一个微妙但重要的点。文件改名（`mv`）在 Git 里几乎零成本——如果内容没变，blob 还是原来那个，变的只是 tree 里那一行的文件名字符串。Git 用启发式（"相似度检测"）来识别"这是改名而非删除+新增"，但底层其实就是：同一个 blob 指针换了条 name 记录。这也解释了为什么 `git log --follow` 能追踪一个改名文件的历史。

### 5. Commit 对象：不可变快照的哈希封装

视频第 27:47 讲 Commit，把它定义为"把一棵 tree 的快照加上元数据封装起来"。一个 commit 对象的内容大致是：

```
tree <root tree 的 SHA-1>
parent <上一个 commit 的 SHA-1>   # 第一个 commit 没有这行；merge commit 有两行
author <名字> <邮箱> <时间戳>
committer <名字> <邮箱> <时间戳>

<提交信息>
```

然后 Git 对这段文本同样算 SHA-1，得到 commit 的"身份证"。注意几个关键事实：**commit 里直接写着它的 parent 指针**——这就是项目历史之所以是一条链的原因；**commit 的 SHA-1 把 parent、tree、作者、时间、message 全部纳入了哈希**。这意味着：哪怕你只改了一个字节（比如提交信息里多了个空格），这个 commit 的 SHA-1 就会变；而因为它后面的所有 commit 都用 parent 指针引用它，所以从这点往后的整条链的哈希全部跟着变。

这就是 Git 不可篡改性的来源，也是 **Merkle DAG（默克尔有向无环图）** 的精髓：每个节点的哈希"包含"了它所有后代的内容。你不可能悄悄改掉历史中间某个 commit 而不被察觉——哈希链会一路断裂报警。这点在供应链安全（验证代码未被篡改）里极其重要。

### 6. 引用（refs）：分支为什么这么轻

讲完三种不可变对象，视频的隐含逻辑指向 Git 设计里另一半：**可变的引用（references）**。这才是 `git branch` 快得离谱的原因。

一条 Git 分支（比如 `main`）到底是什么？它不是目录的副本，不是文件树的拷贝，而是 `.git/refs/heads/main` 这么一个**普通文本文件**，里面只装着一个 41 字节的字符串——某个 commit 的 SHA-1 加换行。就这样。创建一个分支 `git branch feature`，Git 做的全部工作就是新建一个 41 字节的文件，O(1) 操作，不复制任何数据。对比 SVN 这类系统建分支要复制整个目录树，这是数量级的差距。

而 `HEAD` 呢？它是 `.git/HEAD`，内容是 `ref: refs/heads/main`——一个"指向当前分支"的符号指针。`git checkout` 切换分支，本质上就是改 HEAD 指向哪个 ref 文件，再把对应 commit 的 tree 铺到工作区。一旦你把"分支 = 指针"刻进脑子，`detached HEAD`（HEAD 直接指向某个 commit 而非分支）、`fast-forward merge`（只是把分支指针往前挪）、`reset`（移动分支指针）这些概念就全通了。

### 7. 三区模型与 SHA-1→SHA-256 迁移

最后补两个视频外围但实战关键的点。**三区模型**：工作区（Working Directory）是你看到的文件；暂存区（Staging / Index）是 `.git/index` 这个二进制文件，它缓存了"下一次 commit 将形成的 tree"——`git add` 把工作区内容变成 blob 存进对象库并更新 index；`git commit` 则把 index 固化成一棵新 tree 并创建 commit。理解 index 是"下一个快照的草稿"，就能明白为什么 `git add -p` 可以只暂存文件的一部分。

**哈希算法迁移**：Git 长期使用 SHA-1（160 位）。但 2017 年 Google 的 SHAttered 攻击证明了 SHA-1 碰撞可行（约 2⁶³ 次操作），理论上可以让两份不同内容产生相同哈希。虽然对 Git 这种带结构校验的场景实际威胁有限，Git 仍从 2.29 版起逐步迁移到 SHA-256。这是一个值得关注的趋势：未来你 clone 仓库时，可能会遇到 `objectformat = sha256` 的仓库，哈希串会变成 64 位十六进制。

## 知识结构图

下图把 Git 的对象模型与三区流转合并成一张图，左半边是不可变的 Merkle DAG（对象库），右半边是可变的引用与三个工作区：

```mermaid
graph TD
    subgraph OBJ["不可变对象库 .git/objects（内容寻址）"]
        C2["Commit B<br/>tree=...<br/>parent=CommitA"]
        C1["Commit A<br/>tree=...<br/>parent=无"]
        T2["Tree（根目录 v2）"]
        T1["Tree（根目录 v1）"]
        B1["Blob<br/>main.py 内容"]
        B2["Blob<br/>README.md 内容"]
        C2 -->|指向根树| T2
        C1 -->|指向根树| T1
        C2 -.->|parent 链| C1
        T2 -->|name=main.py| B1
        T2 -->|name=README.md, 改过| B2
        T1 -->|name=main.py, 未改→复用| B1
        T1 -->|name=README.md 旧版| B2
    end
    subgraph REF["可变引用"]
        H["HEAD<br/>ref: refs/heads/main"]
        M["refs/heads/main<br/>= Commit B 的 SHA<br/>(41字节文件)"]
        F["refs/heads/feature<br/>= Commit A 的 SHA"]
        H --> M
        M -->|指向| C2
        F -->|指向| C1
    end
    subgraph ZONE["三个工作区"]
        WD["工作区<br/>Working Directory"]
        IDX["暂存区<br/>.git/index<br/>下一个快照草稿"]
        WD -->|git add<br/>生成blob| IDX
        IDX -->|git commit<br/>生成tree+commit| C2
    end
    classDef immu fill:#eef,stroke:#66f,stroke-width:1px;
    classDef mut fill:#efe,stroke:#6c6,stroke-width:1px;
    class C1,C2,T1,T2,B1,B2 immu;
    class H,M,F,WD,IDX mut;
```

## 实战启示

懂了底层模型，Git 就从一堆"玄学命令"变成一套可推理的工具。这里给你几条能立刻上手的认知升级：

**第一，遇到 git 问题先想"指针在哪"。** 90% 的"我代码不见了"恐慌，本质是分支指针或 HEAD 的指向问题。`git reflog` 记录了 HEAD 的每一次移动，几乎所有"误操作"（错误 reset、错误 rebase）都能在这里找回那个 commit 的 SHA——因为对象是不可变的，只要你还有 SHA，数据就在对象库里没丢。**Git 几乎不会真正删除对象**（除非超过 gc 宽限期，默认 90 天），这就是为什么有人说"在 Git 里，只要 commit 过的东西就还在"。

**第二，把 merge / rebase / cherry-pick 当图操作来看。** `rebase` 之所以"改写历史"，是因为它把一串 commit 重新基于新 parent 逐个重放，生成的是**全新的 commit 对象**（SHA 全变）——所以已 push 的公共分支别 rebase。`cherry-pick` 能单独搬一个 commit，是因为 commit 是自包含的快照变更，Git 拿它的 diff 应用到当前 HEAD 再新建一个 commit。这些过去要背的规则，现在都有了机制层面的解释。

**第三，用内容寻址思维理解 `.git` 体积。** 仓库为什么有时很大？通常是历史里混进了二进制大文件（每次改一点就生成一个新的大 blob，delta 压缩也救不回来）。`git filter-repo` 清理历史，本质就是把这些 blob 从对象图里摘掉并重写后续 commit。理解对象模型，你才知道该清理什么、清理后会动哪些哈希。

**第四，对 CI/CD 与供应链安全的意义。** 既然 commit SHA 是对全部历史的哈希封装，那么"锁定某个 commit SHA"就等于锁定了完整代码状态。这就是 reproducible build（可复现构建）和软件供应链验证的基础——你可以用哈希链证明从源码到产物没被篡改。

## 延伸学习路径

1. **《Pro Git》第 10 章 "Git Internals"**（官方免费书，git-scm.com）—— 本文对象模型最权威的来源，配有手把手的 `git cat-file -p` 演示，强烈建议照着敲一遍。
2. **Mary Rose Cook《Git from the inside out》**（reinteractive 博客）—— 用一个玩具版 Git 实现讲透对象图，是经典中的经典。
3. **GitHub 官方文档 "About Git" 与 "Git Internals" tutorial**—— 偏实战视角，讲 refs、remote、packfile。
4. **Linus Torvalds 2007 年 Google Tech Talk on Git**（YouTube）—— Git 创始人亲自讲设计哲学（"content tracker"的原始定义），了解 SHA-1 选择的时代背景。
5. **Git 源码 `Documentation/technical/hash-function-transition.txt`**—— SHA-1→SHA-256 迁移的官方设计文档，想深究哈希迁移细节必读。

## 一句话总结

> Git 的全部魔法只来自一个底层选择——**用内容的 SHA 当地址、存不可变快照、用可变指针表示分支**；一旦把 Blob/Tree/Commit 的 Merkle DAG 想清楚，所有 Git 命令就都成了可推导的逻辑而非待背诵的口诀。

## 参考来源

1. [Git Architecture | How Git works internally — Concept && Coding](https://www.youtube.com/watch?v=U8cSIqoc3DI) — 本文分析的核心视频（Blob/Tree/Commit 三章）
2. [Git Architecture Explained | How Git Works Internally for IT Professionals — Jafer Sabir](https://www.youtube.com/watch?v=z2AcRK0SYyM) — 公开同主题视频，可作为免费替代
3. [Pro Git Book — Git Internals (git-scm.com)](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain) — 对象模型与内容寻址的权威文档
4. [Git from the inside out — Mary Rose Cook](https://maryrosecook.com/blog/post/git-from-the-inside-out) — 用玩具实现讲透对象图
5. [SHAttered — The first collision for full SHA-1 (Google)](https://shattered.io/) — SHA-1 碰撞攻击，SHA-256 迁移的技术动因

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
