vlambda博客
学习文章列表

5分钟学习git内部原理

每天与你分享 

IT编程开发 技术干货 架构方案 技术思维导图 设计模式 算法题库

正文内容

GIT内部原理

每天5分钟学习一项技能,让我们先来看个大概的视图:

5分钟学习git内部原理

本文以一个具体简单的例子结合动图的方式介绍了Git的内部原理,包括Git是什么储存我们的代码和变更历史的、更改一个文件的时候时Git内部是怎么变化的、Git这样实现的有什么好处等等问题展开。
通过上面的动态图解释,让大家了解Git的内部原理。如果你已经可以看懂这张动态图了,下面的内容可能对你来说会比较基础简单了。

前言聊一聊

近年技术发展迅速,更新快,让部分小伙伴养成了一种学习知识停留在表面、基础上,只会调用一些简单常用的指令。我们经常有一种“我会用、就是会了”的错觉,等到真正实际工作遇到问题,才发现有待深入学习。
有时候了解明白一些底层的东西,可以更好的帮你梳理思路,知道真正要做什么,抓住关键点,这样就避免迷失在Git大量的指令和参数上面。

了解Git是储存信息的过程

下面会用一个显而易见的例子让大伙感受一下git是怎么储存信息的。

首先我们先创建两个文件

$ git init$ echo 'codebox111' > file1.txt(a.txt)$ echo 'codebox222' > file2.txt(b.txt)$ git add *.txt


这时候,Git会将整个数据库储存在.git/目录下,如果你此时去查询.git/objects目录,你会看到仓库里面多了两个object。

$ tree .git/objects.git/objects├── 58│ └── c9bdf9d017fcdaasdasdas8c073cbfcbb7ff240d6c├── c2│ └── 00906efd24ec5e783bsdaee7f23b5d7c941b0c12c├── info└── pack


我们来看一下里面究竟存的是什么玩意

$ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6cxKOR0a044K%

一串乱码是什么鬼?这是因为Git将信息压缩成二进制文件了。但是不用害怕,因为Git也提供了一个能够帮助你探索它的api命令 git cat-file [-t] [-p], -t可以查看object的类型,-p可以查看object储存的具体内容:

$ git cat-file -t 5xsadsa$ git cat-file -p 5sdadasd
可以发现这个object是一个blob类型的节点,他的内容是111,也就是说这个object储存着file1.txt(a.txt)文件的内容。
这里我们遇到第一种Git object,blob类型,它只储存的是一个文件的内容,不包括文件名等其他信息。然后将这些信息经过SHA1哈希算法得到对应的哈希值 58c9bdf9xxsdasda8dc8c073cbfcbdasd40d6c,作为这个object在Git仓库中的唯一身份证。
也就是说,我们此时的Git仓库是这样子的:

5分钟学习git内部原理


我们继续看一下,我们创建一个commit。
$ git commit -am '[+] init'$ tree .git/objects.git/objects├── 0c│ └── 96bfcsadasddddxxxasd18f46c7e47ab2├── 4c│ └── aaaxasdasdasdf0716ddd5b209...

我们会看到当commit完成之后,Git仓库里面多出来两个object。同样使用 cat-file 命令,看看它们分别是什么类型以及具体的内容是什么:
$ git cat-file -t 4cxxree$ git cat-file -p 4caaa1100644 blob 58c9bdf9d017fcd178dc8c0... file1.txt(a.txt)100644 blob c200906efd24ec5e783bee7... file2.txt(b.txt)

我们遇到了第2种Git object类型:tree,它把当前的目录结构打了一个快照。从它储存的内容来看是可以发现它储存了一个目录结构(类似于文件夹),以及每一个文件(或者子文件夹)的权限、类型、对应的身份证(SHA1值)、以及文件名。
此刻的Git仓库的样子是:

5分钟学习git内部原理


$ git cat-file -t 0c9xxfcommit$ git cat-file -p 0cxasd1ftree 4caaa1a9asadadad367xef071616e5b209author lsan 李3 123331343 +0800committer lsi 李4 133312102343 +0800[+] init

