快照
你每天都在用 Git。add、commit、push,闭着眼睛也不会错。但只要有人说一句「你 rebase 一下再推」,你心里就会咯噔一下——不是不会敲,是不知道敲下去之后会发生什么。
问题出在一个几乎所有人都有的误解上:你以为 Git 存的是「每次改了什么」。毕竟 git log -p 给你看的就是一页一页的 diff,红红绿绿,加加减减。
但那是算给你看的。Git 的对象库里一条 diff 都没有。它存的是快照——每一次提交,都完整地指向那一刻整棵目录树。而你以为很重的那些东西:分支、标签、HEAD,全都是一张贴纸,一个 41 字节的小文件,里面就写着一个哈希。
把这两句话吃透,Git 会当场从「一堆需要背的命令」变成「一个你能推理的系统」。reset、rebase、cherry-pick、那些吓人的冲突——全都变成了同一张图上的几种简单演算。
而且它是活的:这本书里跑着一台真的 Git 对象数据库——纯 JavaScript 写的 SHA-1 和 git 对象格式,就在这个网页里。你输一段内容,它算出来的哈希和你终端里 git hash-object 吐出来的一模一样,一个字符都不差。外加真 Myers 差分、真 diff3 三路合并、真 merge-base 算法。书里每一个哈希、每一处冲突、每一次 rebase,都是这些引擎当场算的。
这本书为什么不从「git 三板斧」讲起
因为那些你早就会了。真正卡住你的不是命令,是底下那个模型。而模型不清楚的时候,命令就只能靠背——背下来的东西,一遇到没背过的情况就崩。
验一验:下面这些,你能说出为什么吗?
- 为什么
git checkout一个分支是瞬间的,哪怕两个分支差了几千个文件? - 为什么
rebase之后,所有提交号全都变了? - 为什么删掉一个 200 MB 的文件并提交,
.git一点没变小? - 为什么
reset --hard之后提交能救回来,而没 add 过的改动救不回来? - 为什么两个人改了不同的文件,也可能冲突?
- 为什么
git status有两列字母,它们到底在比什么?
这六个问题看起来分属六个话题。但它们其实是同一件事的六个侧面——而那件事,两句话就能说完。
第一句:Git 不存 diff,存快照。
每一次提交,都指向那一刻整棵目录树的完整样子。不是「相对上次改了什么」,是「这一刻长什么样」。你在 git log -p 里看到的 diff,是 Git 拿两个快照当场算给你看的(第 14 章有一台真的 Myers 算法引擎)。
那不会爆炸吗?不会——因为对象的名字就是它内容的哈希。文件没改,内容一样,哈希一样,它天然就是同一个对象,根本不会被存第二次。Git 不需要「检查重复」,重复在它的地址系统里无法表达。
第二句:分支只是一张贴纸。
一个分支就是 .git/refs/heads/ 下的一个文件,41 个字节——40 个十六进制字符加一个换行。建分支 = 写一个文件。切分支 = 改一下 HEAD。reset = 把贴纸撕下来贴到别处。
读懂这两句,上面那六个问题会同时变得显然。
三件这本书坚持做到的事
四种对象,四种颜色,一眼分清
全书守一条视觉规则:BLOB 数据是青的,TREE 目录是橄榄的,COMMIT 提交是琥珀的,TAG 标注是紫的。而贴纸不是对象,所以它是另一个色系——main HEAD 玫红,而且永远歪着贴,带一点阴影。
这不只是装饰。正文代码块里的哈希会自动按类型染色:
tree 3ec3615098e7d29007e5639f4c25747dbabe2da0 parent 06b431f5f556d4b4d9b2f2fa4240e4d12fc6c975 author Sakura <sakura@example.com> 1700000000 +0800
颜色分开之后,一件事会变得很明显:对象是内容决定的、不可变的;贴纸是随手撕下贴上的。Git 里几乎所有的「危险」和「安全」,分界线就在这儿。
每台引擎都是真的
这本书里的关键机制,不是画示意图给你看,是用真代码写出来在浏览器里跑的:
- 真 SHA-1 对象数据库(第 2–5 章):纯 JS 实现的 SHA-1,加上完整的 git 对象格式——包括 tree 那个古怪的二进制布局,和「目录排序时末尾要当作带斜杠」的规则。输
hello world\n,它算出3b18e51,和你终端里一模一样。 - 迷你 Git(第 5 章):真的 add、真的 commit、真的建对象库。跑完两次提交,你会亲眼看到第二次只新增了 3 个对象——没改的那棵子树被原样复用。这两个提交号
06b431f和1bada99,你可以在自己机器上原样复现。 - 真 Myers 差分(第 14 章):1986 年那篇论文里的 O(ND) 贪心算法 + 回溯。它会告诉你这一对文本的编辑距离 D 到底是几——而 D 正是「Git 敢于不存 diff」的底气所在。
- 真 diff3 三路合并(第 17 章):用 LCS 找同步点,三方对比,真的产出你在冲突文件里见过的那些
<<<<<<<标记。三个输入框都能改——冲突不是 Git 的脾气,是一个可以推导的结论。 - 真 DAG 演算(第 15–19 章):拓扑序、祖先集合、merge-base(含「两个 base」的交叉合并病例)、rebase 与 cherry-pick 的重放,全是图算法算出来的,不是画上去的。
每一章都掀开 .git 看一眼
每章都有若干个 ⌗ 掀开 .git 看一眼 的段落,给你真实的文件内容、真实的字节、真实的命令输出。讲分支就 cat 给你看那 41 个字节;讲 index 就把 xxd 的头几行摆出来。Git 没有黑箱,只有你还没打开的目录。
每章末尾还有一节 ↩ 回到你的仓库,把这一章的原理接回一个你大概率真的遇到过的场面。
《独占》讲操作系统怎么维持「每个进程都独占整台机器」的骗局。这本书第 11 章讲 .git/index 为什么慢,答案是几万次 stat 系统调用——正是那本书第 3 章的价目表在起作用。
《密语》讲密码学。这本书从头到尾靠 SHA-1 给对象命名,但用的不是它的抗碰撞性,而是它的确定性——第 2 章会说清楚这个区别,以及为什么 SHA-1 在密码学上早就不安全了,Git 却还能用得好好的。
《当真》讲 PostgreSQL 的 MVCC:旧版本不删除、新版本另存一份、靠可见性规则决定你看到哪个。那和 Git 的对象库是同一种思路的两个实现——只增不改,读的时候再拼装。
这本书写给
- Git 用了很多年,日常命令很熟,但一遇到
rebase/reset --hard/强推就手心出汗的人 - 冲突了只会「留哪个删哪个」,说不清 Git 凭什么判定这里冲突的人
- 被同事的「你 squash 一下」「你 cherry-pick 过来」问住过的人
- 想读《Pro Git》第 10 章(Git Internals)但觉得干巴巴的人
这本书不打算
- 教你 Git 的基本用法(
add/commit/push默认你会) - 逐条列 Git 命令的参数(那是
git help的活儿) - 教你团队该用哪套分支模型(那是管理问题,不是原理问题)
- 逐行讲 Git 的 C 源码(这里讲模型与算法,不讲
struct object有几个字段)
读完之后你会有的东西
一个可以推理的模型,和一个新习惯:碰到任何 Git 命令,先问三个问题——它动的是贴纸,是对象,还是工作区?
这三个问题几乎能定掉一切。动贴纸的:随便来,41 个字节而已,reflog 记着它去过哪儿。造对象的:安全,对象不可变,只增不改。动工作区的:停三秒——那是整个系统里唯一一个 Git 没有替你备份的地方。
顺带你会拿到一句技术上精确的安慰:只要它被 commit 过,它就几乎不可能真的消失。默认还要在库里躺 90 天才有资格被回收。Git 那个吓人的名声,绝大部分是模型不清楚带来的错觉。
最后,你会开始看见那张图。它一直在那儿,你敲的每一条命令都是在上面挪一个点、贴一张纸。