Appearance
Git 对象模型
Git 的核心是一个内容寻址文件系统,所有数据都以对象的形式存储。Git Doctor 直接解析这些对象,不依赖 git 命令。
对象类型
Git 有四种基本对象类型:
| 类型 | 说明 | 存储内容 |
|---|---|---|
| Blob | 文件内容 | 文件的实际数据 |
| Tree | 目录结构 | 文件名、权限、子对象引用 |
| Commit | 提交信息 | Tree 引用、父提交、作者、消息 |
| Tag | 标签 | 指向 Commit 的引用 |
Blob 对象
存储文件的实际内容,不包含文件名或任何元数据。
结构
blob <size>\0<content>示例
blob 12\0Hello World!Git Doctor 解析
typescript
// 读取压缩数据
const compressed = fs.readFileSync(objectPath)
// Zlib 解压
const decompressed = zlib.inflateSync(compressed)
// 解析头部
const nullIndex = decompressed.indexOf(0)
const header = decompressed.slice(0, nullIndex).toString()
const [type, size] = header.split(' ')
const content = decompressed.slice(nullIndex + 1)Tree 对象
存储目录结构,包含文件名、权限模式和子对象引用。
结构
tree <size>\0
<mode> <name>\0<sha-20bytes>
<mode> <name>\0<sha-20bytes>
...Mode 值说明
| Mode | 说明 |
|---|---|
100644 | 普通文件 |
100755 | 可执行文件 |
040000 | 目录(子 Tree) |
120000 | 符号链接 |
160000 | Git 子模块 |
Git Doctor 解析
typescript
function parseTreeObject(content: Buffer): TreeEntry[] {
const entries = []
let offset = 0
while (offset < content.length) {
// 解析 mode(到空格)
const spaceIndex = content.indexOf(0x20, offset)
const mode = content.slice(offset, spaceIndex).toString()
// 解析 name(到 NUL)
const nullIndex = content.indexOf(0x00, spaceIndex)
const name = content.slice(spaceIndex + 1, nullIndex).toString()
// 读取 20 字节 SHA(二进制)
const sha = content.slice(nullIndex + 1, nullIndex + 21).toString('hex')
entries.push({ mode, name, sha })
offset = nullIndex + 21
}
return entries
}Commit 对象
存储提交的元数据。
结构
commit <size>\0
tree <tree-sha>
parent <parent-sha>
author <name> <email> <timestamp> <timezone>
committer <name> <email> <timestamp> <timezone>
<commit message>字段说明
| 字段 | 说明 |
|---|---|
| tree | 根目录 Tree 对象的 SHA |
| parent | 父提交的 SHA(可多个或无) |
| author | 原始作者信息 |
| committer | 实际提交者信息 |
| message | 提交消息 |
Git Doctor 解析
typescript
function parseCommitObject(sha: string, content: Buffer): CommitInfo {
const text = content.toString('utf-8')
const lines = text.split('\n')
const info = { sha, tree: '', parents: [], author: '', message: '' }
let i = 0
// 解析头部字段
while (i < lines.length && lines[i] !== '') {
if (lines[i].startsWith('tree ')) {
info.tree = lines[i].slice(5)
} else if (lines[i].startsWith('parent ')) {
info.parents.push(lines[i].slice(7))
} else if (lines[i].startsWith('author ')) {
const match = lines[i].match(/^author (.+) <(.+)> (\d+)/)
if (match) {
info.author = match[1]
info.authorEmail = match[2]
info.authorDate = new Date(parseInt(match[3]) * 1000)
}
}
i++
}
// 空行后是提交消息
info.message = lines.slice(i + 1).join('\n').trim()
return info
}对象存储
松散对象(Loose Objects)
每个对象单独存储为一个文件:
.git/objects/<sha前2字符>/<sha后38字符>示例:
SHA: e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
路径: .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391存储格式
原始数据 = <type> <size>\0<content>
存储数据 = zlib.deflate(原始数据)Pack 文件
当对象数量增多时,Git 会打包对象:
.git/objects/pack/
├── pack-<hash>.pack # 打包的对象数据
└── pack-<hash>.idx # 索引文件引用系统
HEAD
.git/HEAD内容可能是:
- 直接 SHA:
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 - 符号引用:
ref: refs/heads/master
分支引用
.git/refs/
├── heads/ # 本地分支
│ ├── master
│ └── develop
├── remotes/ # 远程分支
│ └── origin/
│ └── master
└── tags/ # 标签
└── v1.0.0对象关系图
HEAD
│
▼
refs/heads/main
│
▼
┌─────────┐
│ Commit │
│ abc123 │
└────┬────┘
│ tree
▼
┌─────────┐
│ Tree │
│ def456 │
└────┬────┘
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Blob │ │ Tree │ │ Blob │
│ file.js│ │ src/ │ │ README │
└────────┘ └────────┘ └────────┘SHA-1 计算
Git 使用 SHA-1 算法计算对象哈希:
typescript
import * as crypto from 'crypto'
function computeSha(type: string, content: Buffer): string {
const header = Buffer.from(`${type} ${content.length}\0`)
const store = Buffer.concat([header, content])
return crypto.createHash('sha1').update(store).digest('hex')
}