跳转至

Git 对象模型

如果你只能记住 Git 的一个核心事实,那应该是这一句:

Git 本质上是一个基于内容寻址的对象数据库。

这句话比“Git 是版本控制工具”更接近底层真实。

什么叫内容寻址

“内容寻址”指的是:对象的身份来自它的内容,而不是来自文件名、路径名或者用户手工编号。

在 Git 里,一个对象在写入对象库之前,会先被拼成这样的字节串:

[type] [size]\x00[content]

然后对这整段字节计算 SHA-1。这个 SHA-1 结果就是对象 ID。

所以:

  • 如果内容不变,对象 ID 不变;
  • 如果内容变化,对象 ID 一定变化;
  • 两个不同路径但内容完全相同的文件,可以共享同一个 blob 对象。

这就是 Git 去重和快照高效性的起点。

Git 的四类核心对象

Git 里最重要的对象有四类:

  • blob
  • tree
  • commit
  • tag

blob

blob 只存文件内容本身,不存文件名,不存目录层级。

这意味着:

  • 文件名不是 blob 身份的一部分;
  • 两个不同名字的文件,只要内容一样,就能对应同一个 blob;
  • Git 在对象层最先关心的是“内容”,不是“文件路径”。

tree

tree 用来描述目录快照。它会记录:

  • 文件模式;
  • 文件名;
  • 指向的对象 ID。

对文件来说,tree 指向 blob。
对子目录来说,tree 指向另一个 tree。

所以一个 tree 实际上就是“这一层目录长什么样”的描述。

commit

commit 记录的是一次历史节点。它至少会包含:

  • 指向根 tree 的对象 ID;
  • 零个或多个父提交;
  • 作者和提交者信息;
  • 时间戳;
  • 提交说明。

注意:commit 保存的是“这个历史节点对应哪个快照”,不是“这一堆差异操作命令”。

tag

tag 有两种常见形态:

  • 轻量标签:本质是一个 ref;
  • 附注标签:本质是一个单独的 tag 对象,再由 ref 指向它。

附注标签能保存更完整的信息,比如标签说明、标签创建者等。

为什么对象模型这么重要

你以后看到的大多数 Git 行为,都是从对象模型自然推出的:

  • add 是把文件内容放进对象库并记录到 index;
  • commit 是把 index 固化成 tree,再创建 commit;
  • branch 只是给某个 commit 起一个可移动名字;
  • merge 是比较几个 commit 所指向的 tree;
  • pushfetch 其实是在传输对象和更新 refs。

所以,如果对象模型没理解,后面的命令行为就只能靠死记。

这个模型和普通文件系统有什么不同

普通文件系统通常先关心:

  • 路径;
  • 文件名;
  • 当前目录结构。

Git 更先关心:

  • 内容;
  • 快照;
  • 历史节点;
  • 引用关系。

这也是为什么 Git 能同时具备:

  • 内容去重;
  • 快照式历史;
  • 轻量分支;
  • 高效合并;
  • 高效打包传输。

python-git-reproduction 里的对应实现

这个项目的 .pygit 也是按这个模型实现的:

  • loose object 采用官方 Git 风格的 header + zlib 压缩;
  • 支持 blob、tree、commit、tag;
  • 对象 ID 使用 SHA-1;
  • 对象写入和读取都带有完整性校验。

所以学懂 Git 对象模型,不只是理论,也是在学这个项目的真正底层。