Git本质上是一种内容文件系统,以Git对象作为载体,存储着一个个内容版本。不妨把Git仓库想象成一个书架,上面摆放的一本本书就如同Git对象,而每本书存储的正是对应内容的各个版本。
Git对象堪称Git的最小构成单元,Git所有核心底层命令实际上都是对Git对象的操作。例如,git add命令会将文件快照存储为blob
对象;git commit命令则把提交的文件列表和提交信息分别存储为tree
对象与commit
对象;git checkout -b这一创建分支的命令,其作用是创建一个指针,使其指向commit
对象 。
本文将从一个空仓库入手,由浅入深、循序渐进地详细阐释Git的内部原理及底层对象。
一、首先初始化工程
# 初始化工程
$ git init
Initialized empty Git repository in /Users/xxx/workspace/git-inside/.git/
# 查看目录结构
$ tree -a
└── .git
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── ...... # 省略
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
git 初始化时,实际上是在仓库下创建了一个 .git
目录的隐藏目录,以及一些默认的文件:
HEAD
:HEAD
指针,指向当前的操作分支。config
: 存储的本地仓库的配置。description
: 用来存储仓库名称以及仓库的描述信息。hooks/*
: git 钩子,git 钩子可以做非常有用的事情,也是构建 git 工作流中不可或缺的部分,更多参考官方文档。info/exclude
: 该文件的功能和 .gitignore 一样,都是配置 git 忽略本地文件。objects/*
: git 的底层对象。refs/heads
和refs/tags
: git 引用,实现了git 的分支策略,具体参考 Git Refs。
实际上还有更多不常用的文件和目录,更详细的细节可以查阅:Git Repository Layout。
二、Git的几个基础概念
工作区(Workspace)
工作区即我们当前的工作空间,呈现为本地文件夹中的文件结构。在初始化工作区或工作区处于clean状态时,文件内容与index暂存区保持一致。但随着对文件的修改,在尚未将其add到暂存区之前,工作区与暂存区的内容就会出现不一致。
暂存区(Index)
在旧版本中,暂存区也被称为Cache区,它是文件的临时存放地。所有暂存于该区域的文件,会在执行commit操作时一并提交至local repository。此时,local repository中的文件将完全被暂存区的内容所更新。暂存区在Git的架构设计中极为关键,同时也是较难理解的部分。
本地仓库(Local Repository)
Git作为分布式版本控制系统,与其他版本控制系统的显著区别在于其去中心化特性。用户无需与中央服务器(Remote Server)通信,便能在本地完成全部离线操作,如log记录查看、历史追溯、提交(commit)以及差异比较(diff)等。实现这一离线操作的核心在于,Git拥有一个与远程仓库几乎相同的本地仓库,所有本地离线操作都可在此完成,待有需要时,再与远程服务进行交互。
远程仓库(Remote Repository)
远程仓库属于中心化仓库,供所有人共享。本地仓库需要与远程仓库进行交互,以便将其他人的内容更新至本地,同时也能将自己的内容上传分享给他人。其结构与本地仓库大致相似。
三、添加一个文件
3.1、添加文件
使用 git add
命令把当前工作区的变更提交到暂存区:
# 添加文件
$ echo "git-inside" > file.txt
# 把文件添加到暂存区中
$ git add file.txt
此时查看 .git/
工作目录:
$ find .git/objects -type f
.git/objects/6f/b38b7118b554886e96fa736051f18d63a80c85
可以看到新生成了一个 git 对象,路径为.git/objects/6f/b38b7118b554886e96fa736051f18d63a80c85
。
git 对象的文件路径和名称根据文件内容的 sha1 值决定,取 sha1 值的第一个字节的 hex 值为目录,其他字节的 hex 值为名称。这里使用这种方式存储 Git 对象有 2 个好处:
- 对 Git 对象做完整性校验。
- 快速遍历/查找 Git 对象。 为了减少存储大小,**git 对象都是使用 zlib 压缩存储的**。git 提供了 cat-file 命令用来格式化查看 git 对象内容:
# 查看 git 对象内容
$ git cat-file -p 6fb38b7118b554886e96fa736051f18d63a80c85
git-inside
# 查看 git 对象类型
$ git cat-file -t 6fb38b7118b554886e96fa736051f18d63a80c85
blob
可以看到 6fb38b7
(上述 git 对象的 sha1 值简写) 对象类型为 blob
对象,blob
对象存储变更文件的内容快照。
根据 sha1 的散列特性,使用 sha1 的前 7 个字符就基本可以表示该 sha1 值。Github、Gitlab 也一样。 此时查看
.git/
目录下,会新增一个 index 文件(索引文件):
$ file .git/index
.git/index: Git index, version 2, 1 entries
index
文件存储暂存区的文件列表,index
文件代表了 git 的一个重要的概念:暂存区。index
文件的详细说明可以查看 索引文件 。
index
文件使用二进制方式存储暂存区信息,通过 git 提供的 ls-file 底层命令可以查看索引文件的格式化输出:
$ git ls-files -t
H file.txt
有兴趣的同学可以使用
hexdump -C
命令查看索引文件的二进制内容。
3.2、Git的四个核心对象
Git 主要包含四个核心对象,分别为 Blob、Tree、Commit 和 Tag,它们均采用SHA-1
算法进行命名。
你可以运用git cat-file -t
命令查看每个SHA-1
所对应的对象类型,使用git cat-file -p
命令查看每个对象的内容以及简单的数据结构。git cat-file
堪称 Git 的“瑞士军刀”,是 Git 底层的核心命令。
Blob 对象
Blob 对象专门用于存储单个文件的内容,通常为二进制数据文件。它不包含任何与文件相关的额外信息,例如文件名和其他元数据。
blob 对象由<type>
+ <size>
+ <file-content>
拼装并压缩:
可以使用 git cat-file 命令查看blob对象内容:
# 查看 blob 对象内容
$ git cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795
temp_git_file_tBTXFM: blob
$ cat ./temp_git_file_tBTXFM
hello git!
Tree 对象
tree 对象用于存储多个提交文件的信息。 tree 对象由 <type>
+ <size>
+ 文件模式
+ 文件名称
+ 文件sha1值
拼装并压缩:
以下是具体的输出示例:
→ git cat-file -t a1fbc09
commit
→ git cat-file -p a1fbc09
tree 2593e52a5c6e27d139a5ef811bd5c301183b0c3b
parent 5e9e1971331bf290654eb9097ec528644c37b468
parent 7091432b7dc29850e2f2ef980a29d899ab9e0d55
author lifei6671 <lifei6671@gmail.com> 1734599268 +0800
committer lifei6671 <lifei6671@gmail.com> 1734599268 +0800
Merge branch 'rb_20241219_1' into master
→ git cat-file -p 2593e52a5c6e27d139a5ef811bd5c301183b0c3b
100644 blob b888edbb2d6e42ab61e587e4de24bcfa8ac83a0f .gitignore
100644 blob c7ce47922d2bce16ef3e554d768127ff24b4773c BCLOUD
100644 blob b2165ab1eefb49914274b85a06483784aba6d9a2 Dockerfile
100644 blob c9b712941f9e11991019ef32976a9783f8eca8cf Makefile
100644 blob 07a955bc711d7e70d87e1032ef1b8fd9dc58f0ca README.md
040000 tree 234d068db0b8401c90b686bbec94f45f2d9ad2a1 bootstrap
100644 blob c715a4ee19e04e24c398ae4857b608e3e1158524 ci.yml
040000 tree 904d705c019d068d10264dbafaeee805341da521 conf
040000 tree 0697f64f2376e6482b57fa6dcba6b74bbd76a022 conf_base
040000 tree b0a65a4803ada34a62b2eb5ad71bed556439d4c3 conf_online
040000 tree c174bff60c68ee0e1119fe2a9ef333cd81dd7408 conf_qa
100644 blob a12f1a0bbd0498262ed4df6704036435573259ac go.env
100644 blob 7272cd52c9ca414282ce7bdeea49e6b427cbc718 go.mod
100644 blob e857ed54c3531dfda0945b8f3a835f8f0c26ccdf go.sum
040000 tree 8af6a5afc8ac9796aa12ffcb395a75136a3442fb httpapi
040000 tree ce4a5d3732de8d3d03b5ad203070c20459daa2e0 internal
040000 tree 3763f63d45d2c28d0dc21f3f1e26b2aa4b0c4535 k8s_description
040000 tree ada2ded8bd5f8d0a6c3ea536f1210f7c286e0532 k8s_description_qa
040000 tree 232027e3d2b1955ecd303b51aa4a2734af8fb11f library
100644 blob aa905c028b862fbf05bc5491fbce690875e46c71 main.go
100644 blob e3fb85a4926d8f7710f6402df93b2a9bdc23c6ec main_test.go
040000 tree 23c7b2fcf564ffbbaed51857c14442962319cc41 mock
040000 tree ee1f20c31367397867884ef4792e71d4bd749c7e model
040000 tree 8fc68ee336b8045f918c7dc35870c3f096c694d2 script
详细解释如下:
Commit 对象
commit 对象存储一次提交的信息,包括所在的tree信息,parent信息以及提交的作者等信息。 commit 对象由<type>
+ <size>
+ <tree, sha1>
+ <parent, sha1>*
+ <author-info>
+ <committer-info>
+ <comment>
拼装并压缩:
以下是一个 Commit 对象的示例:
→ git cat-file -t a1fbc09
commit
→ git cat-file -p a1fbc09
tree 2593e52a5c6e27d139a5ef811bd5c301183b0c3b
parent 5e9e1971331bf290654eb9097ec528644c37b468
parent 7091432b7dc29850e2f2ef980a29d899ab9e0d55
author lifei6671 <lifei6671@gmail.com> 1734599268 +0800
committer lifei6671 <lifei6671@gmail.com> 1734599268 +0800
Merge branch 'rb_20241219_1' into master
详细解释如下:
Tag 对象
Tag 可被视为一个“固化的分支”,一旦为某个版本打上 Tag 之后,该 Tag 所代表的内容将永远保持不变,因为 Tag 只会关联当时版本库中最后一个 Commit 对象。
与之不同的是,分支随着不断的提交操作,其内容会持续发生改变,这是因为分支指向的最后一个 Commit 对象会不断更新。因此,在应用或软件版本发布时,通常会使用 Tag 来标记特定的版本。
3.3、Index文件
索引文件默认路径为:.git/index
。索引文件用来存储暂存区的相关文件信息,当运行 git add
命令时会把工作区的变更文件信息添加到该索引文件中。索引文件以如下的格式存储暂存区内容:
索引文件 - 标头条目 | ||
---|---|---|
00 - 03(4 字节) | DIRC | 用于目录缓存条目的固定标头。 |
所有索引文件的开头都是此条目。 | ||
04 - 07(4 字节) | Version | 索引版本号(适用于 Windows 的 Git当前使用版本 2)。 |
08 - 11(4 字节) | 条目数 | 作为 4 字节的值,索引最多支持4,294,967,296 个条目! |
索引文件 - 索引条目 | ||
- | - | - |
4 字节 | 32 位创建时间(以秒为单位) | 与 1970 年 1 月 1 日 00:00:00 之间相隔的秒数。 |
4 字节 | 32 位创建时间 - 纳秒组成部分 | 创建时间的纳秒组成部分(以秒为单位)。 |
4 字节 | 32 位修改时间(以秒为单位) | 与 1970 年 1 月 1 日 00:00:00 之间相隔的秒数。 |
4 字节 | 32 位修改时间 - 纳秒组成部分 | 修改时间的纳秒组成部分(以秒为单位)。 |
4 字节 | 设备 | 与文件相关的元数据,源自 Unix OS 上使用的文件属性。 |
4 字节 | Inode | |
4 字节 | mode | |
4 字节 | 用户 ID | |
4 字节 | 组 ID | |
4 字节 | 文件内容长度 | 文件内容字节数。 |
20 字节 | SHA-1 | 相应 blob 对象的 SHA-1 值。 |
2 个字节 | 标记 | 包括文件名长度等信息。 |
长度不固定 | 路径/文件名 | 以空字符结尾 |
读过源码的同学会发现,其实还有一个叫
.git/index.lock
的文件,该文件存在时表示当前工作区被锁定,代表有 git 进程正在操作该仓库。 使用ls-files
可以读取索引文件存储的文件信息:
git ls-files --stage
100644 5664e303b5dc2e9ef8e14a0845d9486ec1920afd 0 README.md
100644 45c7a584f300657dba878a542a6ab3b510b63aa3 0 doc/changelog
100644 aec2e48cbf0a881d893ccdd9c0d4bbaf011b5b23 0 file.txt
当然,ls-files
的输出内容也是经过格式化的。跟 Git 对象 不一样,索引文件 .git/indx
并没有经过 zlib 压缩,使用 hexdump
工具就可以直接查看原始数据:
$ hexdump -C .git/index
00000000 44 49 52 43 00 00 00 02 00 00 00 03 5f cb 65 22 |DIRC........_.e"|
00000010 22 be 40 2c 5f cb 65 22 22 be 40 2c 01 00 00 04 |".@,_.e"".@,....|
00000020 01 3e 09 e3 00 00 81 a4 00 00 01 f6 00 00 00 14 |.>..............|
00000030 00 00 00 04 56 64 e3 03 b5 dc 2e 9e f8 e1 4a 08 |....Vd........J.|
00000040 45 d9 48 6e c1 92 0a fd 00 09 52 45 41 44 4d 45 |E.Hn......README|
00000050 2e 6d 64 00 5f cb 65 26 01 bd 63 4e 5f cb 65 26 |.md._.e&..cN_.e&|
00000060 01 bd 63 4e 01 00 00 04 01 3e 09 f4 00 00 81 a4 |..cN.....>......|
00000070 00 00 01 f6 00 00 00 14 00 00 00 07 45 c7 a5 84 |............E...|
00000080 f3 00 65 7d ba 87 8a 54 2a 6a b3 b5 10 b6 3a a3 |..e}...T*j....:.|
00000090 00 0d 64 6f 63 2f 63 68 61 6e 67 65 6c 6f 67 00 |..doc/changelog.|
000000a0 00 00 00 00 5f cb 65 1f 17 f9 45 e9 5f cb 65 1f |...._.e...E._.e.|
000000b0 17 f9 45 e9 01 00 00 04 01 3e 08 92 00 00 81 a4 |..E......>......|
000000c0 00 00 01 f6 00 00 00 14 00 00 00 1a ae c2 e4 8c |................|
000000d0 bf 0a 88 1d 89 3c cd d9 c0 d4 bb af 01 1b 5b 23 |.....<........[#|
000000e0 00 08 66 69 6c 65 2e 74 78 74 00 00 54 52 45 45 |..file.txt..TREE|
000000f0 00 00 00 35 00 33 20 31 0a 10 da 37 41 b6 e3 65 |...5.3 1...7A..e|
00000100 b6 79 53 35 e1 e2 d3 ed 58 20 e7 94 cd 64 6f 63 |.yS5....X ...doc|
00000110 00 31 20 30 0a 39 fb 0f bc ac 51 f6 6b 51 4f bd |.1 0.9....Q.kQO.|
00000120 58 9a 5b 2b c0 80 9c e6 64 ac 8f 88 7a 1e a4 d0 |X.[+....d...z...|
00000130 b9 83 8d 83 72 4e 7b 71 d2 d8 a0 a5 3d |....rN{q....=|
索引文件大部分内容都是以二进制存储的,可读性很差,喜欢钻研的同学可以去看源码。
3.4、HEAD文件
HEAD 具体路径为 .git/HEAD
,HEAD
实际上是一个指针,指向具体的引用或者 commit-id
,比如 HEAD 指向 master
分支时是如下内容:
$ cat .git/HEAD
ref: refs/heads/master
如果 checkout 了一个特定的 commit-id
时,那 HEAD 的值是这个 commit-id
。
$ git checkout 523d41ce82ea993e7c7df8be1292b2eac84d4659
$ cat .git/HEAD
523d41ce82ea993e7c7df8be1292b2eac84d4659
3.5、引用
Git 引用名义上是指针,实际上是一个很简单的文件,这个文件存储的是指向的提交的 commit-id
:
$ cat .git/refs/heads/master
a0e96b5ee9f1a3a73f340ff7d1d6fe2031291bb0
四、提交到本地版本库
使用 git commit
命令可以把暂存区的变动提交到本地版本库中:
$ git commit -m "first commit"
[master (root-commit) 523d41c] first commit
1 file changed, 1 insertion(+)
create mode 100644 file.txt
其中
100644
是指的文件模式,100644
表明这是一个普通文件。 其他情况比如100755
表示可执行文件,120000
表示符号链接。 如果你是边阅读本文边动手操作,那你会发现生成的 commit 对象的 sha1 值跟本文不一致,因为提交日期以及用户名邮箱是不一样的,可以点击这里 设置固定的时间日期、用户名和邮箱,这样提交的对象就会是一样的 sha1值,也方便阅读本文。
查看 .git/objects
目录下,会新增 2 个 git 对象:
$ find .git/objects -type f
.git/objects/41/20b5f61a582cb12d4dcdaab71c7ef1862dbbca
.git/objects/52/3d41ce82ea993e7c7df8be1292b2eac84d4659
.git/objects/6f/b38b7118b554886e96fa736051f18d63a80c85
分别是 523d41c
和 4120b5f
。
使用 git cat-file
可以看到 2 个 对象的类型和内容:
# 523d41c 是一个 commit 对象
$ git cat-file -t 523d41c
commit
$ git cat-file -p 523d41c
tree 4120b5f61a582cb12d4dcdaab71c7ef1862dbbca
author lifei6671 <lifei6671@gmail.com> 1734599268 +0800
committer lifei6671 <lifei6671@gmail.com> 1734599268 +0800
first commit
# 4120b5f 是一个 tree 对象
$ git cat-file -t 4120b5f
commit
$ git cat-file -p 4120b5f
100644 blob 6fb38b7118b554886e96fa736051f18d63a80c85 file.txt
也可以使用
git cat-file -p 523d41c^{tree}
来查看4120b5f
的内容,523d41c^{tree}
和4120b5f
是等效的。 这里新出现了 2 种新的 git 对象类型,分别是tree
对象(523d41c
) 和commit
对象(4120b5f
),tree 对象用来记录目录结构和 blob 对象索引,commit 对象包含着指向前述 tree 对象的指针和所有提交信息。
操作到这里,git 的底层对象一共生成了 3 个,分别是:
6fb38b7
: blob 对象。4120b5f
: tree 对象,指向6fb38b7
。523d41c
: commit 对象,指向4120b5f
。 他们之间的关系是:
五、提交第二个版本
我们继续提交代码和文件:
$ echo "append content" >> file.txt
$ echo "git" > README.md
$ mkdir doc && echo "v0.0.1" > doc/changelog
$ git add -A
$ git commit -m "second commit"
[master a0e96b5] second commit
3 files changed, 3 insertions(+)
create mode 100644 README.md
create mode 100644 doc/changelog
该提交为 file.txt
添加了内容,同时新增了子目录:doc/
,并新增了 README.md
和 doc/changelog
2个文件。
查看 git 对象列表:
$ find .git/objects -type f | sort
.git/objects/10/da3741b6e365b6795335e1e2d3ed5820e794cd # tree | 第二次提交
.git/objects/39/fb0fbcac51f66b514fbd589a5b2bc0809ce664 # tree: doc/ | 第二次提交
.git/objects/41/20b5f61a582cb12d4dcdaab71c7ef1862dbbca # tree | 第一次提交
.git/objects/45/c7a584f300657dba878a542a6ab3b510b63aa3 # blob | changelog
.git/objects/52/3d41ce82ea993e7c7df8be1292b2eac84d4659 # commit | 第一次提交
.git/objects/56/64e303b5dc2e9ef8e14a0845d9486ec1920afd # blob | README.md
.git/objects/6f/b38b7118b554886e96fa736051f18d63a80c85 # blob | 第一次提交 | file.txt
.git/objects/a0/e96b5ee9f1a3a73f340ff7d1d6fe2031291bb0 # commit | 第二次提交
.git/objects/ae/c2e48cbf0a881d893ccdd9c0d4bbaf011b5b23 # blob | 第二次提交 | file.txt
可以看到除了原先的 6fb38b7
、4120b5f
、523d41c
,又新增了:
10da374
: tree 对象,指向README.md
(5664e30
) 、file.txt
(aec2e48
)、doc/
(39fb0fb
)。39fb0fb
: tree 对象,指向changelog
(45c7a58
)。45c7a58
: blob 对象, 存储changelog
内容快照。5664e30
: blob 对象,存储README.md
内容快照。a0e96b5
: commit 对象,指向10da374
、523d41c
。aec2e48
: blob 对象,存储更改的file.txt
内容快照。 查看新增的 2 个 tree 对象:
$ git cat-file -p 10da374
100644 blob 5664e303b5dc2e9ef8e14a0845d9486ec1920afd README.md
040000 tree 39fb0fbcac51f66b514fbd589a5b2bc0809ce664 doc
100644 blob aec2e48cbf0a881d893ccdd9c0d4bbaf011b5b23 file.txt
$ git cat-file -p 39fb0fb
100644 blob 45c7a584f300657dba878a542a6ab3b510b63aa3 changelog
这里有必要说明一下,Git 使用 tree 对象来存储目录结构,不同的目录对应不同的 tree 对象,这次提交里面,顶层目录对应的 tree 是 10da374
,doc/
目录对应的 tree 是 39fb0fb
。
继续查看 commit 对象 a0e96b5
内容:
$ git cat-file -p a0e96b5
tree 10da3741b6e365b6795335e1e2d3ed5820e794cd
parent 523d41ce82ea993e7c7df8be1292b2eac84d4659
author lifei6671 <lifei6671@gmail.com> 1734599268 +0800
committer lifei6671 <lifei6671@gmail.com> 1734599268 +0800
second commit
仔细的同学会发现,a0e96b5
跟第一次提交生成的 commit 对象(523d41c
)相比,多了一个 parent
字段。parent
字段是用来指向上一次提交的,一般是1个 parent ,有些情况下会是多个 parent ,比如 merge 这种情况。
我们再总结一下这些对象之间的关系:
如图所示,每一次提交可以是一个文件,也可以是多个文件和多个目录,一次提交就是一次版本。
同时这里又引申出来了 git 的一个非常重要的概念,每一次新的提交都会指向上一个提交,这样多个提交就组成了一个提交链。这个提交链使用到了一个非常有名的算法:梅克尔树,感兴趣的同学可以去深入了解,这里就不深入讲解了。merkle tree
有一个重要的特性就是单独更改其中一个节点的内容就会破坏掉这个tree,也就是说 merkle tree
的节点是不可更改的。git 就是通过 merkle tree
来保证每个版本都是连续有效的。
这就是为什么很难修改 git 的历史提交记录的原因,如果要修改某一个提交,那同时还需要修改这个提交之后的所有提交,这样才能保证
merkle tree
是有效成立的。 另外,区块链也是基于merkle tree
来保证数据可靠性的。 可以猜想一下,如果继续提交代码,那 git 对象会是如下的关系:
按照先后时间顺序单独看
commit
对象之间的关系:
这个
commit
对象关系图非常重要,git 分支策略就是围绕着这个关系图来运作的,这里暂且不做展开。
五、打标签
上面的操作涉及了 3 种 git 对象,分别是 blob
、tree
、commit
对象,其实 git 还存在一个 tag
类型的对象,用来存储带注释的标签。
使用如下命令创建标签:
$ git tag "v0.0.2" -m "this is annotated tag"
# 查看 git 对象和引用
$ find .git/objects -type f | sort
.git/objects/03/2ddd9205d65abd773af1610038c764f46a0b12 # tag
.git/objects/10/da3741b6e365b6795335e1e2d3ed5820e794cd # tree | 第二次提交
.git/objects/39/fb0fbcac51f66b514fbd589a5b2bc0809ce664 # tree: doc/ | 第二次提交
.git/objects/41/20b5f61a582cb12d4dcdaab71c7ef1862dbbca # tree | 第一次提交
.git/objects/45/c7a584f300657dba878a542a6ab3b510b63aa3 # blob | changelog
.git/objects/52/3d41ce82ea993e7c7df8be1292b2eac84d4659 # commit | 第一次提交
.git/objects/56/64e303b5dc2e9ef8e14a0845d9486ec1920afd # blob | README.md
.git/objects/6f/b38b7118b554886e96fa736051f18d63a80c85 # blob | 第一次提交 | file.txt
.git/objects/a0/e96b5ee9f1a3a73f340ff7d1d6fe2031291bb0 # commit | 第二次提交
.git/objects/ae/c2e48cbf0a881d893ccdd9c0d4bbaf011b5b23 # blob | 第二次提交 | file.txt
$ tree .git/refs
.git/refs
├── heads
│ └── master
└── tags
└── v0.0.2 # tag 引用
此时新增了一个 032ddd9
的对象,同时在 .git/refs/
中增加了名为 v0.0.2
的标签。使用如下命令查看他们的内容:
# 查看 v0.0.2 的内容
$ cat .git/refs/tags/v0.0.2
032ddd9205d65abd773af1610038c764f46a0b12
# 查看 032ddd9 的类型
$ git cat-file -t 032ddd9
tag
# 查看 032ddd9 的内容
$ git cat-file -p 032ddd9
object a0e96b5ee9f1a3a73f340ff7d1d6fe2031291bb0
type commit
tag v0.0.2
tagger lifei6671 <lifei6671@gmail.com> 1734599268 +0800
this is annotated tag
.git/refs/tags/v0.0.2
是 Git 的一个重要的概念:引用。这个引用实际上是一个指针,内容为 032ddd9
的 sha1 值,代表指向 032ddd9
。而 032ddd9
是一个 tag 对象,指向第二次提交的 commit 对象:a0e96b5
。
tag 对象相对比较独立,不参与构建文件系统,只是单纯的存储信息。
六、总结
到这里其实应该已经对 Git 底层对象有一个深刻的了解了。从根本上来讲,git 底层实际上是由一个个对象(object)组成的,git 底层对象分为4种:
- blob 对象:保存着文件快照。
- tree 对象:记录着目录结构和 blob 对象索引。
- commit 对象:包含着指向前述 tree 对象的指针和所有提交信息。
- tag 对象:记录带注释的 tag 。
一个仓库里面的所有 Git 对象会组成一个图(Graph),按照指向关系可以简单的这么理解:
refs
–>tag 对象
–>commit 对象
–>tree 对象
–>blob 对象
,对象之间通过对方的 sha1 值来确定指向关系,所以要是篡改了对象的内容,那指向关系就会被破坏掉,git fsck 命令就会提示"hash mismatch"
。所以这也是 Git 对象的文件存储结构里面并没有自身数据的校验(checksum)字段的原因。
值得一提的是,git 社区正在积极推进 sha256 的方案,sha1 目前来看并不是绝对安全的,因为 HAttered attack 这种攻击方式能够伪造相同 sha1 值。
最后,我们用一张图来总结上述的一系列步骤生成的对象之间的关系:
git 对象的相关命令
git 擅长的一点是提供了很多丰富抽象的子命令来操作这些 git 对象,比如上面的一系列操作:
git add
:实际上是把当前工作区的文件快照保存下来,产出是 blob 对象。git commit
:保存暂存区的文件层级关系和提交者信息,产出是 tree 对象 和 commit 对象。git tag -m
:保存 tag 标签的信息,产出是 tag 对象。 这些是上层命令,实际上 git 还提供了非常丰富的底层命令用来操作对象:- git-hash-object:把输入内容存储成 blob 对象。
- git-cat-file:读取并格式化输出对象。
- git-count-objects:计算对象数量。
- git-write-tree:把存储区的文件结构存储成 tree 对象。
- git-read-tree:把 tree 对象读取到暂存区。
- git-commit-tree:根据输入信息(tree、父提交、author、commiter、日期等)存储成 commit 对象。
- git-ls-tree:读取并格式化输出 tree 对象。
- git-mktag:把输入内容存储成 tag 对象。
- git-mktree:根据输入(
ls-tree
的输出格式)来生成 tree 对象。 - git-fsck:校验对象链表的正确性和有效性。
- git-diff-tree:比较 2 个tree 对象 的差异并格式化输出。
设置固定的时间日期、用户名和邮箱
本文中的示例都设置了固定的时间日期、用户名和邮箱,如果你是边阅读本文边动手操作,可以如下执行 git commit
或者 git tag
,这样生成的对象hash值和本文中的是一致的:
# git commit
$ GIT_AUTHOR_DATE="1606913178 +0800" GIT_AUTHOR_NAME="lifei6671" GIT_AUTHOR_EMAIL="lifei6671@gmail.com" GIT_COMMITTER_DATE="1606913178 +0800" GIT_COMMITTER_NAME="lifei6671" GIT_COMMITTER_EMAIL="lifei6671@gmail.com" git commit -m "first commit"
# git tag
$ GIT_AUTHOR_DATE="1606913178 +0800" GIT_AUTHOR_NAME="lifei6671" GIT_AUTHOR_EMAIL="lifei6671@gmail.com" GIT_COMMITTER_DATE="1606913178 +0800" GIT_COMMITTER_NAME="lifei6671" GIT_COMMITTER_EMAIL="775117471@gmail.com" git tag "v0.0.2" -m "this is annotated tag"
或者可以使用 export
设置为全局的环境变量:
export GIT_AUTHOR_DATE="1606913178 +0800" GIT_AUTHOR_NAME="lifei6671" GIT_AUTHOR_EMAIL="lifei6671@gmail.com" GIT_COMMITTER_DATE="1606913178 +0800" GIT_COMMITTER_NAME="lifei6671" GIT_COMMITTER_EMAIL="lifei6671@gmail.com"
七、参考
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。
文章由技术书栈整理,本文链接:https://study.disign.me/article/202510/19.git-data-structure.md
发布时间: 2025-03-07