Learn Git Data Structure
Intro
Exactly twenty years ago, on April 7, 2005, Linus Torvalds made the very first commit to a new version control system called Git.
2005年内核团队因 BitKeeper 许可争议需要一个source control的替代工具,Torvalds 在hate CVS with a passion 的情况下决定自己开发。Torvalds 在2004年末开始思考设计方案,2005年初集中用约10天完成 Git 第一个可用版本
So I was like, okay, I’ll do something that works for me, and I won’t care about anybody else. And really that showed in the first few months and years—people were complaining that it was kind of hard to use, not intuitive enough.
Git turns 20: A Q&A with Linus Torvalds - The GitHub Blog
考虑最初只是为 Linux 内核开发团队服务,早期的 Git 只有底层的plumbing commands、缺乏友好的porcelain commands。后面经过社区的努力,Git 才逐渐演变成一个功能强大、相对易于使用的版本控制系统(但相对其他SCM工具,Git 仍然有点吃操作)。
现在 Git 已经成为了最流行的版本控制系统之一,广泛应用于开源和商业项目中, Git 的成功很大程度归功于其network effect:项目基本都使用 Git,后面诞生的新项目也就跟着默认使用 Git。
命令示例 | 说明 | |
---|---|---|
🛠 Plumbing | git hash-object 、git cat-file 、git update-index 、git write-tree | 控制 Git 底层对象的操作,如计算哈希、写入 Git 数据库等 |
🧑💻 Porcelain | git add 、git commit 、git push 、git status | 高级封装,用于日常开发的典型工作流 |
在日常开发中,我们使用的主要是 porcelain 命令(I use LazyGit btw🤓),底层的 plumbing 命令则是 Git 的核心实现。理解Git的数据结构与对象模型,有助于更深入地把握其工作原理,从而降低Git命令的“黑盒”感。不过,即便不熟悉这些底层细节,通常也不会影响日常使用。
下面只简单介绍 Git 的数据结构与对象模型,部分Porcelain命令的实现原理有时间再补充。
Objects
Git 的版本库中主要存储着四种类型的对象,存放路径为 .git/objects/
目录。每个对象都有一个唯一的 SHA-1 哈希值,Git 使用这个哈希值来寻找对象。
对象类型 | 主要用途 | 存储内容 | 可变性 |
---|---|---|---|
Blob | 存储文件内容 | 仅文件原始数据 | 不可变 |
Tree | 表示目录结构 | 文件/目录条目列表 | 不可变 |
Commit | 记录版本快照 | 项目状态元数据 | 不可变 |
Tag | 标记特定版本 | 标记对象的元数据 | 不可变 |
Blob: Binary Large OBject
SHA: Secure Hash Algorithm
Graph
对象之间的指向关系如下,通过记录 SHA-1 哈希值来实现对象之间的引用。
例如 Commit 2
对象存储一个指向 Tree
对象的 SHA-1 值,以及它Parent Commit
(即Commit1
) 的 SHA-1 值。
特别注意几个地方
- Commit对象记录了:
Tree SHA-1
指向项目根目录状态的 tree 对象Parent(s) SHA-1
指向一个或多个父提交对象
- 每个
commit
都指向一个完整的项目快照(通过 tree 对象),而不是记录文件间的差异。差异是在需要时(如git diff
)动态计算出来的。
1
2
3
4
5
6
7
8
9
10
COMMIT1 --> COMMIT2 --> COMMIT3 --> ...
| | |
v v v
TREE TREE TREE
/ | \ / | \ / | \
v v v v v v v v v
B B T B B T B B B
/ \ / \
v v v v
B B B B
一般我们关心的是Commit
对象,它们通过Parent SHA-1
形成一个有向无环图 - DAG,每个提交对象都指向它的父提交对象。通过这个结构,Git 可以高效地跟踪项目的历史版本。
一个分支可能会有多个parent commit,例如下面合并操作产生的Merge commit
,它就有两个父节点

