深入git数据存储原理
git数据存储区结构:
git存储区结构三个区域:
- 工作区
- 暂存区
- 版本库
工作区
工作区,顾名思义呀,就是我们文件放置的位置,这就是工作区。
暂存区
至于暂存区嘛,那就有点意思了。想当初刚开始学习git,对这个结构不清楚的时候,老是觉着在工作区写完代码,然后直接提交到本地仓库(也就是版本库)不就行了吗?为什么还要整一个略显多余的暂存区呢?后来经过自己不断地学习、使用git,又加上看了许多相关的文章,才感觉对暂存区多多少少有点新的认识了。
暂存区,从名字上理解就是文件暂存的区域,从上面这幅图中可以看到,我们可以输入命令 git add 来将相应的文件放入暂存区。好,这个时候我们可以下点本钱,来拍一部有关暂存区前世今生的小电影,那么我们首先要做点准备工作:吕导演派了个人去大街上抓来一个群众演员Doge(不要问我这个演员为什么叫Doge,哈哈哈)。
首先,打算拍一部小电影,那么就进行初步计划吧。没错,初始化一个git仓库
git初始化然后演员准备上场、出镜。是的,这是建立了一个空白文件
新建文件Doge接下来要把演员Doge送上片场,准备开始表演了。OK,就是把该文件加入到暂存区
将文件add到暂存区但是就在这个节骨眼上,Doge这位群众演员突然身体不适,站在一旁监制拍摄的吕导演一眼就看了出来他的细微变化,立刻叫停了拍摄,询问他怎么了。好吧,这就是暂存区的功能之一,之所以有它,就是让它来帮助我们监控每个被放入到暂存区的文件,如果当某些文件被修改了,那么当我们再调用 git status
就可以迅速得知这一消息。这个过程如下图:
好了。上边那个故事不编了。一句话来说就是,暂存区所具有的功能之一就是帮助我们发现哪些文件受到了修改,并在适当地时机告诉我们。(MD,突然感觉这个暂存区有种我大明朝初期锦衣卫的感觉)当然了,暂存区不仅仅就只有这一个功能,暂存区还可以将我们指定的文件纳入其中,并不是只能一次性地把工作区的所有文件都纳入。还有,在工作区和版本库,也就是"写-送"之间再设置一块区域,其实也起到了一个缓冲的作用,我们编辑完文件,添加到暂存区之后,如果出现了什么意外,我们还可以将暂存区的文件回退到工作区,这样也保证了一定程度的容灾性。
那么,接下来要和大家分享一个小秘密。这个秘密是有关暂存区是如何来监控文件是否被修改的?
我们知道,一个文件被创建出来,那么它自身就会有几个属性,比如时间戳,文件长度等。所以,当一个文件被放置到暂存区之后,这个时候暂存区有个家伙,它在 .git
目录下,叫 index
,就会赶紧地把这个文件的时间戳、文件长度等属性给记录下来(不记录文件内容的哦),这样就形成了一棵包含文件索引的目录树,随着被放入的文件越来越多,这棵index目录树就会越长越大。那么当一个文件被修改过之后,它的属性时间戳、文件长度就有可能会发生改变。这样一来,当我们再输入 git status
的时候,index这棵大树就会将目录树中所有文件的时间戳和文件长度和工作区的进行比较,如果发现不一样,那么就能判定不一样的文件经过了修改。那么,细细一想,这样做有什么好处?当然是速度快呀!比较时间戳和文件长度肯定要去比较文件内容要快得多啊!所以,git在监控文件是否修改这一块,我服!
数据存储原理
四种对象
- blob
- tree
- commit
- tag(目前没用到,暂时忽略)
用这幅图简单说明一下blob、tree和Commit这两个对象之间的关系。粗略一看,可以大致感觉出blob类似于文件,而tree类似于文件夹,而commit则是囊括这一大堆东西的一个对象。没错,其实从广义上来说就是这样的。我们可以先有这么一个基本的概念。
SHA-1算法
SHA-1算法具体怎么计算,可以自行google。这里只说一下该算法在git数据存储中的应用。我们可以用git提供的命令来计算一个文件的SHA-1值:
echo 'test content' | git hash-object --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
输入字符串 "test content",git会通过SHA-1算法将其中的内容计算出一个40个字符的HASH值。
同样地,如果我们在工作区新增一个文件,然后通过 git add
加入到暂存区,那么git同样地会根据这个文件通过SHA-1算法来计算出属于该文件的一个HASH值。
两者结合
经过前边简单讲解了两个相关的概念,接下来我们再来看git是如何做到数据管理存储的。
当我们在工作区新增一个文件,然后将其加入到暂存区,那么这个时候,git就会通过SHA-1算法计算出一个属于这个文件的HASH值,注意:
这个值是唯一的!
这个值是唯一的!
这个值是唯一的!
重要的事情说三遍,那么这句话意味着什么?
试想一下,如果一个文件经过修改之后,再次计算得到的HASH值会不会和原来的一样?答案是肯定不会的,所以这个HASH值就可以充当一个标识符的作用。
好,接下来再结合着那三个重要的对象来考虑一下:
对于blob对象,仔细观察上一副图可以发现,blob对象的头顶都有着5个字符,"5b1b3","911e7","cba0a",那么这些莫名的字符串是什么意思?难道是来自东方的一段神秘密语?啊,那肯定不是。其实这一段字符串就是所属文件的HASH值的一部分,那么它为什么会出现在blob的头顶呢?其实,要回答这个问题,很简单。其实当把一个新的文件放入到暂存区之后,git就在自己的目录下 .git/object/
新建了一个文件对象,来保存新加入的文件,与此同时通过SHA-1算法来计算得到一个所属该对象的"标识",而这个对象,就是blob对象。(感觉说的有点拗口)。大致过程如下图:
那么接下来当我把这个文件提交之后,会发生什么呢?
提交测试没错,又多出了两个hash值,即代表了两个对象。同时考虑到上边那个关系图可以大致猜测到,提交之后,又生成了两个对象,一个tree对象,一个commit对象。不过如果继续带着好奇心深入下去,尝试着去打开其中的一个文件,会发现里边是一堆16进制的数字,即使用Winhex打开,也是乱码。这是因为git对原来文本中的数据进行了重新压缩编码,这样既可以保存原来的文本内容,又减少了对空间的占用。
git中的指针是啥?
提到指针,那就肯定要稍微提一下C、C++中的指针了。这种指针的作用很简单,就是存储一个变量的地址,然后根据这个地址来找到该对象,进而对其进行操作。
那么,git中的指针也是这样的吗?其实,也差不多。只不过这里存储的并不是变量的在内存上的地址,而是另外一种形式的"地址",这就是HASH值。没错,就是上边计算出来的那个HASH值,commit对象的HASH值。江信江疑吗?那我们来验证一下:
commit提交在上副图中,我对Doge.txt文件又进行了修改,然后又放入到暂存区中,接着进行commit提交操作,然后输入命令 git log
来查看所有提交的日志记录,我们可以看到有一排黄色的字符串,写着 commit cbfa20cb4fc205477237d3ffc88909f7cb49bd6f
,这就是我们次此提交之后生成的commit对象的HASH值。那么,指针在哪里呢?不急,接下来看下边这幅图:
这个文件是master文件,在 .git/refs/
目录下。为什么要看这个文件呢?我们在用git的时候,经常会看到master这个单词,它是git中默认的一个分支,简单点说这个master文件就是一个指针,这个指针记录着一个commit提交对象的HASH值,通过这个HASH值我们就可以找到本次提交的tree对象、blob对象,这样也就找到了我们提交的文本信息了。更多有关分支的相关知识大家可以自行google。
那么,又有一个问题:我们知道git有一个版本回退的功能,那么这个是怎么实现的呢?
回顾一下我们刚才走过的路,我们知道一次提交就对应着个commit对象,一个commit对象还带着一个唯一标识的HASH值,那么按照这样的逻辑来推理的话,两次提交就有会两个commit对象,两个不同的HASH值。如果要版本回退的话,是不是我们只要指定一下回退到第几个版本,然后获得相应的commit对象就可以做到了?先来简单地试验一下吧:
现在版本 两次提交图中有两行数据,同时在本地版本库中也有两个版本
版本回退 回退之后的文本上边是一个简单的版本回退示意图,通过输入命令 git reset 版本id
来回退到指定的版本。其实一个最简单的版本回退就是如此。其实在内部,就是将带有HASH值的HEAD指针从最新的一个提交对象上转移到上一个提交对象上。
对于版本回退,还有许多其他的用法,包括reset的一些参数使用,checkout、revert命令的使用。有关它们的具体用法请戳这个传送门:
代码回滚:Reset、Checkout、Revert的选择
总结
这篇文章重点不是介绍git的命令用法,而是对其内部原理的一个简单分析。我写的比较白话,而且一直感觉有些地方不够完善,详略不太得当,但也没考虑好怎么去修改、完善。希望自己在以后的不断回看中,能够逐渐完善。
参考文章
Git 工作区、暂存区和版本库
使用原理视角看 Git
《Pro Git》的笔记-git内部原理
Git 用起来 の 基本原理