OBJECT · REF · TREE · DAG · 一座内容寻址的数据库

快照

G I T I N T E R N A L S

你每天都在用 Git。addcommitpush,闭着眼睛也不会错。但只要有人说一句「你 rebase 一下再推」,你心里就会咯噔一下——不是不会敲,是不知道敲下去之后会发生什么。

问题出在一个几乎所有人都有的误解上:你以为 Git 存的是「每次改了什么」。毕竟 git log -p 给你看的就是一页一页的 diff,红红绿绿,加加减减。

但那是算给你看的。Git 的对象库里一条 diff 都没有。它存的是快照——每一次提交,都完整地指向那一刻整棵目录树。而你以为很重的那些东西:分支、标签、HEAD,全都是一张贴纸,一个 41 字节的小文件,里面就写着一个哈希。

把这两句话吃透,Git 会当场从「一堆需要背的命令」变成「一个你能推理的系统」。resetrebasecherry-pick、那些吓人的冲突——全都变成了同一张图上的几种简单演算。

而且它是活的:这本书里跑着一台真的 Git 对象数据库——纯 JavaScript 写的 SHA-1 和 git 对象格式,就在这个网页里。你输一段内容,它算出来的哈希和你终端里 git hash-object 吐出来的一模一样,一个字符都不差。外加真 Myers 差分、真 diff3 三路合并、真 merge-base 算法。书里每一个哈希、每一处冲突、每一次 rebase,都是这些引擎当场算的。

24
6
24个真引擎 Demo
4台真引擎:SHA-1 对象库 · Myers · diff3 · DAG

这本书为什么不从「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 个十六进制字符加一个换行。建分支 = 写一个文件。切分支 = 改一下 HEADreset = 把贴纸撕下来贴到别处。

读懂这两句,上面那六个问题会同时变得显然。

三件这本书坚持做到的事

四种对象,四种颜色,一眼分清

全书守一条视觉规则: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 个对象——没改的那棵子树被原样复用。这两个提交号 06b431f1bada99,你可以在自己机器上原样复现。
  • 真 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 没有黑箱,只有你还没打开的目录。

每章末尾还有一节 ↩ 回到你的仓库,把这一章的原理接回一个你大概率真的遇到过的场面。

这本书写给

  • Git 用了很多年,日常命令很熟,但一遇到 rebasereset --hard/强推就手心出汗的人
  • 冲突了只会「留哪个删哪个」,说不清 Git 凭什么判定这里冲突的人
  • 被同事的「你 squash 一下」「你 cherry-pick 过来」问住过的人
  • 想读《Pro Git》第 10 章(Git Internals)但觉得干巴巴的人

这本书不打算

  • 教你 Git 的基本用法(addcommitpush 默认你会)
  • 逐条列 Git 命令的参数(那是 git help 的活儿)
  • 教你团队该用哪套分支模型(那是管理问题,不是原理问题)
  • 逐行讲 Git 的 C 源码(这里讲模型与算法,不讲 struct object 有几个字段)

读完之后你会有的东西

一个可以推理的模型,和一个新习惯:碰到任何 Git 命令,先问三个问题——它动的是贴纸,是对象,还是工作区?

这三个问题几乎能定掉一切。动贴纸的:随便来,41 个字节而已,reflog 记着它去过哪儿。造对象的:安全,对象不可变,只增不改。动工作区的:停三秒——那是整个系统里唯一一个 Git 没有替你备份的地方。

顺带你会拿到一句技术上精确的安慰:只要它被 commit 过,它就几乎不可能真的消失。默认还要在库里躺 90 天才有资格被回收。Git 那个吓人的名声,绝大部分是模型不清楚带来的错觉。

最后,你会开始看见那张图。它一直在那儿,你敲的每一条命令都是在上面挪一个点、贴一张纸。