然后我们发现了第2种Git object类型——commit,它储存的是一个提交的信息,包括对应目录结构的快照tree的哈希值,上一个提交的哈希值(这里由于是第一个提交,所以没有父节点。在一个merge提交中还会出现多个父节点),提交的作者以及提交的具体时间,最后是该提交的信息。

此时我们去看Git仓库是这样的:

5分钟学习git内部原理

到这里我们就知道Git是怎么储存一个提交的信息的了,那有同学就会问,我们平常接触的分支信息储存在哪里呢?

$ cat .git/HEADref: refs/heads/master$ cat .git/refs/heads/mastedsadas3331c7e47ab2


在Git仓库里面,HEAD、分支、普通的Tag可以简单的理解成是一个指针,指向对应commit的SHA1值。

5分钟学习git内部原理


其实还有第4种Git object,类型是tag,在添加含附注的tag( git tag -a )的时候会新建,这里不详细介绍说明了,有兴趣的朋友按照上文中的方法可以自己去深入探究。
至时我们知道了Git是什么储存一个文件的内容、目录结构、commit信息和分支的。 本质就 是一个key-value的数据库加上默克尔树形成的有向无环图(DAG)

看看Git的三个分区

来看一下Git的三个分区分别是(工作目录、Index 索引区域、Git仓库),以及Git变更记录是怎么形成的。我们来了解一下这三个分区和Git链的内部原理之后可以对Git的众多指令有一个理解了。
接着上面的例子,目前的仓库状态如图所示:

5分钟学习git内部原理

这里有三个区域,所储存的信息分别有3点:
  • 第一点:工作目录 ( working directory ):操作系统上的文件,所有代码开发编辑都在这上面完成。
  • 第二点:索引( index or staging area ):可以理解为一个暂存区域,这里面的代码会在下一次commit被提交到Git仓库。
  • 第三点:Git仓库( git repository ):由Git object记录着每一次提交的快照,以及链式结构记录的提交变更历史。
我们来看一下更新一个文件的内容这个过程是怎么的。

5分钟学习git内部原理

运行echo "333" > file1.txt将file1.txt(a.txt)的内容从111修改成333,此时如上图可以看到,此时索引区域和git仓库没有任何变化。

5分钟学习git内部原理

执行git add file1.txt把file1.txt(a.txt)加入到索引区域,如图所示,git在仓库里新建一个blob object,储存了新的文件内容和更新了索引将flie1.txt指向了新建的blob object。

运行git commit -m 'update'提交这次修改。如上图所示

  1. 第一:Git先根据当前的索引生产一个tree object,当是新提交的一个快照。

  2. 第二:创建一个新的commit object,把这次commit的信息储存起来,parent指向上一个commit,组成一条链记录变更历史。

  3. 第三:把master分支的指针移到新的commit结点。

至此知道了Git的三个分区和作用、历史链的建立原由。Git的大部分指令都是在操作这三个分区和这条链。

