<!-- title: 没存一条差异：Git靠快照骗过所有人 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=_pjnLSUFEqQ -->

# 没存一条差异：Git靠快照骗过所有人

## 导语：你和90%的开发者一样，对Git的理解是错的

如果你是一个写过代码的人，十有八九你的脑子里装着这样一个 Git 的画面：每次 `git commit`，Git 就把"这次改了哪几行"记成一个小补丁（patch），像一摞便利贴一样叠在上一张上面。需要看历史？把这一摞补丁从头翻到尾，一点点还原。需要看差异？翻出对应的两张补丁对比。听起来天衣无缝，对吧？

但这是错的，而且错得相当彻底。

Git **根本不存储差异**。每一次 commit，Git 都在硬盘上存下整个项目的**完整快照**（snapshot）——你仓库里的一万个文件，哪怕你只改了其中一个标点符号，Git 也会把这"一万个文件此刻的状态"整个拍一张照存下来。你在 `git diff`、`git show`、`git log -p` 里看到的那些 `+` 和 `-`，**全是临时算出来的**，从来没有任何一个差异被写进过 Git 的数据库。

这个视频（以及 GitHub 官方博客、Pro Git 一书、jvns.ca 等一众权威来源）要纠正的就是这个普遍到几乎成为"行业共识"的误解。它不是吹毛求疵的概念洁癖——一旦你把心智模型从"差异链"换成"快照流"，Git 里那一堆让你头大的命令：`rebase` 为什么会"改写历史"、`cherry-pick` 为什么会产生新的 commit hash、分支为什么"几乎不要钱"、`git reset` 删掉的东西为什么还能救回来——全部会瞬间变得顺理成章。这篇文章带你把这件事彻底想通。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Git Stores Snapshots, Not File Differences |
| 视频链接 | youtube.com/watch?v=_pjnLSUFEqQ |
| 频道定位 | 版本控制 / 开发者工具科普类 |
| 时长 | 约15–20分钟（概念系统讲解） |
| 受众 | 用了多年 Git 但仍被 rebase/merge 折腾的开发者 |
| 核心主题 | 拆穿"Git 存差异"的集体误解，建立"快照 + 内容寻址"的心智模型 |
| 一句话概括 | 你以为 commit 是补丁，其实它是整盘快照 |

> 说明：本文的核心论点同时由 GitHub 官方博客《Commits are snapshots, not diffs》（Derrick Stolee, 2020）、Pro Git 官方手册、Julia Evans（jvns.ca）的技术拆解，以及 2026 年 6 月新发表的《Git Isn't About Diffs》六误区长文交叉印证。视频是切入点，深度补强来自这些一手资料。

## 核心内容拆解

### 要点一：你脑子里的那个 commit，是一张补丁——但它不是

这是整个误会的源头，也是视频最核心的一句话。绝大多数开发者下意识地把"提交"理解成"这次新增了哪些行、删除了哪些行"的一份小记录，像一块砖头叠在之前的砖头上。这种直觉来自几十年前 CVS、SVN、RCS 那一代版本控制系统——它们确实是这么干的：把每个文件存成一条**反向增量链**（reverse delta），最新版本是完整的，旧版本靠"减去差异"一点点倒推回去。

Git 反着来。Pro Git 手册里有一句被反复引用的原话（视频和所有权威文章都引它）：

> "Git thinks of its data more like **a series of snapshots of a miniature filesystem**."（Git 把自己的数据看作一个微型文件系统的**一系列快照**。）

也就是说，每一次 `git commit`，Git 都会对"此刻整个项目长什么样"拍一张全息照片存下来。你改了一个文件，Git 就为这个文件存一个新的版本对象；你没碰的那 9999 个文件，Git 不重复存储，而是"指向"它们已有的版本。**但模型是快照，不是差异**。这是视频原话，也是一切的起点。

### 要点二：你看到的每一处 diff，都是临场算出来的（on the fly）

这是理解 Git 行为的关键推论。既然底层存的是快照，那 `git diff`、`git show`、`git log -p` 里那些花花绿绿的增删行是从哪来的？答案是：**Git 在显示的瞬间，把两个快照拿来逐字节对比，临时算出来的**。差异从来不是"被存储的东西"，而是"被计算的结果"。

这条推论能解释一个让初学者困惑的现象：你为什么能对**任意两个**提交求 diff，哪怕它们隔着 500 次提交、八竿子打不着？因为 Git 不需要顺着一条补丁链一路翻回起点再相减——它只要把那两个时刻的完整快照拿来比一下就行了。O(1) 级别的两状态对比，而不是 O(n) 级别的链式回溯。这就是快照模型的性能红利。

### 要点三：三个对象撑起整个 Git：blob、tree、commit

要真正相信"Git 是快照机"，得钻进它的内部结构。Git 本质上是一个**内容寻址的对象数据库**（content-addressed object store）——一个把"内容本身"经过哈希运算得到地址（key）的键值仓库。里面主要住着三类对象：

- **blob（数据块）**：只存文件的内容（那一坨字节），**不存文件名、不存权限、不存时间**。同名文件内容变了就是两个 blob；不同名的文件内容相同就是同一个 blob。
- **tree（树）**：对应一个目录。它是一张清单，列出"这个目录下有哪些条目、各自的名字、权限，以及指向哪个 blob 或子 tree"。
- **commit（提交）**：指向一个顶层 tree（这次提交时整个项目的根目录快照）+ 一个或多个父 commit + 作者/时间/提交信息。

每个对象的"地址"就是它自身内容的 SHA-1（新版本支持 SHA-256）哈希值。**内容相同 → 哈希相同 → 只存一份**。这是 Git 省空间的真正诀窍，下面细说。

### 要点四：省空间不靠存差异，靠"内容寻址去重"

很多人会立刻反驳：一万个文件每次提交都存一份全量，那岂不是天文数字的磁盘占用？Git 怎么可能还这么小？

答案是**内容寻址带来的天然去重**。设想你有一万文件的仓库，改了 `main.py` 一个文件就提交：

- `main.py` 内容变了 → 它算出一个**新**的 blob，存一份。
- 其余 9999 个文件内容没动 → 它们的 blob 哈希和上次一模一样 → Git **直接复用已存在的对象，一个字节都不重复存**。
- 新的 tree 对象记录下"main.py 现在指向新 blob，其余指向旧 blob"，新 commit 指向这个新 tree。

所以这一次提交，Git 实际新增的实体只有：1 个新 blob + 几个新 tree + 1 个新 commit，而**不是一万个文件**。未变化的文件靠哈希共享同一个对象。再叠加 zlib 压缩，仓库体积就被压得很小。**去重的钥匙是"内容即地址"，不是"差异存储"**——这是视频里没有展开、但理解了就豁然开朗的延伸点。

### 要点五：一个容易被忽视的反转——packfile 里其实藏着 delta 压缩

这里要补充一个连很多老手都说不清的细节，也是"差异"误解如此顽固的另一半原因。在**逻辑模型**上，Git 确实是快照。但在**物理存储**层，Git 会周期性地把零散对象打包进 packfile，而 packfile 内部**确实使用了 delta 压缩**——它会把某些对象编码成"相对于某个 base 对象的差异"来节省空间。

所以严格来说：Git 在对象数据库层面是快照，在打包压缩层面又用回了 delta。这正是误会顽固的根源——"存差异"并非空穴来风，只是**差异出现在压缩优化层，而不是出现在你以为的逻辑模型层**。你脑子里应该装的那个模型，仍然是"一串快照"。把它想反了，`rebase`、`cherry-pick` 这些操作就永远像玄学。

### 要点六：换了心智模型，rebase 和 cherry-pick 立刻不吓人了

这是视频和 devdigest 文章共同强调的"实战收益"。人们怕 `git rebase` 和 `git cherry-pick`，根子就在于把 commit 当成"可以搬来搬去的补丁块"。一旦你接受 commit = 一张完整快照，这些命令的逻辑就清晰了：

- **`cherry-pick`**：Git 先**当场算出**目标 commit 隐含的差异（拿它和它自己的 parent 比一下），把这个差异应用到你的当前状态上，然后**记录一张全新的快照**，于是诞生了一个**全新的 commit**——这就是为什么 cherry-pick 出来的提交 hash 和原来不一样。
- **`rebase`**：把你的若干 commit 逐个"重放"到新基点之上——每次都是"算差异 → 应用 → 记新快照 → 得到新 hash"。所谓"改写历史"，改的其实是这一串新快照。

这里有一个漂亮的推论（我的延伸分析）：为什么"同样的改动，commit hash 却不同"？因为 commit 对象里塞进了 parent 指针和 tree 指针，parent 一变，commit 的内容就变了，哈希自然就变了。**哈希是整段关系的指纹，不是改动的指纹**。理解这一点，你就再也不会问"我明明 cherry-pick 了，为什么算成新提交"。

### 要点七：分支只有 41 字节，HEAD 是个游标

继续顺着快照模型往下走，两个常被神化的概念会瞬间祛魅：

