skip to content

Search

对象原理

8 min read Updated:

Git内部对象原理

.git目录

执行git init后,Git会在当前目录下创建一个.git目录,用于存储Git的所有数据和配置

 
.git/
├── config                 # 文件:包含项目特有的配置选项
├── description            # 文件:仅供 GitWeb 程序使用,我们无需关心
├── HEAD                   # 文件:【核心】指向目前被检出的分支
├── index                  # 文件:【核心】保存暂存区信息(执行 git add 后生成)
├── hooks/                 # 目录:包含客户端或服务端的钩子脚本(hook scripts)
├── info/                  # 目录:包含一个全局性排除(global exclude)文件
├── objects/               # 目录:【核心】存储所有数据内容(Blob、Tree、Commit 等对象)
│   ├── <前2位哈希>/       # 子目录:对象存储的物理位置(松散对象)
│   │   └── <后38位哈希>   # 文件:经过 Zlib 压缩的 Git 对象
│   └── pack/              # 子目录:经 git gc 打包后的对象和索引
│       ├── *.pack
│       └── *.idx
└── refs/                  # 目录:【核心】存储指向数据(分支、远程仓库和标签等)的提交对象指针
    ├── heads/             # 子目录:本地分支引用(如 master、feature)
    │   └── <分支名>       # 文件:内容为 40 位提交哈希
    ├── remotes/           # 子目录:远程仓库引用(如 origin/main)
    │   └── <远程名>/
    │       └── <分支名>
    └── tags/              # 子目录:标签引用(轻量标签与附注标签)
        └── <标签名>       # 文件:内容为提交或对象哈希
 

Blob对象

Git将文件内容存储为Blob对象并计算其SHA-1哈希值作为唯一标识

Blob对象只包含文件内容,不包含文件名或其他元数据

Blob对象存储在.git/objects目录下,其SHA-1哈希值的前两位作为子目录,后38位作为文件名

使用 git hash-object -w <file> 可将文件内容存入对象数据库

# 写入内容为 "version 1" 的文件test.txt
git hash-object -w test.txt
# 输出:e32092a...
 
# 追加 "version 2" 后再次写入
git hash-object -w test.txt
# 输出:7834af9...

创建完成之后可以在.git/objects目录下面找到e3/2092a…与78/34af9…两个文件,分别对应两个版本的test.txt内容

使用git cat-file -p <SHA-1>可以查看Blob对象的内容

git cat-file -p e32092a
# 输出: version 1

树对象

树对象对应当前项目切片下的目录结构

树对象存储了子树对象和Blob对象的引用信息,包括模式,类型,文件名以及对应的SHA-1哈希值

使用git write-tree可以将当前暂存区的内容写入一个树对象

# 将blob对象文件名记为test.txt添加到暂存区
git update-index --add --cacheinfo 100644 e32092a test.txt
# 将新文件new.txt添加到暂存区
git update-index --add new.txt
# 生成树结构
git write-tree
# 输出:13697ff...
 
git cat-file -p 13697ff
# 输出:
# 100644 blob 1271944... new.txt
# 100644 blob e32092a... test.txt

Tree 对象是递归构建的: 执行 git read-tree —prefix=bak <树哈希>,暂存区会记录 bak/test.txt 的路径,再次 write-tree 将自动生成包含 bak/ 子树的根树对象

Commit对象

使用 git commit-tree <TreeHash> 可手工生成提交对象(不依赖 git commit):

$OutputEncoding = [System.Text.Encoding]::UTF8; "first commit`n" | git commit-tree c403552
# 输出:9616b474...
git cat-file -p 9616b47
# 输出:
# tree c403552...
# author ...
# committer ...
# 
# first commit

通过 -p 参数串联提交链,使得构建的commit有父提交,实现一条提交历史链:

$OutputEncoding = [System.Text.Encoding]::UTF8; "second commit`n" | git commit-tree 13697ff -p 9616b474

此时执行git log --stat,Git 会自动递归遍历 parent 链,并实时比对相邻 Tree 的差异,输出文件变更统计。这说明 Git 存储的是完整快照,而差异是现场计算得出的

Blob, Tree, Commit对象之间的关系

Commit对象有如下组成

  • tree: 指向一个Tree对象,表示当前提交的目录结构
  • parent: 指向一个或多个父Commit对象,形成提交历史链
  • author: 提交作者信息
  • committer: 提交者信息
  • message: 提交信息备注

Tree只指向Blob和Tree对象,形成目录结构;Blob只包含文件内容;他们关系如下

Commit (提交对象)
  └── Tree (根目录树)
        ├── Blob (文件内容) 或
        └── Tree (子目录树)
              └── Blob (文件内容)
  • Commit 变更:若文件内容改变 → Blob 哈希变 → Tree 哈希变 → Commit 哈希变。
  • Commit 复用:若只更改提交备注,仅 Commit 哈希改变,Tree 与 Blob 完全复用

Blob存储机制

Blob 对象在磁盘上的物理生成过程,遵循以下 4 个步骤

  1. 构造头部

Git构造一个包含类型和长度的头部

例如:what is up, doc? 长度为 16,头部为 “blob 16\0”,头部blob指明类型

  1. 拼接载荷

将头部和文件原始内容拼接在一起,形成完整的字节流 例如:blob 16\0what is up, doc?

  1. 计算SHA-1哈希值

对字节流进行 SHA-1 哈希计算,得到 40 位哈希值(如 bd9dbf…)

  1. Zlib 压缩与入库

使用 Zlib 算法压缩字节流得到二进制数据

取哈希前 2 位创建子目录,后 38 位作为文件名

将压缩后的二进制数据写入该文件

git add与git commit底层

git add

  1. 调用 git hash-object -w test.txt。把文件内容压缩成 Blob 对象,存入 .git/objects,并拿到对应的 40 位哈希值(比如 e32092…)
  2. 调用 git update-index。把拿到的 Blob 哈希值,连同文件名、文件模式(如 100644)写入 .git/index(暂存区)文件中

git commit

  1. 调用 git write-tree。读取当前 .git/index(暂存区)里的所有路径清单,生成一个完整的 Tree 对象(根目录树),并返回该树的哈希值
  2. 调用 git commit-tree <树哈希> -p <父哈希>。把上一步得到的树哈希、当前的 HEAD(父提交)、作者信息、时间戳以及你的备注(-m)打包在一起,生成一个新的 Commit 对象
  3. 调用 git update-ref。把当前所在的分支(比如 master)的指针,从旧的提交哈希,更新为新生成的 Commit 对象哈希