<!-- title: Git不存差异存快照：分支因此近乎免费 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=Ala6PHlYjmw -->

# Git不存差异存快照：分支因此近乎免费

## 导语：你用错了十年，是因为从第一天起就理解反了

如果你是一个程序员，你每天大概要敲几十次 `git commit`、`git push`、`git pull`。但要是有人突然问你："Git 到底把你的代码以什么形式存进去了？"——大部分人会脱口而出："当然是存差异（diff）啊，每次提交只记录改了哪几行。"

**这个直觉，是错的。** 而且 GitHub 自己的工程师 Derrick Stolee 都承认，正是这个错误直觉，是"Git 让人困惑的根因"。

LearnThatStack 频道那部播放量破百万的《Git Will Finally Make Sense After This》，干的事情就是一句话捅破这层窗户纸：**Git 不存改动，它存整份快照；它甚至不是传统意义上的"版本控制工具"，而是一个披着 VCS 外衣的内容寻址键值存储（content-addressable key-value store）**。一旦你接受这个事实模型，那些让你头疼多年的 `rebase`、`cherry-pick`、`reset --hard`、`detached HEAD`，会像被打通任督二脉一样，瞬间全部说通。

这篇文章要做的不只是转述这个视频，而是把"快照模型"这条暗线一路追到底——从 SHA 哈希、默克尔有向无环图，到分支为什么只是一个 41 字节的文件，再到 packfile 里那个"概念是快照、物理却是增量"的精妙妥协。读完它，Git 在你眼里将不再是一堆需要死记的命令，而是一台设计极其优雅的机器。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Git Will Finally Make Sense After This |
| 频道 | LearnThatStack |
| 时长 | 约 13 分钟 |
| 播放量 | 100 万+（1M views） |
| 发布时间 | 2025 年 12 月 |
| 频道订阅 | 10.7 万（107K subscribers） |
| 核心主题 | Git 真实的数据模型：commit、branch、reset、rebase 背后的快照与指针 |

## 核心内容拆解

### 一、最大的误解：Git 不是"存差异"的

我们先把这个误解钉死。在 Git 之前，老一代版本控制系统——CVS、SVN，更早的 RCS——都是"增量存储"思维：第一次存完整文件，之后每次提交只记录"相对上一版的差异"，像一条不断累加的补丁链。这种思维非常符合直觉，于是几乎所有 Git 新手都默认 Git 也是这样工作的。

但 GitHub 官方博客那篇被引用无数次的《Commits are snapshots, not diffs》里，维护者 Derrick Stolee 写得非常直接：**用户在 `cherry-pick`、`rebase` 这些"重写历史"命令上反复翻车，根因就是把 commit 当成了"可以搬来搬去的差异块"**。

真相是：**每一次 commit，Git 都对整个项目当时的状态拍一张"全家福"——一个完整快照（snapshot）。** 你看到的 `git diff` 那些花花绿绿的增删行，根本不是被存下来的东西，而是 Git 在你需要的那一刻，把两个快照拿来现场比对、**临时算出来的**。差异是派生视图（derived view），不是存储内容。这个区分，是理解 Git 的第一块基石。

### 二、Git 的真身：一个内容寻址的键值存储

Git 官方文档《Pro Git》里有一句被低估的话："Git 本质上是一个内容寻址文件系统（content-addressable filesystem），上面套了一个 VCS 的用户界面。"

什么叫"内容寻址"？普通数据库是你给它一个"地址"（行号、文件路径、键名），它去那个位置取数据。而内容寻址反过来：**你把数据本身（加上一个类型头）做一次哈希，用算出来的哈希值当键，数据本身当值存起来。** 同样的内容，喂给同样的哈希函数，永远得到同样的键——这是确定性的、可复现的。

Git 用的是 SHA-1（目前正在向更安全的 SHA-256 迁移）。这意味着三件极其重要的事情自动成立：

1. **去重是免费的**：两个文件内容完全相同，哈希就相同，Git 只存一份。你复制 100 份一模一样的文件进仓库，`.git` 体积几乎不变。
2. **完整性是密码学保证的**：篡改任何一个字节，哈希就变，整条引用链就断。你不可能"偷偷改了历史"而不被发现。
3. **寻址不靠位置靠身份**：一个对象的名字（哈希）就是它的"指纹"，谁能算出同样的指纹，谁就拿到了同样的东西——这正是 Git 能彻底去中心化的根基。

顺带说一句，这套"把内容哈希当指针"的思想，后来被区块链原封不动抄了过去。比特币用默克尔根做交易校验，而 Linus 在 2005 年就用它造了 Git——**比中本聪早了整整四年**。所以别觉得 Git 老土，它的内核其实是密码学里最时髦的招。

### 三、四种对象：blob、tree、commit、tag

