Git 实现原理,你真的了解吗?
共 4659字,需浏览 10分钟
·
2021-01-25 14:12
诞生的原因
最初版本的git 诞生于Linus和他的团队在开发Linux内核的过程中。一开始,Linus和他的团队选用的版本控制工具是BitKeeper(一款非开源,但是有条件免费的产品)。但后来由于种种原因BitKeeper宣布终止免费,当时市面上也没有能满足Linux内核开发所需要的分布式版本控制系统,Linus为了解决这一问题,闭关一周,独自设计编写了初版git。
名字含义
Linus本人对git这一名字的解读是愚蠢的内容追踪器(the stupid content tracker),git在俚语中表示的是愚蠢这一类的贬义词。比较受人欢迎的解释应该是全局信息追踪器(Global Information Tracker)。
Git实现原理
初始化一个git仓库,此时文件夹中只有一个隐藏文件夹 .git
, .git
文件夹为git的版本库,存放git实现版本控制所需要的全部信息。使用 find .git
命令查看 .git
中存储的内容如下:
下文将围绕上图中的文件目录展开介绍(由于仓库初始化,index文件,COMMIT_EDITMSG文件以及logs文件夹等没有在上图中显示)。
config
config文件记录项目的配置,如下图:
更改配置时,也可以直接对config文件中的内容进行修改。如果某个git仓库中的config文件配置与全局配置冲突,以该仓库配置文件为准。例:
objects(对象库)
对象库(objects)中包含该仓库的原始文件数据。
可寻址内容名称
Git对象库被组织及实现成一个内容寻址的存储系统。具体而言,对象库中的每个对象都有一个唯一的名称,这个名称是向对象的内容应用SHA1得到的SHA1散列值(一个对象的完整内容决定了这个散列值)。
实例展示:
初始时,仓库中有已提交的 file01.txt
以及 file02.txt
文件:
根据类型来分,对象库中的数据可以分为以下三类:
块(blob)
git仓库中的每个文件的每一个版本表示为一个块。每个块被视为一个黑盒。一个块保存一个文件的数据,但不包含任何关于这个文件的元数据,甚至连文件名也没有。如下图:
目录树(tree)
一个目录树对象代表一层目录信息。它记录blob标识符、路径名和在一个目录里所有文件的一些元数据。它也可以递归引用其他目录树或子树对象,从而建立一个包含文件和子目录的完整层次结构。如下图:
提交(commit)
一个提交对象保存版本库中每一次变化的元数据,包括作者、提交者、提交日期和日志消息。每一个提交对象指向一个目录树对象。如下图:
上图展示的例子中,提交为master分支的首次提交,对于非首次提交,commit对象还会记录parent信息,即本次commit所对应的上一次提交。三者关系对应图示如下:
git追踪内容
当Git放置一个文件到对象库中的时候,它基于文件内容计算散列值而不是文件名。事实上,Git并不追踪那些与文件次相关的文件名或者目录名。如果两个文件的内容完全一样,无论是否在相同的目录,Git在对象库里只保存一份blob形式的内容副本。Git仅根据文件内容来计算每一个文件的散列码,如果文件有相同的SHA1值,它们的内容就是相同的,然后将这个blob对象放到对象库里,并以SHA1值作为索引。项目中的这两个文件,不管它们在用户的目录结构中处于什么位置,都使用相同的对象指代其内容。
实例展示:
初始时,仓库中只有一个已提交的 file01.txt
文件:
上述过程的图示变化如下:
如果这些文件中的一个发生了变化,Git会为它计算一个新的SHA1值,识别出它现在是一个不同的blob对象,然后把这个新的blob加到对象库里。原来的blob在对象库里保持不变,给没有变化的文件使用。其次,当文件从一个版本变到下一个版本的时候,Git的内部数据库有效地存储每个文件的每个版本,而不是它们的差异。因为Git使用一个文件的全部内容的散列值作为文件名,所以它必须对每个文件的完整副本进行操作。
存储机制
Git使用了一种叫做打包文件(packfile)的存储机制。当版本库中有太多的松散对象(Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式),或者你手动执行 git gc
命令,以及你向远程服务器push时,Git 都会将这些松散对象打包成一个称为“包文件(packfile)”的二进制文件。要创建一个打包文件,Git首先定位内容非常相似的全部文件,然后为它们之一存储整个内容。之后计算相似文件之间的差异并且只存储差异。因为Git是由文件内容驱动的,所以它并不关心计算出来的两个文件之间的差异是否属于同一个文件的两个版本。也就是说,Git可以在版本库里的任何地方取出两个文件并计算差异,只要它认为它们足够相似来产生良好的数据压缩。(Git有一套相当复杂的算法来定位和匹配版本库中内容相似的文件)
实例展示:
初始时,仓库中只有一个已提交的大小为12215字节的 file01.txt
文件:
在打包前,objects/pack和objects/info文件夹为空,文件以松散对象的形式存储在objects目录下。打包后,文件全部打包到 objects/pack 中。pack后缀文件存储对象文件,idx后缀文件是索引文件,用于允许它们被随机访问。objects/info 文件夹记录对象存储的附加信息,info/pack文件中记录打包文件的文件名(上图中为:P pack-c4750e76b36c227...idx)。
index(索引)
了解索引之前先说一下git的三个工作区域以及git仓库中文件的三个状态。
三个工作区域
1. 版本库(repository)
即上文中提到的 .git
文件夹。
2. 工作目录(working directory)
工作区即当前分支所对应的文件目录。
3. 暂存区(staging area)
在工作目录与仓库之间还有一个暂存区。当我们在工作目录中对文件进行增删改等操作时,这些被修改的内容在 git add
命令之后,就会被添加到暂存区中。git commit
之后,将暂存区中的内容添加到版本库中(进入对象库中)。三个工作区域交互如下图:
文件分类
Git将所有文件分成3类:已追踪的、被忽略的以及未追踪的。
1. 已追踪的(Tracked)
已追踪的文件是指已经在版本库,或者是在暂存区中的文件。
2. 被忽略的(Ignored)
被忽略的文件是指在工作目录中出现,但git并不记录该文件(夹)的变动。被忽略的文件(夹)必须在版本库中被明确声明为不可见或被忽略,即 .gitignore
文件中声明的文件(夹)。.gitignore
文件的格式如下:
- 空行会被忽略,而以井号(#)开头的行可以用于注释。然而,如果#跟在其他文本后面,它就不表示注释了。
- 一个简单的字面置文件名匹配任何目录中的同名文件。
- 目录名由末尾的反斜线(/)标记。这能匹配同名的目录和子目录,但不匹配文件或符号链接。
- 包含shell通配符,如星号(*),这种模式可扩展为shell通配模式。正如标准shell通配符一样,因为不能跨目录匹配,所以一个星号只能匹配一个文件或目录名。
- 起始的感叹号(!)会对该行其余部分的模式进行取反。此外,被之前模式排除但被取反规则匹配的文件是要包含的。取反模式会覆盖低优先级的规则。
此外,Git允许在版本库中任何目录下有 .gitignore
文件,每个文件都只影响该目录及其所有子目录。.gitignore
的规则也是级联的:可以覆盖高层目录中的规则。为了解决带多个 .gitignore
目录的层次结构问题,也为了允许命令行对忽略文件列表的增编,Git按照下列从高到低的优先顺序对文件进行忽略:- 在命令行上指定的模式( git update-index
)
- 从与文件在相同目录的 `.gitignore` 文件中读取的模式
- 上层目录中指定的模式(最接近当前目录的上层目录的模式优先于更上层的目录的模式)
- 来自 `.git/info/exclude` 文件的模式
- 来自配置变量core.excludedfile指定的文件中的模式。
3. 未追踪的(Untracked)
未追踪的文件是指那些不在前两类中的文件。Git把工作目录下的所有文件当成一个集合,减去已追踪的文件和被忽略的文件,剩下的部分作为未追踪的文件。在工作目录下新建的文件也是未追踪文件。
索引用来定位暂存区以及版本库中的文件。当对工作目录下未追踪文件或新的修改执行 git add
时,这些内容会被以blob对象的形式存入对象库中,同时,index文件(二进制文件)记录这些blob对象与文件的对应关系。
实例展示:
初始时,仓库中只有一个新建未提交的 file01.txt
文件:
对文件作出修改并将修改添加到暂存区后,index文件变更对应关系:
HEAD、refs
HEAD文件记录当前工作目录所对应的git分支,refs文件夹记录分支以及tag(tag指向某一次commit,用来给开发分支做一个标记,以便后续回退)。
实例展示:
初始时,仓库中有已提交的 file01.txt
以及 file02.txt
文件:
切换分支并提交新的commit后:
在分别给两次commit打上tag后:
上述过程对应图示如下:
info
info目录下只有exclude文件,其作用与 .gitignore
功能类似。他们的区别在于 .gitignore
这个文件本身也是存储在对象库中,用来保存的是公共需要排除的文件;而exclude文件中设置的则是本地需要排除的文件,不会影响到其他人。
description
description文件中的内容用于GitWeb。GitWeb是Git提供的 CGI 脚本,让用户在web页面查看git内容。如果我们要启动 GitWeb,可用命令 git instaweb --httpd=webrick
。这个命令在本地启动了一个监听 1234 端口的 HTTP 服务器,并且自动打开了浏览器。想要关闭GitWeb,只需要在启动命令的末尾加上 --stop
。打开的浏览器页面如下:
上图中description字段对应的内容就是 .git/description
文件中记录的内容。
hooks
hooks目录下的文件如下:
Git 能在特定的动作发生时触发自定义脚本,这些脚本(钩子)都被存储在 hooks子目录中。如上图,这些示例文件的名字都是以.sample结尾,如果想启用这些钩子,得先移除sample后缀。把一个正确命名(不带扩展名)且可执行的文件放入hooks子目录中,即可激活该钩子脚本。这样一来,它就能被 Git 调用。
COMMIT_EDITMSG
COMMIT_EDITMSG文件记录本地最后一次commit对应的message。
logs
logs文件夹用来记录操作信息,它的目录如下:
HEAD文件记录所有分支上的操作信息:
refs/heads/branch-name(分支名称)文件记录对应分支上的操作信息: