日常工作中,经常使用git,最多使用的都是一些对人友好的命令,用久了就想了解git是如何存储文件的,如何管理文件的以及命令背后到底发生了什么。
先学习下面的知识,然后通过命令慢慢理解。
git是一套内容寻址文件系统,即任何内容都是以文件的形式存储的;然后通过文件内容生成一个键值,通过键值找到文件。git是以面向对象的形式存储文件,既然是面向对象,那肯定有对象类型,下面就学习下git中的对象类型。
git中对象类型有三种:blob对象,tree对象和committer对象。
1 blob对象
概念:blob对象是用来存储文本内容的。即把一个包含具体文本内容的文件作为一个blob对象存储在git系统中。
下面通过命令的形式创建一个blob对象。
[1] 首先新建一个git仓库
$ mkdir test-git
$ cd test-git
$ git init
Initialized empty Git repository in E:/git_code/test-git/.git/
一个干净的git仓库包含下面的文件:
$ ll
total 7
-rw-r--r-- 1 XXX8 1049089 130 三月 23 10:30 config
-rw-r--r-- 1 XXX8 1049089 73 三月 23 10:30 description
-rw-r--r-- 1 XXX8 1049089 23 三月 23 10:30 HEAD
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 hooks/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 info/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 objects/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 refs/
初始化仓库中的文件夹和文件的含义如下:
- config文件:包含项目的特定配置项,比如命令别名的配置可以配置在该文件;
- description文件:仅供GitWeb程序使用;
- HEAD文件:文件自始至终只有一行内容,表示当前分支;
- hooks/文件夹:包含客户端或者服务端的钩子文件,主要用来设置自动脚本减少手工操作;为了保证项目的安全,一般不会用到钩子脚本;
- info/文件夹:默认含有一个可执行的文件,文件为.gitignore忽略的内容;
- objects/文件夹:用来存储所有的对象信息;
- refs/文件夹:存储指向分支的提交对象的指针,包括项目中所有的分支指针;
[2] 通过命令创建一个文本文件,并生成一个blob对象:
$ echo 'test conten' | git hash-object -w --stdin
04b35ded9a6e06accbb72875d2b1f05ccbb26afb
命令中参数含义如下:
- ‘text conten’为文本内容;
- git hash-object:git底层命令,可以根据传入的文本内容返回表示这些内容的键值。
- –w:使hash-object命令存储数据,否则仅返回键值而不存储内容;
- –stdin:指定从标准输入设备如键盘来读取内容,若不用这个参数则需要指定一个要存储的文件的路径;
- 04b35ded9a6e06accbb72875d2b1f05ccbb26afb是命令返回的40位的十六进制的键值,称为SHA-1码
[3] 通过键值查看对象内容:
$ git cat-file -p 04b35ded9a6e06accbb72875d2b1f05ccbb26afb
test conten
命令中参数含义如下:
- git cat-file:git底层命令,用来查看二进制文件的内容;
- -p:让命令输出键值指向的文本内容;
[4] 查看该键值的对象类型:
$ git cat-file -t 04b35ded9a6e06accbb72875d2b1f05ccbb26afb
blob
命中参数含义如下:
- -t:让命令输出键值的类型;
[5] 查看对象的文件位置
git仓库所有对象都存在objects/文件夹下,通过命令去该文件夹下查找对象
$ cd objects/
$ ll
total 0
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:51 04/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 info/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 pack/
已经新增了04/文件夹:
$ cd 04
$ ll
total 1
-r--r--r-- 1 XXX8 1049089 28 三月 23 10:51 b35ded9a6e06accbb72875d2b1f05ccbb26afb
包含一个文件。这里不难发现,文件夹名和文件名合起来就是对象的键值。
PS:到此为止,只是存储了文本内容,并没有为其命名。
GIT之所以能够作为版本控制系统,是因为GIT会把对文件的每次修改结果作为一个对象保存起来。下面通过实例来说明这句话。
[6] 创建一个test.txt文件并写入内容’version 1’:
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
含有’version 1’内容的对象,键值为:83baae61804e65cc73a7201a7252750c76066a30
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30
version 1
[7] 往test.txt文件写入内容’version 2’:【覆盖之前的内容】
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
含有version 2内容的对象,键值为:1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
version 2
[8] 往test.txt文件新增内容’version 3’:【追加内容】
$ echo 'version 3' >> test.txt
$ git hash-object -w test.txt
d2a22b76d77aed216728332470d3673d5b582055
含有version2 version 3内容的对象,键值为:d2a22b76d77aed216728332470d3673d5b582055
$ git cat-file -p d2a22b76d77aed216728332470d3673d5b582055
version 2
version 3
[9] 查看objects/文件夹内对象个数:
$ cd objects/
$ ll
total 0
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:51 04/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 11:40 1f/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 11:39 83/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 14:29 d2/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 info/
drwxr-xr-x 1 XXX8 1049089 0 三月 23 10:30 pack/
可以通过git cat-files –p查看每个键值对应的内容。
git之所以把每次修改的结果都保存为一个blob对象,是方便版本回撤,即能把文本内容退回到任何时刻。
[10] 查看当前test.txt文本的内容:
$ cat test.txt
version 2
version 3
[11] 把test.txt内容退回到上个版本:
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
查看此时test.txt文本内容:
$ cat test.txt
version 2
本小节旨在说明git系统把文本内容存储为blob对象,通过git hash-code命令返回存储文本的键值sha-1码,通过git cat-file –p sha-1命令查看键值对应的内容。
PS:关于sha-1码的介绍见文末的附录1.
2 tree对象
知道文本通过sha-1键值存储在git系统中,sha-1键值只是存储文本的内容,并没有存储文件名,而且sha-1码是40位十六进制的数字,查找文本内容非常麻烦,因此tree对象可以存储blob对象和子tree对象,使得查找更加方便。
GIT系统通过tree对象和blob对象存储系统中所有的目录以及目录下的文本,类似于文件夹以及文件夹下的文件;因此一个tree对象可以包含若干个子tree对象和若干个blob对象,子tree对象含有一个指向blob对象或子tree对象的sha-1指针。
我们可以用test-git仓库创建一个tree对象。通常GIT是根据暂存区或者索引文件index来创建tree对象,因此要把文件存储到暂存区进并建立index文件:
[1] 建立索引文件index
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
注意,此处不能通过git –add test.txt命令添加暂存区,因为test.txt文件并不在test-git目录下,而是在.git目录下,即已经在数据库中,因此要通过plumbing命令update-index创建一个索引文件;并且把test.txt文件的’version 1’内容键值(第一版本)存储到暂存区和索引文件中。
命令中参数含义如下:
- 100644表示为文件模式。blob对象的文件模式一般都为100644,即普通文本,但也有其他模式:100755表示可执行文件,120000表示符号链接。
- –cacheinfo是必须参数,因为要添加的文件并不在当前目录下而在数据库中。
- –add是必须参数,因为该键值对是首次加入到暂存区。
[2] 查看索引文件index的内容:
$ git ls-files --stage
100644 83baae61804e65cc73a7201a7252750c76066a30 0 test.txt
注意:sha-1码后面的数字0表示文件的状态,当有冲突时不为0,其他一般状态为0.
[3] 基于当前索引文件index建立tree对象:
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
注意:返回键值sha-1码;因此可以通过键值查看tree对象的具体内容。
[4] 验证其是否是一个tree对象,并查看其内容:
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
那么一个tree对象如何加入到另一个tree对象中呢?
[5] 首先新建另外一个tree对象,该tree对象存储第二版本的test.txt和一个新文件new.txt.
$ git update-index --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ echo 'new file' > new.txt
$ git hash-object -w new.txt
fa49b077972391ad58037050f2a75f74e3671e92
$ git update-index --add --cacheinfo 100644 fa49b077972391ad58037050f2a75f74e3671e92 new.txt
```java
查看当前索引文件index内容
```java
$ git ls-files --stage
100644 fa49b077972391ad58037050f2a75f74e3671e92 0 new.txt
100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a 0 test.txt
基于当前索引文件新建tree对象
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
然后把第一个tree对象d8329fc1cc938780ffdd9f94e0d364e0ea74f579作为子目录加入该tree对象0155eb4229851634a0f03eb265b69f5a2d56f341中:
[6] 在此之前得把tree对象d8329fc1cc938780ffdd9f94e0d364e0ea74f579加入索引文件index内
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
注意:此时若提示错误,请返回上一层级目录再执行.
[7] 查看当前索引文件index内容
$ git ls-files --stage
100644 83baae61804e65cc73a7201a7252750c76066a30 0 bak/test.txt
100644 fa49b077972391ad58037050f2a75f74e3671e92 0 new.txt
100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a 0 test.txt
注意:此时索引文件index包含三个blob对象,83baae6对象位于bak目录下;
[8] 基于当前索引文件index新建tree对象,即完成子tree对象加入tree对象内.
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
[9] 查看tree对象内容
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
可以看到,第一个tree对象d8329fc1cc938780ffdd9f94e0d364e0ea74f579 已经加入到tree对象0155eb4229851634a0f03eb265b69f5a2d56f341中,并构成一个新的tree对象3c4e9cd789d88d8d89c1073707c3585e41b0e614。
此时或许会有疑惑,为什么最新的tree包含一个tree对象和两个blob对象,而不是两个tree对象呢?这是因为第二个tree对象0155eb4并没有增加到索引文件index内,而是把第一个tree对象d8329f直接增加到第二tree对象文件内。
注意:这里可以看到tree对象的文件模式为040000.
此时git系统中数据的内容结构为:
这一小节学习tree对象,着重学习如何构建tree对象,如何把子tree对象加入tree对象,也知道tree对象与blob对象的关系。
3 committer对象
最后一种git对象committer对象。committer对象用来保存提交信息,提交信息包括基于当前索引文件生成的tree对象、作者、提交者信息、提交时间以及提交备注等内容;每次提交都会产生一个committer对象。
[1] 这里创建committer对象.
$ echo 'first commit' | git commit-tree d8329f
80516b024108ecc079e39f1c0cedc6502f666b12
注意:first commit为committer对象内备注信息;commit-tree为创建committer对象的底层命令;d8329f 为tree对象d8329fc1cc938780ffdd9f94e0d364e0ea74f579的缩写。
返回该committer对象的键值sha-1码。
[2] 查看committer对象80516b024108ecc079e39f1c0cedc6502f666b12的内容.
$ git cat-file -p 80516b024108ecc079e39f1c0cedc6502f666b12
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author XXXX8 <XXXX8 @qq.com> 1553331951 +0800
committer XXXX8 <XXXX8 @qq.com> 1553331951 +0800
first commit
可知,每次commit都会基于当前索引文件index新建tree对象,那么当前索引文件是在上次提交的基础上更新来的,所以每次提交产生的committer对象有前后关系或者称为父子关系。
[6] 这里是手动创建tree对象和committer对象,因此可以在创建committer对象的时候加上此关系。
$ echo 'second commit' | git commit-tree 0155eb4 -p 80516b0
5e76ed431b5f7347b1f683b3a60b3d6d139573b6
$ echo 'third commit' | git commit-tree 3c4e9cd -p 5e76ed4
f40e5e55c2492c5140db9036c688dc74770c63b5
注意:0155eb4表示tree对象0155eb4229851634a0f03eb265b69f5a2d56f341的缩写,-p表示前一个committer对象。
[7] 至此把三个tree对象分别放到三个committer对象中,通过命令查看committer对象的内容.
$ git log --stat f40e5e55
commit f40e5e55c2492c5140db9036c688dc74770c63b5
Author: 18073638 <18073638@cnsuning.com>
Date: Sat Mar 23 17:22:13 2019 +0800
third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit 5e76ed431b5f7347b1f683b3a60b3d6d139573b6
Author: 18073638 <18073638@cnsuning.com>
Date: Sat Mar 23 17:21:07 2019 +0800
second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit 80516b024108ecc079e39f1c0cedc6502f666b12
Author: 18073638 <18073638@cnsuning.com>
Date: Sat Mar 23 17:05:51 2019 +0800
first commit
test.txt | 1 +
1 file changed, 1 insertion(+)
注意:此处无法通过git log命令查看committer对象,而只能通过git log –stata查看,这是因为所有对象的创建都是使用低级命令,而非普通命令创建的GIT历史。这些低级命令也是执行普通命令git add和git commit命令后,git系统所执行的操作。
基于GIT历史可以得到GIT所有对象关系图:
总结
今天主要学习GIT系统中的3种对象:committer对象、tree对象和blod对象。通过学习可以回答以下几个问题:
- 三种对象的含义以及其之间的关系;
- 三种对象分别包含哪些内容;
- 利用低级命令创建三种对象;
- 普通命令add和commit背后发生的操作
附录1 SHA-1
SHA(Secure Hash Algorithm)的意思是安全哈希算法。SHA-1将文件中的内容通过其hash算法生成一个160bit的报文摘要(字符串),即40个十六进制数字(每个十六进制数字占4位),如果两个文件的SHA-1值是相同的,那么它们确是完全相同的内容;当然,产生相同哈希码的可能性是存在的,但是这种可能性非常低,可以忽略。从数学的角度来讲,这种可能性是2的63次方分之一,也就是1/9223372036854775808。
SHA-1主要有两种用途,一个是加密,一个是数据完整性校验。另外,哈希函数是一种将大的变长的数据集映射到一个固定长度的较小数据集的算法。40位的哈希码很难记忆。其实大多数情况下,没必要写那么长。7位或8位的哈希码就可以基本保证提交名称具有唯一性。在本书中,主要使用缩写形式,也就是完整哈希码中的前7位,事实上,Git自己主要也是使用缩写形式。