- **分支（branch）**：在老一代 VCS 里，开分支是件大事——又慢又占空间，所以老程序员"舍不得开分支"，恨不得一个分支干到底。Git 的分支是什么？**一个文件，里面一行 40 字符的 commit hash**，总共 41 字节。创建分支 = 写 41 字节。所以 Git 圈才流行"为每一件小事都开分支"——它几乎是免费的，删掉也只是删个指针，底层快照会等垃圾回收时再清。
- **HEAD**：很多人以为 HEAD 是"最新的那个 commit"。错。HEAD 是"**你现在站在哪儿**"——一个游标，几乎总是指向你当前所在的分支（分支再指向某个 commit）。`HEAD~1` 是"从我现在的地方往回退一格"，`git reset --hard HEAD~3` 是"把当前分支指针往后挪三格"。"detached HEAD（游离头指针）"也不再是吓人的报错——它只是说 HEAD 直接指到了某个 commit 上、没挂在任何分支名下而已。

### 要点八：reflog——你以为删掉的 commit，其实还在

误解清单里最让人胆战心惊的一条是："一次 `git reset --hard`、一次误删分支、一次搞砸的 rebase，我的代码就永远没了。"这也是为什么那么多人对 Git 战战兢兢、不敢碰历史。

真相是：**几乎什么都还没真正丢掉**。Git 维护着一份叫 **reflog（引用日志）** 的日记，记录 HEAD 的每一次移动——每一次 commit、checkout、reset、rebase 都会留下一条。即使某个 commit 已经没有任何分支引用它，它仍然作为对象躺在数据库里：可达的提交保留约 90 天，不可达的保留约 30 天，之后才被垃圾回收真正清除。

这恰恰是快照模型的安全性所在——删除分支只是摘掉了一个 41 字节的指针，**快照本身原地不动**，直到 GC 才清理。所以 `git reflog` + `git reset --hard <那个旧 hash>`，就能把"误删"的工作救回来。

### 要点九（延伸）：Linus 当年为什么选了"反直觉"的快照方案

最后补一段视频未展开的设计哲学。Git 是 Linus Torvalds 在 2005 年为管理 Linux 内核源码而造的——内核有数万个文件、数千名贡献者、极其疯狂的速度和完整性要求。Linus 选择"快照 + 内容寻址"而非传统的"差异链"，是刻意为之的权衡：

1. **完整性（integrity）**：每个对象的地址就是它内容的哈希。篡改任何一处，哈希就变，引用链立刻断裂、立刻被发现。Git 天然防篡改，这在分布式协作里是命根子。
2. **速度**：checkout、diff、log 全是本地磁盘读取，不需要联网、不需要重建 delta 链。
3. **分布式优先**：每个 clone 都是完整仓库，断网也能工作，因为没有"中心服务器存着唯一真相"。

存储效率被 Linus 排在了后面，用"哈希去重 + packfile 压缩"补足。也就是说，**Git 是用"看起来费空间"的快照，换来了完整性和速度，再回头用工程手段把空间省回来**。这是一个值得反复品味的设计抉择。

## 知识结构图：Git 的"快照 + 内容寻址"模型

```mermaid
graph TD
    subgraph 提交层 commit
      C1["commit 7b2e…<br/>第一次提交"]
      C2["commit a3f9…<br/>第二次提交"]
      C2 -->|parent| C1
    end
    subgraph 快照层 tree
      T1["tree 9d4a<br/>项目快照①"]
      T2["tree f1c0<br/>项目快照②"]
      C1 -->|指向顶层tree| T1
      C2 -->|指向顶层tree| T2
    end
    subgraph 数据层 blob 内容寻址
      BO["blob r5t7<br/>main.py 旧版本"]
      BN["blob x1z2<br/>main.py 新版本"]
      BU["blob p9q8<br/>utils.py"]
      BR["blob m3n6<br/>README.md"]
    end
    T1 --> BO
    T1 --> BU
    T1 --> BR
    T2 --> BN
    T2 --> BU
    T2 --> BR
    BU -.->|"内容没变→同一对象<br/>(哈希去重)"| BU
    BR -.->|"内容没变→同一对象<br/>(哈希去重)"| BR
    style BU fill:#cfe8d6,stroke:#2e7d32
    style BR fill:#cfe8d6,stroke:#2e7d32
    style BN fill:#fde2c8,stroke:#e65100
```

