理解 Git 对象模型:blob、tree、commit 与 ref 的底层原理

大多数开发者每天都在使用 git addgit commitgit push,但对 Git 内部如何存储数据却知之甚少。当你真正理解了 Git 的对象模型,很多看似“魔法”的行为——比如为什么切换分支如此之快、为什么 Git 能精确追踪文件重命名、为什么 reset 有三种模式——都会变得理所当然。本文将从底层数据结构出发,系统讲解 Git 的四种核心对象:blob、tree、commit 和 ref。

Git 是一个内容寻址文件系统

Git 的核心是一个键值数据库。你往里面存入任意内容,它会返回一个唯一的键,之后你可以用这个键取回内容。这个键就是内容的 SHA-1 哈希值(Git 正在逐步迁移到 SHA-256)。

Git 将数据存储在 .git/objects 目录下。每个对象经过 zlib 压缩后存放,文件路径由哈希值的前两个字符作为子目录、后 38 个字符作为文件名。例如哈希 a1b2c3... 对应的路径是 .git/objects/a1/b2c3...

关键点在于:相同的输入永远产生相同的哈希。这意味着如果两个文件内容完全一样,Git 只会存储一份。这也是 Git 仓库通常比 SVN 等系统更紧凑的原因之一。

Git 有四种对象类型:blobtreecommittag。加上 ref 作为引用机制,构成了完整的版本控制模型。

blob:存储文件内容

blob(Binary Large Object)是 Git 中最基本的对象,它只存储文件的内容,不包含文件名、权限或任何元数据。

创建一个 blob 非常简单:

echo "hello world" | git hash-object -w --stdin
# 输出: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

你可以用 git cat-file -p 3b18e5 取回内容。注意,blob 的哈希只取决于内容本身——两个不同路径下内容相同的文件,共享同一个 blob 对象。

blob 的存储格式为:blob <内容长度>\0<内容>。头部信息也参与哈希计算,这防止了不同对象类型之间的哈希碰撞。

tree:目录结构的快照

单个 blob 只能表示一个文件。要表示目录结构,Git 使用 tree 对象。一个 tree 包含若干条目,每个条目指向一个 blob(文件)或另一个 tree(子目录)。

git cat-file -p 查看一个 tree 的典型输出:

100644 blob a1b2c3d4...    README.md
100755 blob e5f6a7b8...    build.sh
040000 tree 9c8d7e6f...    src

每行包含三部分:

  • 模式100644 表示普通文件,100755 表示可执行文件,040000 表示子目录,120000 表示符号链接。
  • 对象类型和哈希:指向具体的 blob 或 tree。
  • 名称:该条目在目录中的文件名。

tree 对象完美地表示了某一时刻的目录快照。由于 tree 可以嵌套,整个项目的目录结构就形成了一棵有向无环图(DAG)。修改一个文件只会产生新的 blob 和从根到该文件路径上的新 tree,其余 tree 对象完全复用——这就是 Git 高效存储的秘诀。

commit:时间线上的节点

commit 对象将一棵 tree(代表项目某个时刻的完整快照)与历史记录关联起来。一个 commit 包含以下信息:

tree 9c8d7e6f...
parent 1a2b3c4d...
author Alice <alice@example.com> 1700000000 +0800
committer Alice <alice@example.com> 1700000000 +0800

feat: add user authentication

关键字段解读:

  • tree:指向本次提交对应的根 tree 对象,代表项目在该时刻的完整状态。
  • parent:指向父 commit。普通提交有一个父节点,合并提交有两个或多个父节点,初始提交没有父节点。
  • author / committer:作者和提交者信息,包括时间戳和时区。注意这两者可以不同——比如你 rebase 别人的提交时,author 不变但 committer 变成你。
  • 提交信息:描述本次变更的文本。

commit 的哈希由其全部内容计算得出,包括 tree 哈希、parent 哈希、作者信息和提交信息。这意味着任何微小的改动都会产生完全不同的 commit 哈希。这也解释了为什么修改历史(如 rebase)会导致所有后续 commit 的哈希全部改变。

通过 parent 指针,commit 形成了一条链(或更准确地说,一个有向无环图)。git log 本质上就是从某个 commit 出发,沿着 parent 指针不断回溯的过程。

ref:人类可读的指针

如果每次操作都要输入 40 位的 SHA-1 哈希,Git 就没法用了。ref(引用)就是指向 commit 的指针,用人类可读的名称代替哈希值。

常见的 ref 类型包括:

  • 分支.git/refs/heads/main 是一个文本文件,内容就是该分支最新 commit 的哈希。
  • 标签.git/refs/tags/v1.0 指向标签对应的 commit(或 tag 对象)。
  • 远程跟踪分支.git/refs/remotes/origin/main
  • HEAD.git/HEAD 是一个特殊文件,通常内容是 ref: refs/heads/main,表示当前所在分支。

分支的本质就是一个包含 commit 哈希的文件。创建分支就是写入一个 41 字节的文件(40 字符哈希加换行符),这就是为什么 Git 创建分支几乎瞬间完成——它只是写了一个文件而已。

HEAD 的间接引用机制也很优雅:当 HEAD 指向 refs/heads/main 时,提交新 commit 会自动更新 main 的哈希值。而当你 git checkout 某个具体 commit 时,HEAD 会直接包含哈希值,进入“分离头指针”状态。

四者如何协同工作

让我们用一个完整流程串联这些概念:

  1. 你编辑了 src/app.js,运行 git add src/app.js。Git 对文件内容计算哈希,创建 blob 对象存入 .git/objects
  2. 运行 git commit。Git 根据暂存区的状态创建新的 tree 对象(包括 src/ 子 tree 和根 tree),每个 tree 指向对应的 blob。
  3. Git 创建一个 commit 对象,指向根 tree,parent 设为当前 HEAD 指向的 commit。
  4. Git 更新当前分支 ref(如 refs/heads/main),使其指向新 commit 的哈希。

整个过程没有任何文件被“复制”或“移动”——只有新对象的创建和指针的更新。

理解对象模型的实际价值

掌握这些底层原理后,很多日常操作会变得清晰:

  • git reset --soft vs --mixed vs --hard:三者的区别在于移动 HEAD/分支指针后,是否更新暂存区(index)和工作目录。对象本身从不被删除,只是引用关系变了。
  • git gc 和悬空对象:当对象不再被任何 ref 或 tree 引用时,它会成为“悬空对象”,最终被垃圾回收。git reflog 保留的引用能防止对象被过早回收。
  • 浅克隆为什么快--depth=1 只拉取最新的 commit 及其 tree/blob,跳过了历史 commit 对象。
  • git cat-filegit ls-tree:这些底层命令让你直接查看对象内容,是调试仓库问题的利器。

Git 的对象模型设计精巧而简洁:blob 存内容,tree 存结构,commit 存历史,ref 存入口。四者各司其职,共同构建出一个高效、可靠、分布式的版本控制系统。下次当你执行 git commit 时,不妨想想背后那些正在 .git/objects 中诞生的新对象——你已经在用底层思维理解 Git 了。

未经允许不得转载:任鹏个人博客 » 理解 Git 对象模型:blob、tree、commit 与 ref 的底层原理

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