Git管理文件的原理分析以及Git的树对象

我们知道Git与SVN有着很多区别。Git相比SVN更加高效,其中主要的原因就是它把文件内容按元数据形式存储,可以理解为存到了一种类似K/V型的数据库里。

image-20200222075617191那么我们来分析下,它到底是如何存储文件以及如何管理提交与回滚的。


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查看提交

image-20200222083157204

如此,我们便成功的提交了一个文件。那么让我们进入**.git目录下的objects**文件夹看看发生了什么。

image-20200222083616879

我们发现这里有个d8开头的目录,与我们上次提交后产生的hash码的开头前两位是一样的。

我们使用命令ls -l d8看看它究竟有什么

image-20200222084000069

这里是一个名称为85f1211e0cd1930bfdeecda5ac85998639f7d5的文件,我们发现将d8和这个文件名组合一下居然和上面的提交id是一样的。这两者有什么关联呢?


2.使用Git命令查看提交内容

2.1 内容写入及读取

在探究上面使用git 的 commit后生成上述目录及文件的原理之前,我们先看看如何使用git命令将内容写入到上面我们提到的K/V数据库里。

$ echo 'first write content to object' | git hash-object -w --stdin;

image-20200222085717811

返回给我们一个hash值7163feb56727aea8393398aac17ec94e037da1b8;

既然是一个K/V型数据库,那么使用该key应该可以获取到value才对。git给我们提供这样一个命令

$ git cat-file -p 7163feb56727aea8393398aac17ec94e037da1b8;

image-20200222090125894

我们发现使用该命令根据key(也就是哈希码)获取到了文件内容。


接着我们使用命令查看一下有多少个提交文件。

find .git/objects/ -type f;

image-20200222090639792

我们发现总共有四个结果。其中最后一个就是我们这次用git hash-object -w --stdin写入后返回的key值。我们还看到了前面提交的d885f1211e0cd1930bfdeecda5ac85998639f7d5,至于为什么还会产生其它两个我们暂时不管,后面将揭晓。


那按照上面的结果,我们是不是可以猜测下,当我们每次写入一个新的不同内容到对象里时,都会产生一个结果呢?

image-20200222091601482

结果确实如我们所料,当我们的second和third的内容写入后,确实生成了不同的提交,返回了不同的key。但是大家有没有发现,我最后一次操作,所 写入的内容和我们第一次写入的内容是一致的,都是first。但是它是没有生成不同的key的,没有产生新的文件。你会发现它仍然和我们提交的第一次返回的7163feb56727aea8393398aac17ec94e037da1b8是一个值。

到此,我们可以做个小总结。Git会对你提交的内容进行一个运算,返回一个基于内容的唯一key值,根据这个key值你可以获取到相关的内容。并且如果你提交的内容前后是一样的,git计算出的key值自然也是和上次是一样的。所以从这点可以看出,Git是一个基于内容的文件寻址系统

上述如果将 echo结果输出到一个文件中,那么当每次add文件时其实就相当于执行了git hash-object -w --stdin命令,将修改后的内容存储到数据库中。

image-20200222093306636


2.2 内容回滚指定版本

假设我们需要回滚某个文件的版本,只需要取出指定key的内容重新写入到这个文件中就行了,是不是有点感觉了?

比如我们要回滚second的内容

$ git cat-file -p 53082820b5e5 > test-file-01.txt;

image-20200222093637219

注意提交的key不需要全部,只截取开头的部分也是可以的,只要能保证唯一性。

可以发现我们的文件内容已经被更新了!由此我们思考到,每次我们修改文件后,如果前后内容不一致,就产生了一个文件(其实是blob类型)。当我们需要回到该文件的某个版本时,再根据key读取出内容重新写入即可。

image-20200222094412515

但是你是否有个疑问,我一次提交可能提交多个文件,多个文件夹(目录),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}查看分支树

image-20200222100815028

然后使用命令git log --pretty=oneline再次查看提交历史

image-20200222101014384

接着使用命令git cat-file -p查看当前最新的提交

image-20200222101531409

可以发现对git log中最新一次提交的key 查看后,内容包括了如下几部分

  • tree-本次提交的顶级树对象
  • parent-上一次提交的key(通过类似指针/引用 的方式指向上一次提交)
  • author-提交者
  • comitter-邮箱等信息
  • 最下方的提交内容

那么我们接着查看这个tree对象的key对象的内容

image-20200222102011679

再和上面我们查看的提交对象的内容比对下

image-20200222100815028

你会发现,惊人的一样!

这就解释了为什么我们当前分支的各个文件及目录的版本信息,和我们当前所处提交的版本信息是一致的,且记录着各个文件、文件名、目录。

你会发现,我们的a目录成为了一个tree对象被包含在了当前的顶级树对象中。我们的文件成为了blob类型的元数据文件。

最后的结构如下图:

提交对象1


假设我们此时要修改文件,git_test_03.txt的内容,然后再做一次提交。

image-20200222131151533

image-20200222131524501

我们对本次提交进行树对象的分析,得到如下的结构图:

提交对象3

比较两个树,我们用红色标注了发生变化的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目录下,并查看文件

image-20200222142434657

比对两个文件的内容

image-20200222142522766

发现内容都是一样的,保存的是当前提交对象的key值。那么根据这个key值就可以得到完整的提交信息以及顶级树对象了。也就能获取到各个文件的key了,根据key又可以获取文件内容了!

这其实就是git分支的本质,其实就是记录了一次提交的key值!

上述如果我们develop和master不同自然两个文件里记录的也是不一样的提交对象key值!

同理,我们可以推导出标签🏷 tag也是一个道理,只不过它不在heads下,而是在tags下。大家可以自己试验一下


5.总结

本篇博客分享了个人对于Git原理的学习心得,如果有不当之处希望积极指出,这可以帮助我纠正不足同时也能让更多的人有一定的收获,避免踩坑~谢谢大家,后续会继续分享学习心得。最后附一下Git基础命令脑图

Git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章