Git 的世界里只有四种"对象"，全部塞在那个内容寻址存储里。理解了它们，你就理解了 Git 的全部：

- **blob**：存文件**内容**，注意——只存内容，**不存文件名**。一份 README 的文字，是一个 blob。
- **tree**：存一个**目录**。它是一张映射表：把"文件名 + 权限"映射到对应的 blob（文件）或子 tree（子目录）。
- **commit**：存**元数据**——作者、提交者、时间戳、提交信息，外加两个指针：一个指向"根 tree"（这次提交时项目的完整样子），一个或多个指向"父 commit"。
- **tag**（带注解的 annotated tag）：一个有自己元数据的、指向某个 commit 的命名指针。

这里面藏着一个特别精妙的解耦：**文件名和文件内容被彻底分开了**。文件名住在 tree 里，内容住在 blob 里。于是当你 `git mv` 给一个文件改名时，那个 blob 的哈希纹丝不动——变动的只是 tree 里那一条映射记录。Git 因此能在 `git log --follow` 里追踪被改名的文件：内容指纹没变，它就知道"这其实是同一个文件"。

### 四、默克尔有向无环图：改一个字，哈希全链雪崩

把上面的对象拼起来，你会得到一棵树套树的结构：一个 commit 指向一个根 tree，根 tree 指向若干子 tree 和 blob，每个子 tree 又指向更深一层的子 tree 和 blob……每个父节点都把子节点的哈希"包"进自己里再算哈希。这就是**默克尔树（Merkle tree）**，而 commit 之间通过 parent 指针连成的，是一条**有向无环图（DAG）**。

这套结构的直接后果叫"哈希雪崩"：**你只改了某个文件里的一个字符，那个 blob 的哈希就变了；它的哈希一变，包含它的 tree 哈希就变了；tree 一变，上一层 tree 也变……一路传导到根 tree，最后这个 commit 的哈希也变了。** 任何一个下游节点动了，所有上游节点的哈希都必须重新计算。

这不是缺陷，这是特性。它让 Git 历史**天然防篡改**：你没法在不破坏整条哈希链的前提下悄悄改动任何一个历史提交。整个 Git 的完整性校验（`git fsck`）就是建立在这个机制上——扫一遍对象图，重算哈希，对不上就是坏了。

### 五、分支只是一个 41 字节的文件指针

这大概是 Git 里最反直觉、也最解放思维的一个事实。在很多人的想象里，"分支"是一个容器，里面装着属于这个分支的一堆 commit，创建分支是一项"大工程"。

**真相是：一个分支（Git 叫 ref）只是 `.git/refs/heads/` 目录下的一个小文件，里面写着它当前所指的那个 commit 的 40 位 SHA-1（加一个换行符，正好 41 字节）。** 创建一个分支，本质上就是往磁盘上写 41 个字节。这就是为什么 Git 的分支创建是 O(1) 的、几乎零成本——你大可以开几百个分支随便玩，而 SVN 时代分支昂贵得没人敢多用。

而 `HEAD` 呢？它本身又只是一个指针，指向"你当前在哪个分支"。所以真实的指针链是这样的：**HEAD → 当前分支（refs/heads/main）→ 某个 commit → 根 tree → …… → blobs**。一整条"当前状态"的引用，是从几个轻量指针一路牵出来的。

一旦你看清"commit 是不可变的快照节点，分支和 HEAD 只是几根会动的指针"，所有 Git 操作的本质就变成了同一道题：**移动指针，或生成新快照。**

### 六、为什么 rebase、reset 这么绕？因为它们在"重算差异"

既然 commit 是快照不是差异，那 `git diff`、`git show` 里的差异从哪来？答案是——**Git 每次都拿两个 commit 的根 tree 做现场比对，临时算出来**。它从不存储"提交之间的差异"。

这个事实，恰好解释了几个最让人抓狂的命令为什么是那个样子：

- **`git rebase` 和 `git cherry-pick`**：它们号称"重放（replay）提交"。但既然提交是快照，"重放"就只能这样实现——**先算出这个 commit 相对它自己父 commit 的差异，再把这个差异补丁打到新的基底上，生成一个全新的快照**。冲突为什么会产生？因为同一个补丁换了个上下文，可能就贴不上了。理解了"快照 + 现算差异"，你才明白冲突是计算的副产物，而不是 Git 在故意刁难你。
- **`git reset` 的三种模式**：`--soft` 只移动分支指针；`--mixed` 移动指针 + 重置暂存区（index）；`--hard` 移动指针 + 重置暂存区 + 重置工作目录。本质上就是"移动这根指针时，要不要顺便把另外两层缓存也对齐"。三层（commit、index、worktree）是 Git 的经典心智模型，全靠指针 + 快照这套底层逻辑支撑。
- **`git revert` vs `git reset`**：revert 是"新增一个反向快照"（前进式撤销，历史可追溯）；reset 是"把指针往回拨"（回退式撤销，会丢历史）。两者孰优孰劣，取决于你要不要保留那段历史。

