Git Internals

Git 对象数据库

理解 blob、tree、commit、tag 四类对象,以及 Git 为什么说自己是内容寻址的对象数据库。

一句话理解

Git 最底层不是“文件版本列表”,而是一个把内容写成对象、再用对象 ID 反向定位内容的数据库。

为什么对象模型是第一层基础

很多 Git 现象只有放到对象模型里才说得通:

  • 为什么一次 commit 会生成新的对象
  • 为什么同样内容在不同分支上仍然可以复用
  • 为什么 branch 创建几乎是瞬时的
  • 为什么 reset 之后旧提交往往还没有真的消失

1. 什么叫内容寻址

Git 写入对象时,不是先给对象随便编号,而是先根据内容计算对象 ID。也就是说:

  1. 内容先存在
  2. ID 由内容决定
  3. 之后 Git 再通过这个 ID 反向找到对象

这也是“content-addressable”最重要的含义。

2. 四类最常见对象

blob

blob 只保存文件内容,不保存文件名,也不保存它在目录树中的位置。

tree

tree 负责表达目录结构。它会记录目录下有哪些名字,以及这些名字指向哪个 blob 或哪个子 tree。

commit

commit 用来固定一次项目状态。它通常会记录:

  • 当前快照对应的 tree
  • 父提交
  • 作者和提交者信息
  • 提交说明

tag

tag 为某个对象提供更稳定、更可读的名字,常见于发布版本。

3. 为什么 blob 不保存文件名

这件事经常让人第一次接触 Git internals 时觉得反直觉。Git 把“内容”和“结构”分开保存:

  • 文件内容放在 blob
  • 文件名和目录位置放在 tree

好处是内容复用会更自然。只要内容一样,底层就能共享对象,而不必把“路径”和“内容”绑死。

4. 一次 commit 在对象层到底发生了什么

从对象数据库的视角看,一次提交大致是:

  1. 根据暂存区内容写出 tree
  2. 创建一个新的 commit 对象
  3. 让 commit 指向这次 tree 和父提交
  4. 再把当前分支引用移动到新提交

所以 commit 的本质不是“改写旧历史”,而是“新增对象,再移动名字”。

5. 对象和工作区不是一回事

很多混乱来自把这两层混在一起。更稳的理解是:

  • 工作区:你正在编辑的文件
  • 暂存区:准备写入下一次快照的内容
  • 对象库:已经被 Git 固化下来的对象

只有进入对象库之后,内容才真正成为 Git 历史的一部分。

6. 为什么对象模型能解释很多命令

git add

它不是直接创建 commit,而是把当前内容朝下一次 tree 靠近。

git commit

它会真正创建新的 tree 和 commit 对象。

git branch

它通常不需要复制对象,只是新建一个指向已有提交的引用。

git checkout / git switch

它们会让工作区去匹配另一个引用所指向的对象状态。

7. 对象“存在”不等于“容易找到”

这是理解恢复操作时最关键的一点。一个对象就算还在数据库里,只要没有分支、tag、reflog 之类的名字把它接住,你就会越来越难找到它。

所以很多恢复问题其实不是“对象没了”,而是“引用丢了”。

常见误区

Git 会为每个版本完整复制整个项目吗

不应该这样理解。Git 底层是对象模型,加上后面的 packfiles 压缩和复用机制,并不是简单粗暴地重复存整份目录。

commit 只是写一段日志吗

不是。提交说明只是 commit 对象里的一部分元数据。真正关键的是它把 tree、父提交和作者信息固定了下来。

一个最实用的记忆方式

如果只记一件事,可以记住:

  1. blob 保存内容
  2. tree 保存结构
  3. commit 保存一次快照与父子关系
  4. branch 和 HEAD 只是指向这些对象的名字

后面理解 refs、历史图、rebase、reset 就会容易很多。