我们知道Git与SVN有着很多区别。Git相比SVN更加高效,其中主要的原因就是它把文件内容按元数据形式存储,可以理解为存到了一种类似K/V型的数据库里。
那么我们来分析下,它到底是如何存储文件以及如何管理提交与回滚的。
1.基础环境准备
在当前目录初始化一个用于测试的Git仓库git_test_01
$ git init git_test_01;cd git_test_01;
创建一个文件并写入内容
$ echo 'first line' > test-file-01.txt;
添加到暂存区并且提交该文件
$ git add -A;git commit -am "first commit";
使用git log --pretty=oneline
查看提交
如此,我们便成功的提交了一个文件。那么让我们进入**.git目录下的objects**文件夹看看发生了什么。
我们发现这里有个d8
开头的目录,与我们上次提交后产生的hash码的开头前两位是一样的。
我们使用命令ls -l d8
看看它究竟有什么
这里是一个名称为85f1211e0cd1930bfdeecda5ac85998639f7d5
的文件,我们发现将d8
和这个文件名组合一下居然和上面的提交id是一样的。这两者有什么关联呢?
2.使用Git命令查看提交内容
2.1 内容写入及读取
在探究上面使用git 的 commit后生成上述目录及文件的原理之前,我们先看看如何使用git命令将内容写入到上面我们提到的K/V数据库里。
$ echo 'first write content to object' | git hash-object -w --stdin;
返回给我们一个hash值7163feb56727aea8393398aac17ec94e037da1b8
;
既然是一个K/V型数据库,那么使用该key应该可以获取到value才对。git给我们提供这样一个命令
$ git cat-file -p 7163feb56727aea8393398aac17ec94e037da1b8;
我们发现使用该命令根据key(也就是哈希码)获取到了文件内容。
接着我们使用命令查看一下有多少个提交文件。
find .git/objects/ -type f;
我们发现总共有四个结果。其中最后一个就是我们这次用git hash-object -w --stdin
写入后返回的key值。我们还看到了前面提交的d885f1211e0cd1930bfdeecda5ac85998639f7d5
,至于为什么还会产生其它两个我们暂时不管,后面将揭晓。
那按照上面的结果,我们是不是可以猜测下,当我们每次写入一个新的不同内容到对象里时,都会产生一个结果呢?
结果确实如我们所料,当我们的second和third的内容写入后,确实生成了不同的提交,返回了不同的key。但是大家有没有发现,我最后一次操作,所 写入的内容和我们第一次写入的内容是一致的,都是first。但是它是没有生成不同的key的,没有产生新的文件。你会发现它仍然和我们提交的第一次返回的7163feb56727aea8393398aac17ec94e037da1b8
是一个值。
到此,我们可以做个小总结。Git会对你提交的内容进行一个运算,返回一个基于内容的唯一key值,根据这个key值你可以获取到相关的内容。并且如果你提交的内容前后是一样的,git计算出的key值自然也是和上次是一样的。所以从这点可以看出,Git是一个基于内容的文件寻址系统。
上述如果将 echo结果输出到一个文件中,那么当每次add文件时其实就相当于执行了git hash-object -w --stdin
命令,将修改后的内容存储到数据库中。
2.2 内容回滚指定版本
假设我们需要回滚某个文件的版本,只需要取出指定key的内容重新写入到这个文件中就行了,是不是有点感觉了?
比如我们要回滚second的内容
$ git cat-file -p 53082820b5e5 > test-file-01.txt;
注意提交的key不需要全部,只截取开头的部分也是可以的,只要能保证唯一性。
可以发现我们的文件内容已经被更新了!由此我们思考到,每次我们修改文件后,如果前后内容不一致,就产生了一个文件(其实是blob类型)。当我们需要回到该文件的某个版本时,再根据key读取出内容重新写入即可。
但是你是否有个疑问,我一次提交可能提交多个文件,多个文件夹(目录),Git是怎么知道每次提交的各个文件名的呢?
3.Git的树对象
Git采用树对象来解决文件名的问题。一个树对象中包含了多个文件名称与其对应的key,还有其它树对象的引用。
我们现在来验证这个树对象的存在。
由于上面的测试我们将git_test_01文件内容写为了second write content to object
,我们先提交本次修改。
$ git add -A;git commit -am'second commit';
现在,让我们在当前目录新增一个 文件git_test_02.txt,再新增一个文件 /a/b/git_test_0.3.txt
$ echo 'second file' > git_test_02.txt;
$ mkdir -p ./a/b; echo 'third file' > ./a/b/git_test_03.txt;
最后执行提交。
我们使用命令git cat-file -p master^{tree}
查看分支树
然后使用命令git log --pretty=oneline
再次查看提交历史
接着使用命令git cat-file -p
查看当前最新的提交
可以发现对git log中最新一次提交的key 查看后,内容包括了如下几部分
- tree-本次提交的顶级树对象
- parent-上一次提交的key(通过类似指针/引用 的方式指向上一次提交)
- author-提交者
- comitter-邮箱等信息
- 最下方的提交内容
那么我们接着查看这个tree对象的key对象的内容
再和上面我们查看的提交对象的内容比对下
你会发现,惊人的一样!
这就解释了为什么我们当前分支的各个文件及目录的版本信息,和我们当前所处提交的版本信息是一致的,且记录着各个文件、文件名、目录。
你会发现,我们的a目录成为了一个tree对象被包含在了当前的顶级树对象中。我们的文件成为了blob类型的元数据文件。
最后的结构如下图:
假设我们此时要修改文件,git_test_03.txt的内容,然后再做一次提交。
我们对本次提交进行树对象的分析,得到如下的结构图:
比较两个树,我们用红色标注了发生变化的key值
情况,并且下面逐一解析为什么会发生变化。
- parent发生变化,指向了上次提交的key,也就是提交对象1
- git_test_03.txt文件内容发生变化,当提交后产生新的key
- 因为b里面存的是git_test_03.txt的key,该key发生了变化,因此b本身也会变化
- 因为a里面存的是b的key,因为b的key发生了变化,因此a本身也发生了变化
- 顶级tree包含了a,git_test_02.txt,test-file-01.txt,虽然两个文件没发生变化,但是a发生了变化,因此顶级tree也变化
小结:
当commit后,至少会生成当前
提交对象
,生成顶级树对象
。以及该树对象包含的文件、子树对象等。其实这棵树的存储方式类似于链表,存储的是就是指针/引用的概念,也就是key,git根据该key就可以获取到value。
当每次回滚,代码切换的时候,只需要比对树即可更新相应的文件内容!如果文件内容没变的话,对应的key也不会变化,也就无需重写等操作了。因此git的效率是非常高的,因为它只需要比对树对象里各个key值即可。
4.分支是什么?
我们创建一个新分支develop
$ git checkout -b develop;
进入到.git/refs/heads
目录下,并查看文件
比对两个文件的内容
发现内容都是一样的,保存的是当前提交对象的key值。那么根据这个key值就可以得到完整的提交信息以及顶级树对象了。也就能获取到各个文件的key了,根据key又可以获取文件内容了!
这其实就是git分支的本质,其实就是记录了一次提交的key值!
上述如果我们develop和master不同自然两个文件里记录的也是不一样的提交对象key值!
同理,我们可以推导出标签🏷 tag也是一个道理,只不过它不在heads下,而是在tags下。大家可以自己试验一下
5.总结
本篇博客分享了个人对于Git原理的学习心得,如果有不当之处希望积极指出,这可以帮助我纠正不足同时也能让更多的人有一定的收获,避免踩坑~谢谢大家,后续会继续分享学习心得。最后附一下Git基础命令脑图