git相关操作命令复习一下

  • Git提交记住用户名和密码

    • https://www.jianshu.com/p/6c539d1956d5

  • 创建版本库

    • git clone <url>
      克隆远程版本库

    • git init
      初始化本地版本库

  • 远程仓库管理

    • git remote
      查看远程仓库

    • git remote remove 远程库名字
      git删除远程库

    • git remote add origin http://XXX.git
      git添加远程库

    • git remote rename origin mypython
      修改远程库名称

  • 文件操作

    • git rm  文件名
      删除文件

    • git mv config.php ./inc/config.php
      对文件移动或改名

  • 创建和提交

    • git checkout -b dev
      创建并切换到分支从已有的分支创建新的分支(如从master分支),创建一个dev分支

    • git push origin dev
      提交该分支到远程仓库

    • git branch -d <BranchName>
      删除本地分支

    • git branch -a | -r
      查看项目的分支们(包括本地和远程) -r 查看远程的分支

    • git status
      查看状态

    • git diff
      查看变更内容

    • git add .
      跟踪所有改动过的文件

    • git add <file>
      跟踪指定的文件

    • git mv <old><new>
      文件改名

    • git rm --cached <file>
      删除文件

    • git commit -m "commit message"
      停止跟踪文件但是不删除

    • git commit --amend
      修改最后一次提交

  • 查看提交历史

    • git reflog
      查看版本变化

    • git log
      查看提交历史

    • git log --pretty=oneline
      查看提交历史-单行显示

    • git log -p <file>
      查看指定文件的提交历史

    • git blame <file>
      以列表方式查看指定文件的提交历史

  • 撤销

    • git reset --hard HEAD
      撤销工作目录中所有未提交文件的修改内容

    • git checkout HEAD <file>
      撤销指定的未提交文件的修改内容

    • git revert <commit>
      撤销指定的提交

  • 分支和标签

    • git branch
      显示所有本地分支

    • git checkout <branch/tag>
      切换到指定分支或标签

    • git branch <new-branch>
      创建新分支

    • git branch -d <branch>
      删除本地分支

    • git tag
      列出所有本地标签

    • git tag <tagname>
      基于最新提交创建标签

    • git tag -d <ragname>
      删除标签(本地)

    • git push origin :refs/tags/v1.0.0
      删除github远端的指定tag

    • git push origin --tags
      共享标签

  • 合并与衍合

    • git merge <branch>
      合并指定分支到当前分支

    • git rebase <branch>
      衍合指定分支到当前分支

  • 解决合并的冲突

    • 先git status 查看一下冲突的文件

    • 然后修改内容add+commit提交

    • 最好git push 并删除分支

  • tag标签

    • git tag -a t1 -m "标签"
      添加一个t1的标签

    • git show t1

    • git push origin t1
      推送到远程

  • 版本回退

    •  git reset --hard f0bc0ab5a32a6780b913a74ae9761c0ce1680ef0
      回退到指定的版本号-可以用git log查看

    • git push -f -u origin master
      强制提交到master分支(具体哪个分支请酌情修改)

  • 远程操作

    • git remote -v
      查看远程版本信息

    • git remote show <remote>
      查看指定远程版本信息

    • git remote add <remote> <url>
      添加远程版本库

    • git fetch <remote>
      从远程库获取代码

    • git pull <remote> <branch>
      下载代码及快速合并

    • git push <remote> <branch>
      上传代码及快速合并

    • git pull <remote> :<branch/tag-name>
      删除远程分支或标签

    • git push --tags
      上传所有标签

  • 初始配置


一些有趣的问题

问题1:为什么要把文件的权限和文件名储存在tree object里面而不是blob object呢?

想象一下修改一个文件的命名。
如果将文件名保存在blob里面,那么Git只能多复制一份原始内容形成一个新的blob object。而Git的实现方法只需要创建一个新的tree object将对应的文件名更改成新的即可,原本的blob object可以复用,节约了空间。

问题2:每次commit,Git储存的是全新的文件快照还是储存文件的变更部分?

由上面的例子我们可以看到,Git储存的是全新的文件快照,而不是文件的变更记录。也就是说,就算你只是在文件中添加一行,Git也会新建一个全新的blob object。那这样子是不是很浪费空间呢?
这其实是Git在空间和时间上的一个取舍,思考一下你要checkout一个commit,或对比两个commit之间的差异。如果Git储存的是问卷的变更部分,那么为了拿到一个commit的内容,Git都只能从第一个commit开始,然后一直计算变更,直到目标commit,这会花费很长时间。而相反,Git采用的储存全新文件快照的方法能使这个操作变得很快,直接从快照里面拿取内容就行了。
当然,在涉及网络传输或者Git仓库真的体积很大的时候,Git会有垃圾回收机制gc,不仅会清除无用的object,还会把已有的相似object打包压缩。

问题3:Git怎么保证历史记录不可篡改?

通过SHA1哈希算法和哈希树来保证。假设你偷偷修改了历史变更记录上一个文件的内容,那么这个问卷的blob object的SHA1哈希值就变了,与之相关的tree object的SHA1也需要改变。又因为Git是分布式系统,所有人都有一份完整历史的Git仓库,因此所有人都能很快发现存在问题,啊哈哈。

推荐阅读