目录

git原理分析,存储对象,树对象,提交对象.md

[TOC]

git存储对象(HashMap)

Git 是一个内容寻址文件系统,其核心部分是一个简单的键值对数据库(key-value data store),你可以向数据库中插入任意内容,它会返回一个用于取回该值的 hash 键。

1
2
3
4
5
6
7
# 将 'Let us go' 存入键值库,然后返回相应对象的key值
echo 'Let us go' | git hash-object -w --stdin
4ab1f81f376332da3ca586a3ff63c25d6b8bf062

# 通过 git cat-file -p <key> 可以得到相应的value
git cat-file -p 4ab1f81f376332da3ca586a3ff63c25d6b8bf062
Let us go

Git基于该功能 把每个文件按照不同版本分别保存在数据库中,当要进行版本回滚的时候就通过其中一个键将期取回并替换。下面我们来演示一下:

1
2
3
4
# 新建一个git工作空间
git init
# 查找所有的git对象
find .git/objects/ -type f

此时没有git对象

1
2
# 写入版本1
echo 'version1' > README.MF; git hash-object -w README.MF;

http://img.cana.space/picStore/20211220162425.png

在Git中,根据commit的sha1值40个十六进制数字进行了简单的划分目录,以前2位数字作为目录名,其下面是剩余38位数字组成的一个文件名。

再向 README.MF 中写入两次

1
2
3
4
5
6
7
# 写入版本2
$ echo 'version2' > README.MF; git hash-object -w README.MF;
df7af2c382e49245443687973ceb711b2b74cb4a

# 写入版本3
$ echo 'version3' > README.MF; git hash-object -w README.MF;
777d3c2b51e73cc9177c34d14d9b6079b7c3ae7d

问题一:那么现在 README.MF 中的内容是是什么?version3,因为它是最后写入的。

1
2
$ cat README.MF
version3

问题二:如果我现在想版本回滚,怎么搞?读取之前版本的内容,然后再写入到 README.MF 就好了

1
2
3
4
5
6
7
8
$ find .git/objects -type f
.git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2
.git/objects/77/7d3c2b51e73cc9177c34d14d9b6079b7c3ae7d

$ git cat-file -p df7a > README.MF
$ cat README.MF
version2

问题三:你说的这些跟我 git add 又有什么关系呢?

我们通过实例来演示一下,

1
2
3
4
# 新建一个文件
echo 'hello world' > test.txt
# 查看暂存区状态
git status

http://img.cana.space/picStore/20211220163746.png

1
2
3
4
5
# 查找所有的git对象
$ find .git/objects -type f
.git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2
.git/objects/77/7d3c2b51e73cc9177c34d14d9b6079b7c3ae7d

可以看到现在存在的是之前存的v2,v1,v3的哈希对象

1
2
3
4
# 将 test.txt 添加到暂存区
git add test.txt
# 再次查找所有的git对象
find .git/objects/ -type f

http://img.cana.space/picStore/20211220163956.png

可以看见在.git\objects\ 目录下多了一个哈希对象。所以,当我们执行 git add test.txt 等同于执行了 git hash-object -w test.txt 把文件写到数据库中。

注意,上面如果我们 git add README.MF 并不会产生新的哈希对象,因为它的三种存在形式 v1,v2,v3 的哈希对象已经在里面了。

我们解决了存储的问题,但其只能存储内容,并没有存储文件名,如果要进行回滚怎么知道哪个内容对应哪个文件呢?接下要说的就是树对象,它解决了文件名存储的问题 。

git树对象

树对像解决了文件名的问题,它的目的将多个文件名组织在一起,其内包含多个文件名称与其对应的Key和其它树对像的引用,可以理解成操作系统当中的文件夹,一个文件夹包含多个文件和多个其它文件夹。

http://img.cana.space/picStore/20211220164506.png

每一个分支当中都关联了一个树对像,他存储了当前分支下所有的文件名及对应的 key。

http://img.cana.space/picStore/20211220171707.png

git提交对象

一次提交即为当前版本的一个快照,该快照就是通过提交对像保存,其存储的内容为:一个顶级树对象、上一次提交的对像啥希、提交者用户名及邮箱、提交时间戳、提交评论。

下面我们就来演示一下树对象和提交对象:

1
2
3
4
# 提交上面的 test.txt
$ git commit -am 'test'
# 再次查找所有的git对象
find .git/objects/ -type f

http://img.cana.space/picStore/20211220164915.png

问题一:可以看到相比提交之前,多了两个新的哈希对象,所以这俩对象是什么呢?提交对象和树对象

1
2
# 查看提交情况,我们可以得到提交对象的key值,以此来区分提交对象和树对象
git log

http://img.cana.space/picStore/20211220165208.png

问题二:提交对象有什么?树对象的key值 + 作者信息 + 提交信息 + 提交注释

1
$ git cat-file -p 559b

http://img.cana.space/picStore/20211220165443.png

问题三:那这个树对象里存的是什么呢?内容对象的 key 值 + 文件名

1
2
$ git cat-file -p c3b8
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad	test.txt

http://img.cana.space/picStore/20211220165702.png

通过上面的分析推理,我们可以推测出从修改一个文件到提交的过程总共生成了三个对像:

  1. 一个内容对象(add后生成) ==> 存储了文件内容
  2. 一个提交对像(commit后生成) ==> 存储了树对像的 key
  3. 一个树对像(commit后生成) ==> 存储了文件名及内容对像的 key

