首页 › programming › 分支只是个指针:Git凭什么存快照不存差异

分支只是个指针: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(对象库),右半边是可变的引用与三个工作区:

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 — 本文分析的核心视频(Blob/Tree/Commit 三章)
  2. Git Architecture Explained | How Git Works Internally for IT Professionals — Jafer Sabir — 公开同主题视频,可作为免费替代
  3. Pro Git Book — Git Internals (git-scm.com) — 对象模型与内容寻址的权威文档
  4. Git from the inside out — Mary Rose Cook — 用玩具实现讲透对象图
  5. SHAttered — The first collision for full SHA-1 (Google) — 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学通信知识星球海报-扫码加入