看图说话：两次提交之间，`utils.py` 和 `README.md` 没有改动，所以 tree① 和 tree② **指向同一个** blob（绿色，去重）；只有改了的 `main.py` 生成了一个新 blob（橙色）。**没有一处存的是"差异"，存的是"指针 + 快照"。** 你在 `git log -p` 看到的差异，是 Git 临场把两个快照比出来的。

## 实战启示：换掉心智模型，你能立刻拿回三样东西

**第一，敢于频繁开分支、频繁提交。** 既然分支只是 41 字节、commit 只是记快照，那"开分支很贵""提交太多很乱"的旧恐惧就该丢掉。正确习惯是反过来的：每个小想法一个分支，每写完一小段就 commit。错了大不了删指针，快照还在 reflog 里候着。

**第二，不再害怕 rebase。** 记住 rebase 唯一的真正风险：**重写那些别人已经拉取（pull）过的共享历史**，会逼别人做痛苦的对账。你自己机器上没推送过的本地分支，rebase、squash、改顺序、改信息随便玩——你改的是只存在于你本机的历史快照，没人受影响，搞砸了还有 reflog 兜底。"别 rebase 共享分支"是好建议，"别 rebase"是坏建议，后者只是让你白怕很多年。

**第三，把 `git pull` 拆成两步。** `git pull` 是"穿着一件外套的两条命令"——`git fetch` + `git merge`（或 rebase）。那步隐藏的 merge 才是惊喜来源：可能造出你没要的 merge commit，或者撞上未提交改动卡在半路。改成先 `git fetch`（只下载、绝不碰你的工作区，永远安全）→ 看一眼进来了什么（`git log HEAD..@{u}`）→ 再**主动决定** merge 还是 rebase。把决定权从命令的"默认行为"手里拿回到你自己手里。

**第四，调试 Git 问题时，永远可以打开 `.git` 目录用 plumbing 命令验证。** `git cat-file -p <hash>` 能打印任意对象的原始内容，`git ls-tree <tree>` 能列出某个快照的文件清单。一旦你亲手"看见"一个 commit 长什么样（指向 tree + parent + message），快照模型就从抽象变成肌肉记忆。

## 延伸学习路径

1. **《Pro Git》官方免费书 · "What is Git?" 与 "Git Internals" 两章** —— 快照模型与对象数据库的权威出处，可在线全文阅读，配合 `git cat-file` 边读边验。
2. **GitHub 官方博客《Commits are snapshots, not diffs》（Derrick Stolee, 2020）** —— 把"为什么 rebase/cherry-pick 难懂"归结为心智模型错位，本文核心论点的一手来源。
3. **Julia Evans（jvns.ca）《Do we think of git commits as diffs, snapshots, or histories?》** —— 从"人到底怎么理解 commit"切入，配她标志性的手绘 zine，极其适合建立直觉。
4. **devdigest《Git Isn't About Diffs: Fix Your Mental Model in 6 Steps》（2026-06）** —— 本文重点引用的六误区清单，覆盖 commit / rebase / pull / branch / HEAD / reflog，最贴近当下。
5. **动手实验**：建一个空仓库，连做两次只改一个文件的提交，用 `git cat-file -p` 和 `git ls-tree` 打印每次 commit 的 tree 与 blob 哈希——你会亲眼看到没变的文件共享同一个 blob 对象。

## 一句话总结

Git 从没存过一条差异——它存的是一串完整快照，靠内容寻址去重和 packfile 压缩把空间省回来；一旦你把"差异链"换成"快照流"，rebase、cherry-pick、分支、HEAD、reflog 就全部不再神秘。

## 参考来源

1. [Git Stores Snapshots, Not File Differences（YouTube）](https://www.youtube.com/watch?v=_pjnLSUFEqQ) — 本文分析的核心视频
2. [Commits are snapshots, not diffs — The GitHub Blog（Derrick Stolee, 2020）](https://github.blog/open-source/git/commits-are-snapshots-not-diffs/) — 快照模型的官方权威论证
3. [Git Isn't About Diffs: Fix Your Mental Model in 6 Steps — Devdigest（2026-06-29）](https://www.devdigest.org/articles/git-isnt-about-diffs-fix-your-mental-model-in-6-steps) — 六大 Git 误区清单
4. [Do we think of git commits as diffs, snapshots, or histories? — Julia Evans / jvns.ca](https://jvns.ca/blog/2024/01/05/do-we-think-of-git-commits-as-diffs--snapshots--or-histories/) — 心智模型视角的技术拆解
5. [Pro Git Book · "What is Git?" 与 "Git Internals"](https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F) — "series of snapshots of a miniature filesystem" 原话出处

---
> 本文由「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)