### 七、概念是快照，物理是增量：packfile 的精妙妥协

如果你够敏锐，此刻应该有个疑问：每次 commit 都存一份完整快照？那一个几 GB 的大仓库，提交几万次，`.git` 岂不是要爆炸？

**并不会——而这就是 Git 设计里最容易被误读的一层妥协。**

答案是 **packfile**。Git 平时（松散对象，loose objects）确实每个对象单独存。但当你执行 `git gc`、`git repack`，或者在 push/pull 传输时，Git 会把大量对象"打包"。打包时，**它确实会做增量压缩（delta compression）：把某些对象表示成"相对某个基础对象的差异"**，从而大幅省空间。

这里有两点必须分清，否则你就会在"快照派"和"差异派"的争论里迷失：

1. **概念模型永远是快照。** 你的大脑、你的操作直觉，都应该基于"每次提交是一张完整快照"。这是 Git 暴露给你思考的真相。
2. **物理存储格式可以是增量。** packfile 里用 delta 编码，纯粹是底层为了省磁盘、省带宽的**透明优化**，对你不可见，也不该影响你的心智模型。

更聪明的是，Git 会**根据访问模式选择增量的方向**：通常把较新、较常访问的版本存成完整对象，把旧的、不常碰的版本存成"相对新版本的逆向 delta"。这样最近的历史（你最常查的）解压快，代价摊到冷数据上。这就是为什么 `git gc` 之后仓库往往小一大圈，却又几乎不拖慢日常操作。

一句话收尾这层：**Git 在语义层是"快照主义者"，在物理层是"实用主义的增量压缩者"。两层各司其职，别把它们混为一谈。**

### 八、为什么 Linus 要这样设计：速度、完整性、去中心化的三合一

回到 2005 年。Linux 内核团队原本用一款叫 BitKeeper 的商业版本控制工具，免费许可被收回后，Linus 一怒之下，花了大约十天，自己写了 Git。他要同时满足三个在当时看似矛盾的目标：

1. **极快的分支与合并**——内核每天有海量的合并操作；
2. **数据完整性**——绝不能出现"悄无声息被篡改的代码"；
3. **彻底去中心化**——每一个克隆都是完整备份，断网也能干活。

而"内容寻址 + 快照 + 指针"这套设计，竟然一次性把三者全解决了：内容寻址给出密码学完整性；快照让分支只是一根指针（O(1) 创建）；对象自包含让任何克隆都能从 `.git` 这个自洽数据库里重建一切。这是少有的"第一性原理设计"案例——Linus 没有去改良现有的 VCS，而是从"如何用哈希管理有版本的内容"这个最底层问题重新出发。

理解了这个动机，你也就理解了 `.git` 目录的本质：**它不是一个配置文件夹，而是一个完整的、自包含的、密码学保护的数据库。** 删掉远端，你的本地克隆依然握有全部历史——这就是去中心化的底气。

## 知识结构图

```mermaid
graph TD
    HEAD["HEAD<br/>(指针: 当前在哪个分支)"] --> BR["refs/heads/main<br/>(41字节文件: 指向某commit)"]
    BR --> C2["commit B<br/>SHA: c9d0…<br/>(快照 + 元数据)"]
    C2 -.->|"parent"| C1["commit A<br/>SHA: a1b2…"]
    C2 --> RT2["根 tree B<br/>(目录映射表)"]
    C1 --> RT1["根 tree A"]

    RT2 --> TS["tree: src/<br/>哈希未变 → 复用"]
    RT2 --> B3["blob: README v2<br/>(新内容→新哈希)"]
    RT2 --> BL["blob: logo.png<br/>(未变→复用)"]

    RT1 --> TS
    RT1 --> B2["blob: README v1"]
    RT1 --> BL

    TS --> MAIN["blob: main.py"]

    style C2 fill:#b3e5fc
    style B3 fill:#fff3e0
    style HEAD fill:#c8e6c9
    style BR fill:#c8e6c9
```

**读图要点**：实线箭头是 commit/tree 的"内容指针"（哈希引用）；虚线是 commit 之间的 parent 链（构成 DAG）。绿色的是会动的轻量指针（HEAD、分支），蓝色 commit 是不可变快照，橙色 blob 是这次提交里**唯一真正变化**的对象。注意 `tree: src/`、`blob: logo.png` 被 commit A 和 B **共享**——这就是内容寻址带来的免费去重：没变的东西，哈希相同，只存一份。

## 实战启示：这套模型能帮你做什么

