本文以一个具体例子结合动图介绍了Git的内部原理,包括Git是什么储存咱们的代码和变动历史的、更改一个文件时,Git内部是怎么变化的、Git这样实现的有什么好处等等。html
经过例子解释清楚上面这张动图,让你们了解Git的内部原理。若是你已经可以看懂这张图了,下面的内容可能对你来讲会比较基础。前端
本文是2019/11/24在深圳腾讯大厦2楼多功能厅举办的FCC前端分享会(freeCodeConf 2019 深圳站)上分享的文字版。git
视频:www.bilibili.com/video/av772…github
近几年技术发展十分迅猛,让部分同窗养成了一种学习知识停留在表面,只会调用一些指令的习惯。咱们时常有一种“我会用这个技术、这个框架”的错觉,等到真正遇到问题,才发现事情没有那么简单。shell
后来我开始沉下心,回归一开始接触编程的时候,那时候学习一个知识都会深刻一点去思考背后的原理。但这并非说掌握并会使用高级Api不重要,他们也很是重要,而且是平常工做中大部分时间都在使用的,快速掌握它们意味着高效学习,能够快速的应用在开发生产上。数据库
只是有时候知道一些底层的东西,能够更好的帮你理清思路,知道你真正在操做什么,不会迷失在Git大量的指令和参数上面。编程
这里会用一个简单的例子让你们直观感觉一下git是怎么储存信息的。api
首先咱们先建立两个文件bash
$ git init
$ echo '111' > a.txt
$ echo '222' > b.txt
$ git add *.txt
复制代码
Git会将整个数据库储存在.git/
目录下,若是你此时去查看.git/objects
目录,你会发现仓库里面多了两个object。
$ tree .git/objects
.git/objects
├── 58
│ └── c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
├── c2
│ └── 00906efd24ec5e783bee7f23b5d7c941b0c12c
├── info
└── pack
复制代码
好奇的咱们来看一下里面存的是什么东西
$ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
xKOR0a044K%
复制代码
怎么是一串乱码?这是由于Git将信息压缩成二进制文件。可是不用担忧,由于Git也提供了一个可以帮助你探索它的api git cat-file [-t] [-p]
, -t
能够查看object的类型,-p
能够查看object储存的具体内容。
$ git cat-file -t 58c9
blob
$ git cat-file -p 58c9
111
复制代码
能够发现这个object是一个blob类型的节点,他的内容是111,也就是说这个object储存着a.txt文件的内容。
这里咱们遇到第一种Git object,blob类型,它只储存的是一个文件的内容,不包括文件名等其余信息。而后将这些信息通过SHA1哈希算法获得对应的哈希值 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c,做为这个object在Git仓库中的惟一身份证。
也就是说,咱们此时的Git仓库是这样子的:
咱们继续探索,咱们建立一个commit。
$ git commit -am '[+] init'
$ tree .git/objects
.git/objects
├── 0c
│ └── 96bfc59d0f02317d002ebbf8318f46c7e47ab2
├── 4c
│ └── aaa1a9ae0b274fba9e3675f9ef071616e5b209
...
复制代码
咱们会发现当咱们commit完成以后,Git仓库里面多出来两个object。一样使用cat-file
命令,咱们看看它们分别是什么类型以及具体的内容是什么。
$ git cat-file -t 4caaa1
tree
$ git cat-file -p 4caaa1
100644 blob 58c9bdf9d017fcd178dc8c0... a.txt
100644 blob c200906efd24ec5e783bee7... b.txt
复制代码
这里咱们遇到了第二种Git object类型——tree,它将当前的目录结构打了一个快照。从它储存的内容来看能够发现它储存了一个目录结构(相似于文件夹),以及每个文件(或者子文件夹)的权限、类型、对应的身份证(SHA1值)、以及文件名。
此时的Git仓库是这样的:
$ git cat-file -t 0c96bf
commit
$ git cat-file -p 0c96bf
tree 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
author lzane 李泽帆 1573302343 +0800
committer lzane 李泽帆 1573302343 +0800
[+] init
复制代码
接着咱们发现了第三种Git object类型——commit,它储存的是一个提交的信息,包括对应目录结构的快照tree的哈希值,上一个提交的哈希值(这里因为是第一个提交,因此没有父节点。在一个merge提交中还会出现多个父节点),提交的做者以及提交的具体时间,最后是该提交的信息。
此时咱们去看Git仓库是这样的:
到这里咱们就知道Git是怎么储存一个提交的信息的了,那有同窗就会问,咱们日常接触的分支信息储存在哪里呢?
$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
0c96bfc59d0f02317d002ebbf8318f46c7e47ab2
复制代码
在Git仓库里面,HEAD、分支、普通的Tag能够简单的理解成是一个指针,指向对应commit的SHA1值。
其实还有第四种Git object,类型是tag,在添加含附注的tag(git tag -a
)的时候会新建,这里不详细介绍,有兴趣的朋友按照上文中的方法能够深刻探究。
至此咱们知道了Git是什么储存一个文件的内容、目录结构、commit信息和分支的。其本质上是一个key-value的数据库加上默克尔树造成的有向无环图(DAG)。这里能够蹭一下区块链的热度,区块链的数据结构也使用了默克尔树。
接下来咱们来看一下Git的三个分区(工做目录、Index 索引区域、Git仓库),以及Git变动记录是怎么造成的。了解这三个分区和Git链的内部原理以后能够对Git的众多指令有一个“可视化”的理解,不会再常常搞混。
接着上面的例子,目前的仓库状态以下:
这里有三个区域,他们所储存的信息分别是:
咱们来看一下更新一个文件的内容这个过程会发生什么事。
运行echo "333" > a.txt
将a.txt的内容从111修改为333,此时如上图能够看到,此时索引区域和git仓库没有任何变化。
运行git add a.txt
将a.txt加入到索引区域,此时如上图所示,git在仓库里面新建了一个blob object,储存了新的文件内容。而且更新了索引将a.txt指向了新建的blob object。
运行git commit -m 'update'
提交此次修改。如上图所示
至此咱们知道了Git的三个分区分别是什么以及他们的做用,以及历史链是怎么被创建起来的。**基本上Git的大部分指令就是在操做这三个分区以及这条链。能够尝试的思考一下git的各类命令,试一下你能不可以在上图将它们“可视化”**出来,这个很重要,建议尝试一下。
若是不能很好的将平常使用的指令“可视化”出来,推荐阅读 图解Git
有兴趣的同窗能够继续阅读,这部分不是文章的主要内容
想象一下修改一个文件的命名。
若是将文件名保存在blob里面,那么Git只能多复制一份原始内容造成一个新的blob object。而Git的实现方法只须要建立一个新的tree object将对应的文件名更改为新的便可,本来的blob object能够复用,节约了空间。
由上面的例子咱们能够看到,Git储存的是全新的文件快照,而不是文件的变动记录。也就是说,就算你只是在文件中添加一行,Git也会新建一个全新的blob object。那这样子是否是很浪费空间呢?
这实际上是Git在空间和时间上的一个取舍,思考一下你要checkout一个commit,或对比两个commit之间的差别。若是Git储存的是问卷的变动部分,那么为了拿到一个commit的内容,Git都只能从第一个commit开始,而后一直计算变动,直到目标commit,这会花费很长时间。而相反,Git采用的储存全新文件快照的方法能使这个操做变得很快,直接从快照里面拿取内容就好了。
固然,在涉及网络传输或者Git仓库真的体积很大的时候,Git会有垃圾回收机制gc,不只会清除无用的object,还会把已有的类似object打包压缩。
经过SHA1哈希算法和哈系树来保证。假设你偷偷修改了历史变动记录上一个文件的内容,那么这个问卷的blob object的SHA1哈希值就变了,与之相关的tree object的SHA1也须要改变,commit的SHA1也要变,这个commit以后的全部commit SHA1值也要跟着改变。又因为Git是分布式系统,即全部人都有一份完整历史的Git仓库,因此全部人都能很轻松的发现存在问题。
但愿你们读完有所收获,下一篇文章会写一些我平常工做中以为比较实用的Git技巧、常常被问到的问题、以及发生一些事故时的处理方法。
若是你喜欢个人文章,请关注我和个人博客,谢谢。