1. Git对象这套机制,解决的到底是什么问题
1.1 从一次"对象损坏"事故说起
可能很多人在学习Git时都是这个路径:先学会 add、commit、push、pull,能应付日常开发就觉得自己"会用Git了"。我之前也是这样,直到有一次在切换分支时碰到 error: object file ... is empty,整个仓库直接无法操作,才意识到自己对Git的理解停留在"命令行背诵"层面——根本不知道仓库里到底存了什么,自然也不知道该怎么修。
那次问题最后通过 git fsck 加手工拷贝对象文件恢复了,但过程相当狼狈。也是从那时候开始,我决定把Git的底层机制补一遍。今天这篇笔记,就围绕最核心的一块:Git对象。
Git对象的英文是 Git Objects,它的精译本质是"Git存储系统的最小组成单元"。一句很经典的英文表述是:
Everything in Git is an object.(Git中的一切皆对象。)
这句翻译不难,但它背后藏着一整套存储设计:一个Git仓库里保存的所有文件内容、目录结构、提交历史、标签信息,最终都会被转换成若干对象,以特定格式写入磁盘。理解了这一层,"Git为什么这么快""Git为什么删了文件还能找回""Git凭什么保证内容不被篡改"这类问题就都有了答案。
这篇内容适合两类人:一是像我这样已经会用Git、但想再往深走一步的开发者;二是刚开始学Git、不想只停留在记命令层面的新手。只要你能看懂终端里的命令输出,就可以跟着下面的实验一步步做。
1.2 快照存储 vs 差异存储:两种完全不同的哲学
理解Git对象之前,先要理解Git和传统版本控制工具在存储思路上有一个根本区别:快照存储(Snapshot)。
像SVN这类老牌工具,通常采用差异存储(Delta):只记录每个版本之间的变化量,初始版本加上一连串补丁,才能还原出某个历史版本。这种做法的优点是省空间,缺点是历史一长,还原版本的速度会变慢,而且一旦中间某个补丁损坏,后面所有版本都跟着遭殃。
Git反过来,选择的是一次快照。每次提交,Git都会把当前整个项目的文件状态完整记录一遍。听起来很浪费空间,对吧?这也是很多初学Git的人最容易怀疑的地方:每个版本都全量存一份,仓库迟早爆炸。
Git的解决办法有两个:
- 不可变对象:每个文件内容、每棵目录树、每次提交,都是一个不可修改(immutable)的对象,内容一样就复用,绝不多存。
- 打包压缩(Packfile):当对象越来越多,
git gc会把松散对象打包,并在包内部用增量压缩算法处理,所以最终仓库不会因为快照方式而无限膨胀。
我后来在整理旧仓库时专门看过:一个包含几百次提交、几万个文件的项目,.git目录体积通常比工作区加历史全部文件还小。原因就是Git在打包阶段做了相当激进的压缩。所以,"快照一定费空间"是个误区,Git的实际表现远好于直觉。
这种设计带来的直接好处是:每个提交都是完整可独立读取的。你不需要从最早版本一路应用补丁才能拿到某个历史版本,Git定位任何一个提交都几乎是瞬时的,因为那个提交本身就是一个完整的目录树快照。
1.3 内容寻址:SHA-1哈希与"对象身份证"
Git对象的第二个关键设计是内容寻址(Content-addressable):每个对象在Git中的"名字",就是它自身内容经过哈希计算出来的值。也就是说,对象的名字不是随机生成的编号,而是由内容本身推导出来的。
这一点非常重要,可以说是理解整个Git对象体系的钥匙。具体来说:
- 对象的ID是一个40位的十六进制字符串,由 SHA-1(Secure Hash Algorithm 1) 算法计算得出。
- 只要对象内容不变,这个ID就永远不变;内容哪怕只有一个字节不同,ID就会完全改变。
- 因为ID由内容决定,Git在存储时天然具备"去重"能力——相同的文件内容无论出现多少次,在仓库里只会保存一个对象。
Git官方文档里有一句很精准的总结:
Git is a content-addressable filesystem. Not a VCS. But it also happens to be a VCS.(Git是一个内容寻址文件系统,而不仅仅是一个版本控制系统,只是它恰好也能当版本控制系统用。)
这句话我一直觉得是理解Git的"题眼"。Git的底层就是一个按内容寻址的存储系统,版本控制只是建立在这套存储之上的应用。你要查某一个文件在某次提交里的内容,本质上是:提交对象 → 指向一棵树对象 → 树对象指向若干blob对象 → 在blob里拿到内容。整条链全靠哈希ID连接。
所以Git对象的官方翻译语境里,"对象"这个词的精确含义是:一个由内容哈希唯一标识、被Git存储系统管理的不可变数据单元。为了方便对照,我把核心术语的原文和精译整理在后面章节的表格里,这里先建立一个整体感觉。
1.4 不可变性:为什么对象一旦创建就绝不修改
Git对象的另一个重要特性是不可变性(Immutability)——对象一旦被写入对象库,它的内容就不会再改变。这听起来像技术细节,实际上对使用体验影响巨大。
如果一个对象的内容变了,它的哈希ID就会变,Git会把它当作一个全新对象保存。这就意味着:
- 历史不会因为分支操作而被改写:你在某个分支上执行 rebase 或 reset,看起来像是"改了历史",实际上只是创建了新的提交对象,旧的提交对象依然躺在对象库里,直到被垃圾回收。
- 数据完整性有保障:因为对象ID就是内容的哈希,Git每次读取时都可以校验对象是否被意外篡改或损坏。只要对象内容被破坏,哈希校验就会失败,Git会立刻报错,而不是默默给你一个损坏的数据。
- 并发安全:多个进程同时往同一个仓库写对象,不会互相干扰——每个对象的内容是确定的,写同一个对象和写不同对象都不会产生冲突。
我早期用Git时,总觉得"rebase后历史就消失了",后来通过 git reflog 找回了一个被"覆盖"的提交,才真正理解不可变对象的设计价值。Git不会主动毁掉任何数据,它只是把"删除"延迟到了垃圾回收阶段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四种核心对象类型逐一拆解:blob、tree、commit、tag
2.1 blob对象:只管文件内容,不管文件名
Blob 是 Git 中最基础的对象类型,全称是 Binary Large Object(二进制大对象),但在Git语境里,它代表的是文件内容的快照。
我最初理解blob时有个误区:以为blob就是"文件"。实际上blob和文件是两回事:
- blob里只有文件内容,不包含文件名、文件权限、创建时间这些元数据。
- blob也不关心你给它起了什么名字。同一个内容,你叫它
a.txt还是b.txt,产生的blob对象ID完全一样。 - blob甚至不关心它属于哪个项目——只要内容相同,跨仓库它都是同一个哈希值。
Git为什么这么设计?答案是复用与去重。文件名和目录结构属于"树"层级的职责,内容单独存放的好处是:当你把 a.txt 重命名为 b.txt 时,文件内容没变,Git只需要创建一个新的tree对象来更新文件名映射,内容对象可以原样复用;当你复制一个文件到另一个目录时,两个路径都指向同一个blob,完全不占额外空间。
在Git源码里,blob对象的构建逻辑非常直接:把输入内容在头部加上类型标识和长度信息,然后做一次SHA-1哈希。我们可以用 git hash-object 命令亲手验证这一点。
2.2 tree对象:把文件名、目录结构和权限信息补回来
blob只解决了"内容"的存储,那"这个文件叫什么、放在哪个目录、有什么权限"这些信息存放在哪里?答案是tree对象。
Tree对象在Git数据模型里扮演的是"目录"角色。官方文档里的定义是:
A tree is a simple list of trees and blobs that represent the contents of a directory.(树对象是一个简单的清单,记录了该目录下包含哪些子树和blob,用于表示目录的内容。)
一个tree对象内部大致包含以下几个字段:
- 模式(Mode):表示文件类型和权限,比如
100644代表普通文件、100755代表可执行文件、040000代表子目录、120000代表符号链接。 - 类型(Type):说明这一项是
blob还是tree。 - **文件名(Nam