**第一，别再害怕分支。** 既然分支只是 41 字节的指针，那就尽情开。功能分支、实验分支、临时分支，开完删掉，对仓库几乎零负担。Git 工作流（Git Flow、Trunk-based、GitHub Flow）的差异，本质上就是"什么时候、往哪根指针上挪 commit"的策略差异，底层完全一样。

**第二，`.git` 目录就是你的保险箱。** 把一个项目的整个 `.git` 拷贝到 U 盘里，你就拥有了它的**全部历史、全部分支、全部标签**——一个自包含的完整备份。这比任何"备份方案"都更彻底，因为它是密码学可验证的。养成定期对重要仓库的 `.git` 做 tar 打包冷备的习惯。

**第三，用对象思维 debug，而不是背命令。** 下次遇到 `detached HEAD`，别慌——它只是说 HEAD 此刻直接指着一个 commit 而不是某个分支名；想保留当前状态，就创建一根新指针指过去（`git branch <name>` / `git switch -c`）即可。遇到 `rebase` 冲突，记住 Git 在"把补丁打到新基底"，冲突是因为上下文对不上，手动解决后 `--continue`。一旦你用"指针 + 快照"看问题，就没有学不会的 Git 命令。

**第四，理解 packfile，理解 `git gc`。** 大仓库变慢、`.git` 膨胀，往往是因为松散对象太多或 packfile 老化。定期 `git gc`（或 `git repack -ad`）能重算增量、压缩体积、加速日常操作。对 CI/镜像构建场景，浅克隆（`--depth`）和部分克隆（`--filter=blob:none`）也是基于这套对象模型做的取舍——少拉一些 blob，换取速度。

**第五，内容寻址是一种可迁移的设计哲学。** Git、Docker 镜像层（每层按内容哈希）、Nix/Guix 包管理器、IPFS、区块链——它们共享同一个内核：**用内容的哈希当它的唯一身份。** 看懂 Git，你就半只脚踏进了现代分布式存储和可复现构建（reproducible builds）的世界。

## 延伸学习路径

1. **《Pro Git》第 10 章 "Git Internals"**（git-scm.com 官方，免费）——把对象模型、packfile、refspec 讲到骨子里，是这篇解读的事实底座。
2. **GitHub Engineering 博客：《Commits are snapshots, not diffs》**（Derrick Stolee）——由 Git 核心维护者亲自澄清那个最大的误解，权威性无可辩驳。
3. **动手实验：`git cat-file -p <hash>` 与 `git hash-object`**——亲手用底层命令把 blob、tree、commit 哈希出来、读回去，比读十篇文章都管用。试着改一个字符，观察哈希雪崩。
4. **Julia Evans 的 Git 漫画《Oh shit, git!》及 zine**——以极其直观的图示讲清 reset/rebase/reflog，配上本文的快照模型食用效果最佳。
5. **Linus Torvalds 2007 年 Google Talk：《Tech Talk: Linus Torvalds on git》**——听设计者本人解释为什么 Git 必须是分布式的、为什么他鄙视 SVN 的分支模型，能让你从源头理解这套设计的动机。

## 一句话总结

**Git 不是存差异的版本工具，而是一台用"内容哈希 + 完整快照 + 轻量指针"搭起来的、密码学自洽的去中心化数据库——一旦你从快照而非差异去看它，所有的困惑都会自动归位。**

## 参考来源

1. [Git Will Finally Make Sense After This — LearnThatStack](https://www.youtube.com/watch?v=Ala6PHlYjmw) — 本文分析的核心视频（1M+ views）
2. [Commits are snapshots, not diffs — The GitHub Blog (Derrick Stolee)](https://github.blog/open-source/git/commits-are-snapshots-not-diffs/) — 官方维护者对"快照 vs 差异"误解的权威澄清
3. [Git Internals: Git Objects — Pro Git (git-scm.com)](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) — 官方文档对 blob/tree/commit 对象模型与内容寻址的原始定义
4. [Git Commit: Snapshot vs Delta Storage Mechanics Explained — sqlpey.com](https://sqlpey.com/git/git-commit-snapshot-vs-delta-storage/) — 澄清"概念快照、物理 packfile 增量"两层模型的技术细节
5. [Git: The Elegant Machinery of Content-Addressable Storage — decoupled.dev](https://decoupled.dev/case-studies/git-content-addressable) — 从 Linus 第一性原理设计角度解读默克尔树与 DAG
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
### 完成摘要

**标题核验**：「Git不存差异存快照：分支因此近乎免费」= 19 字（≤20），domain=programming，video 链接完整，三行元信息格式合规。

**注意事项**：视频时长 13:25 略低于 15 分钟建议线，但百万播放量与频道质量已充分保证内容深度与热度；核心论点全部基于 GitHub 官方维护者文章与 Git 官方文档，技术准确性有据可查。

> [!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)