问题四:多级目录的树对象是什么样呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 新建一个工作空间
cd ..; mkdir hash02; cd hash02; git init;
# 创建一个目录
mkdir -p src/main/java/com
# 在目录底层创建一个文件
echo 'hello java' > src/main/java/com/hello.java

# 加入暂存区并提交
git add -A; git commit -am 'hello'
# 查找所有的git对象
$ find .git/objects/ -type f
.git/objects//0d/5fd5e3f28eeb99f12200f6763863dc8ff454a0
.git/objects//92/6d8f6502d1546c36b0284016853ea43800cd13
.git/objects//63/49162fdf1fc358aa6c38b5c414fdb67f4fce62
.git/objects//a1/58e71c23418e55639ca153a8728594147a40c5
.git/objects//79/72b3cab0fc6556ded9686a420124d6bd3c1a71
.git/objects//41/b8e96a018f6b16524f4fc60b186ed00adf26b0
.git/objects//23/985632781b319da0a19b9143e6894f8455cbb2

我们可以看到,这次他创建了7个哈希对象,那为什么会有7个呢?

http://img.cana.space/picStore/20211220170108.png

可以推导出,不管有没有文件夹,都会有一个顶级树对象

问题五:如果我对目录结构进行了变化,这些哈希对象如何变化呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ cd src/main
$ touch newFile.txt
# 将新文件提交
git add -A; git commit -am 'test'
# 查看git对象
cd ../.. && find .git/objects/ -type f
.git/objects/0d/5fd5e3f28eeb99f12200f6763863dc8ff454a0
.git/objects/92/6d8f6502d1546c36b0284016853ea43800cd13
.git/objects/ca/dacc9e111ab61d231698d184f4183c5390e06e
.git/objects/6b/ae3ec583d67c146891bcdc676ff033b14024c8
.git/objects/63/49162fdf1fc358aa6c38b5c414fdb67f4fce62
.git/objects/a0/fae046da51d69d5cccb4294e5b0689ba948a84
.git/objects/a1/58e71c23418e55639ca153a8728594147a40c5
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/e0/dc228c918c6d6e63ce88a0f5026a3d67ec2399
.git/objects/79/72b3cab0fc6556ded9686a420124d6bd3c1a71
.git/objects/41/b8e96a018f6b16524f4fc60b186ed00adf26b0
.git/objects/23/985632781b319da0a19b9143e6894f8455cbb2

可以发现他变成成了 12 个git对象,多了5个为什么呢?

http://img.cana.space/picStore/20211220170502.png

所以,Git 这么设计有什么优势呢?

  1. 有没有点像链表的感觉,所以 Git 添加分支和切换分支都很快。
  2. 如果我们要回滚到某个快照时,我们需要遍历提交对象指向的树对象,通过不断比对 key 值,然后从树对象中找到相应存储对象的 key。因为比对的只是是 key 所以回滚的速度也很快。

git引用

当我们执行 git branch {branchName} 时创建了一个分支,其本质就是在git 基于指定提交创建了一个引用文件,保存在 .git\refs\heads\ 下。

原来只有master分支,现在我们新建一个分支dev

1
2
3
4
git branch dev
$ cd .git/refs/heads
$ ls
dev    master

问题一:那这分支两个文件中存储的是什么呢?

1
2
3
4
5
6
7
8
9
$ pwd
/Users/david/hash02/.git/refs/heads
# david @ MacBook-Pro in ~/hash02/.git/refs/heads on git:master o [17:09:04]
$ cat master
cadacc9e111ab61d231698d184f4183c5390e06e

# david @ MacBook-Pro in ~/hash02/.git/refs/heads on git:master o [17:09:43]
$ cat dev
cadacc9e111ab61d231698d184f4183c5390e06e

问题二:它们俩的值是一样的,所以 cadacc9e111ab61d231698d184f4183c5390e06e 是什么?最近一次提交对象的 key。

1
2
3
4
5
6
7
$ git cat-file -p cada
tree e0dc228c918c6d6e63ce88a0f5026a3d67ec2399
parent 41b8e96a018f6b16524f4fc60b186ed00adf26b0
author lienhui <enhui.li@amh-group.com> 1639990986 +0800
committer lienhui <enhui.li@amh-group.com> 1639990986 +0800

test

Git的commit对象与相应tree对象之间的关系如下图所示:

http://img.cana.space/picStore/20211220171844.png

总结

  • blob对象只能保存某个文件的内容本身以及它的唯一键,它并不会保存文件名。而tree对象,不仅可以保存文件名,还可以保存多个文件的内容及其唯一键,而且它还允许嵌套子树(subtree),即:让一个tree对象包含另一个tree对象。所以,如果说blob对象和文件相对应,那么tree对象就是和目录相对应的。
  • 利用tree对象,我们实际上为本地Git库保存了一份反映当前更改的快照(snapshot),因为它包含了本次要提交的全部更改。但问题在于,我们必须记住这些快照的唯一键,才能利用键值得到快照所对应的变更。并且,除了具体变更内容外,我们并不知道是谁,在什么时候,保存了这份快照,以及为什么要保存(相当于git commit时提供的备注信息)。这些信息都可以包含在一个commit类型的Git对象里。
  • 本质上,我们在执行git add/commit命令时,Git在背后做的就是上面这些工作:把修改过的文件存成blob对象,更新index,写入tree对象,写入commit对象并指向tree对象,在多个commit对象之间建立起前后相继的关联关系。包括blob,tree,以及commit对象在内,每个Git对象都会对应到.git/objects目录下的一个文件。

参考