Releated Plumbing Commands
可以用上面所说的plumbing commands验证git object的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 查看blob对象的内容
$ git cat-file -p 38223a8b88cb79e6c50587200dddaa2bee632039
*.log
*.log.*
history.txt
**/state.yml
**/state.json
misc/yasb/*.py
elevated-state.json
lazy-lock.json
*.bak
# mac
.DS_Store
# chrome extension
*.pak
# 查看当前HEAD指向的commit对象的内容
$ git cat-file -p HEAD~
tree 6aa9038b7598f6c2fd28e1452ec12ce704889868
parent df297f141f21ac41c6a225214b35729209e30b63
parent b0a01d5c938c4bf617141898ecda2bfeeba39408
author Efterklang <[email protected]m> 1747413172 +0800
committer Efterklang <[email protected]m> 1747413172 +0800
Merge branch 'main' of github.com:Efterklang/config
# 上面输出了一个tree对象的SHA-1值,查看tree对象的内容
$ git cat-file -p 6aa9038b7598f6c2fd28e1452ec12ce704889868
100644 blob 38223a8b88cb79e6c50587200dddaa2bee632039 .gitignore
100644 blob 1af14d0fa103a0b5dd83294bbffe622afb7fc3b7 .gitmodules
100644 blob 5acc0b549e71749f3dcfed94df73799688f3fca5 LICENSE.md
100644 blob 5f01256728f790fb046b4a5ddf56b686711c1479 README.md
040000 tree b5e786a9baf2ddc55c0dcf76f0aaf33c914d271b application
040000 tree 9c417f15cdf72696abfd4b9d0de89922c714ca3e assets
160000 commit b8891c5fb72485316fba54d2c1310320c9ebf4d5 dotbot
100755 blob 841bc2e32b2f56a44569d24a481c6d4902dfcf68 install.ps1
100755 blob 89195ec2a3b9f68939b77a6b71d0834ffc1603e2 install.sh
100644 blob 7f6171b6606dbd811e4ce74b445ba08e09c86545 linux.yaml
100644 blob 522b64e9e3533bf61720e054667189eda272727e mac.yaml
040000 tree e9734049cb57b7427d64b24691a8ac7849352538 misc
040000 tree 18a69f6feba9237aeccac753abe939f3218ccfa3 packages
040000 tree 70301eeee8f9a1f3db7d7c6e35c7da73deaf83f0 shells
040000 tree 62f6055d5e5983c976802e570c565b4e68b2068d terminal
040000 tree adf3d257c2b7e8b63677d0488bf8939d00a7ace8 tui_cli
100644 blob 551b11fa060531730df24bf230182d8bf3052526 windows.yaml
References
引用类型 | 存储位置 | 主要用途 | 示例 |
---|---|---|---|
Local Branch | refs/heads/ | 标记本地开发线 | refs/heads/master |
Remote Branch | refs/remotes/ | 跟踪远程仓库分支 | refs/remotes/origin/master |
Tag | refs/tags/ | 标记特定提交点 | refs/tags/v1.0 |
HEAD | .git/HEAD | 指向当前工作点 | HEAD |
在 Git 中,“引用” (reference, 通常简写为 ref) 是指向提交对象 (commit object) 或其他引用 (如另一个分支或标签) 的指针。它们是 Git 用来追踪历史和分支的便捷方式。常见的引用有分支名 (如 main, feat/gjx) 和标签名 (如 v1.0.0
Git Refs: What You Need to Know | Atlassian Git Tutorial关于 Git Refs的详细介绍可以参考这篇文章。
Ref的设计有这么几个好处:
- 可读性/可管理性:使用人类可读的名称来引用对象,而不是直接使用 SHA-1 哈希值。最主要使用的ref应该就是branch了,多人协作时,使用分支名比 SHA-1 哈希值更容易理解和管理
- 轻量级:引用本身是轻量级的,只存储指向对象的 SHA-1 哈希值,而不是完整的对象数据,这使得引用的存储和操作更加高效
Common Refs
HEAD 是 Git 中最重要和最常用的特殊引用,通常指向当前检出的本地分支的最新提交(Symbolic HEAD
),但也可以“分离”出来直接指向某个特定提交(Detached HEAD
)。储在项目根目录下的 .git/HEAD
文件中。
另外,在执行git merge
, git cherry-pick
, git rebase
等操作时,Git 会创建一些临时引用,如 MERGE_HEAD
, CHERRY_PICK_HEAD
, REBASE_HEAD
等。这些引用用于跟踪正在进行的操作的状态。
Branch
在 Git 的内部实现中,branch实际上是对某个提交(commit)的引用。存储在 .git/refs/heads/
目录下
例如,我建了main
,bugfix/fix1
,bugfix/别急
, feat/gjx
四个分支,在文件系统中体现为:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ll ./.git/refs/heads/
drwxr-xr-x - gjx 21 May 00:25 bugfix
drwxr-xr-x - gjx 21 May 00:24 feat
.rw-r--r-- 41 gjx 18 May 23:17 main
$ tree ./.git/refs/heads/
./.git/refs/heads/
├── bugfix
│ ├── 别急
│ └── fix1
├── feat
│ └── gjx
└── main
Git 分支名允许使用 /
作为分隔符,这是因为在 .git/refs/heads/
目录下,Git 会将分支名中的 /
解释为子目录分隔符。这样在大型repo中,可以使用层级结构来组织分支,便于管理和查找。有兴趣可以参考Conventional Branch
Ref本身是一个文本文件,内容就是一个 SHA-1 哈希值,指向当前分支的最新提交对象。比如我用cat
命令查看一下bugfix/fix1
分支的内容,输出一行SHA-1哈希值,参考上面的操作,看下这个分支的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 分支就是一文本文件,直接cat出来
$ cat .git/refs/heads/bugfix/fix1
File: .git/refs/heads/bugfix/fix1
Size: 41 B
1 eac2c62fcf0fe905ea475d1eb94d0e6d42916da8
# 看看这个commit的内容
$ git cat-file -p eac2c62fcf0fe905ea475d1eb94d0e6d42916da8
tree b09d937e4cdc5628e5818b08c7f3c303b5223470
parent c53af81f5493d6c7aa1b7da30670da8b168d2c5c
author Efterklang <[email protected]m> 1747581423 +0800
committer Efterklang <[email protected]m> 1747581423 +0800
feat(sketchybar): add more app icons
Releated Plumbing Commands
不过多赘述,日常中也不建议使用,just for fun
1
2
3
4
5
6
7
8
9
10
11
12
# 查看所有引用
$ git show-ref
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/heads/bugfix/别急
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/heads/bugfix/fix1
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/heads/feat/gjx
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/heads/main
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/remotes/origin/HEAD
eac2c62fcf0fe905ea475d1eb94d0e6d42916da8 refs/remotes/origin/main
# 查看符号引用, 我现在在bugfix/别急分支上
$ git symbolic-ref HEAD
refs/heads/bugfix/别急
Refs
Git turns 20: A Q&A with Linus Torvalds - The GitHub Blog
Git Refs: What You Need to Know | Atlassian Git Tutorial
Conventional Branch
Learn Git Data Structure