Git 内部原理
从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面
Git 目录
当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的东西。
Git Object
Git 是一个内容寻址文件系统,这意味着,Git 的核心部分是一个简单的键值对数据库(key-value data store)。 可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的 key,通过该键可以在任意时刻再次取回该内容。
这个 key 一般为一个长度为 40 的 SHA-1 哈希值 —— 一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。
Git 存储内容的方式 —— 一个文件对应一条内容, 以该内容加上特定头部信息一起的 SHA-1 校验和为文件命名
校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。 最后,Git 将内容存经 zlib 压缩后,存入入对应地址
一旦将内容存储在了对象数据库中,那么可以通过 cat-file
命令从 Git 那里取回数据。
Git Object Type
Git Object 存在以下几种类型
blob
tree
commit
tag
Blob Object
数据对象(blob object),用于存储文件内容。 当 git 添加一个文件,例如 example_file.txt
时,git 会创建一个包含文件内容的 blob-object。
Tree Object
Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。
Git 使用 树对象(tree object),解决文件名保存,以及多个文件组织的问题。
一个 tree-object 包含了一条或多条树对象记录(tree entry), 每条记录含有一个指向 blob-object 或者 sub tree-object 的 SHA-1 指针,以及相应的模式、类型、文件名信息。
Commit Object
**提交对象(Commit Object)**包含目录树对象哈希,父提交哈希,作者,提交者,日期和消息。
Tag Object
标签对象(tag object) 非常类似于一个提交对象,它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。 主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用,永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
PS: 标签对象并非必须指向某个提交对象;可以对任意类型的 Git 对象打标签。
如何计算 SHA-1
假设某文件内容为 "what is up, doc?"
Git 引用
在 Git 中,使用一个文件来保存 SHA-1 值,而该文件有一个名字,这种名字被称为“引用(references,或简写为 refs)”。 这些文件被存储在 refs 文件目录中.
Git 分支的本质就是 Refs,例如,master 分支。
HEAD 引用
HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用,表示它是一个指向其他引用的指针。
在某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。 例如,merge 失败,解决冲突的过程中。
本质上,通过 HEAD 最终的得到的也是一个 git 对象。
标签引用
存在两种类型的标签:附注标签和轻量标签。
创建一个轻量标签:
这就是轻量标签的全部内容,一个固定的引用。
附注标签则更复杂一些。 若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。
远程引用
如果添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。 **远程引用(remote reference)**和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。
包文件
每次提交文件,Git 都会用一个全新的对象来存储新的文件内容,就算只是在一个 400 行的文件后面加入一行新内容。
如果 Git 只完整保存其中一个,再保存另一个对象与之前版本的差异内容,岂不更好?
Git 最初向磁盘中存储对象时所使用的格式被称为 **“松散(loose)”**对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为 “包文件(packfile)” 的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者手动执行 git gc 命令,或者向远程服务器执行推送时,Git 都会这样做。
Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。
打包对象之后原始的版本将以差异方式保存的,这是因为大部分情况下需要快速访问文件的最新版本。
引用规范
**引用规范(refspec)**的格式为 (+)<src>:<dst>
。 <src>
代表远程版本库中的引用; <dst>
是本地跟踪的远程引用的位置; +
号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。
<src>
和<dst>
均支持以表达式表达。
如果想让 Git 每次只拉取远程的 master 分支,而不是所有分支, 可以把(引用规范的)获取那一行修改为只引用该分支:
传输协议
Git 可以通过两种主要的方式在版本库之间传输数据: “dumb” 协议和 “smart” 协议。
Dumb 协议
如果正在架设一个基于 HTTP 协议的只读版本库,一般而言这种情况下使用的就是 Dumb 协议。 这个协议之所以被称为 “Dumb 协议”,是因为在传输过程中,服务端不需要有针对 Git 特有的代码; 抓取过程是一系列 HTTP 的 GET 请求,这种情况下,客户端可以推断出服务端 Git 仓库的布局。
Dumb 协议步骤:
获取服务器端 info/refs
基于 info/refs,检出对应的 ref,一般包含一个 Tree-Object 和一个 Blob-Object
获取对象信息,
如果服务器端存在,从服务器端获取;
否则,检查所有替代版本库。如果派生项目存在,从派生项目获取;
如果替代版本库找不到该对象,检查服务端有哪些可用的包文件,从包文件中查找对象信息
重复步骤 3,直到获取完所有所需的目录信息
Dumb 协议虽然很简单但效率略低,且它不能从客户端向服务端发送数据。
Smart 协议
智能协议是更常用的传送数据的方法,但它需要在服务端运行一个进程,而这也是 Git 的智能之处 —— 它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。 总共有两组进程用于传输数据,它们分别负责上传和下载数据。
上传数据,通过 SSH
为了上传数据至远端,Git 使用 send-pack
和 receive-pack
进程。
运行在客户端上的 send-pack
进程连接到远端运行的 receive-pack
进程。
Git 运行 send-pack
进程,通过 SSH 连接 Git 服务器,然后在服务端执行命令。
类似于:
上传数据,通过 HTTP
上传过程在 HTTP 上,与 SSH 几乎是相同的,除了握手阶段有一点小区别。 连接是从下面这个请求开始的:
接下来客户端发起另一个请求,这次是一个 POST 请求,这个请求中包含了 send-pack
提供的数据。
这个 POST 请求的内容是 send-pack
的输出和相应的包文件。 服务端在收到请求后相应地作出成功或失败的 HTTP 响应。
下载数据,通过 SSH
客户端启动 fetch-pack
进程,连接至远端的 upload-pack
进程,以协商后续传输的数据。
如果通过 SSH 使用抓取功能,fetch-pack 会像这样运行:
在 fetch-pack
连接后,upload-pack
会返回类似下面的内容:
这与 receive-pack
的响应很相似,但是这里还包含 HEAD
引用所指向内容(symref=HEAD:refs/heads/master)
, 这样如果客户端执行的是克隆,它就会知道要检出什么。
下载数据,通过 HTTP
抓取操作的握手需要两个 HTTP 请求。 第一个是向和 Dumb 协议中相同的端点发送 GET 请求:
这和通过 SSH 使用 git-upload-pack
是非常相似的,但是第二个数据交换则是一个单独的请求:
维护与数据恢复
有的时候,需要对仓库进行清理 —— 使它的结构变得更紧凑,或是对导入的仓库进行清理,或是恢复丢失的内容。
维护
Git 会不定时地自动运行一个叫做 auto gc
的命令。 如果有太多松散对象(不在包文件中的对象)或者太多包文件,Git 会运行一个完整的 git gc
命令。
这个命令会做以下事情:
收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。
打包引用到一个单独的文件
大约需要 7000 个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。 可以通过修改 gc.auto
与 gc.autopacklimit
的设置来改动这些数值。
如果更新了引用,Git 并不会修改这个文件,而是向 refs/heads
创建一个新的文件。 为了获得指定引用的正确 SHA-1 值,Git 会首先在 refs
目录中查找指定的引用,然后再到 packed-refs
文件中查找。
数据恢复
Git 会默默地记录每一次改变 HEAD 时它的值。 每一次提交或改变分支,引用日志都会被更新。 可以通过 git reflog
找回丢失的提交信息。
如果引用日志信息也丢失
可以通过 git fsck --full
显示出所有没有被其他对象指向的对象,从而找到丢失的 commit。
移除对象
git clone 会下载整个项目的历史,包括每一个文件的每一个版本。 然而,如果某个人在之前向项目添加了一个大小特别大的文件,即使将这个文件从项目中移除了,每次克隆还是都要强制的下载这个大文件。 之所以会产生这个问题,是因为这个文件在历史中是存在的,它会永远在那里。
所以,Git 项目执行 CI 时,可以考虑只获取最新的历史,gitlab ci 就提供了这样的功能。
环境变量
Git 总是在一个 bash shell 中运行,并借助一些 shell 环境变量来决定它的运行方式。
具体环境变量参考:Git 内部原理 - 环境变量
最后更新于