前言
git 是一种程序员几乎每天都会用到的工具,给我们代码管理带去了极大的方便。以往的 git 介绍,多是介绍git 的高级命令,如git rebse
、git cherry-picker
、git bisect
等,少有看到剖析git 内部原理的。原因也很简单,即使对 git 的原理不甚了解,也并不会影响我们熟练使用 git。但是很多事我们不光要知其然,更要知其所以然,方能举一反三。
概念
在介绍 git 的原理之前,先介绍几个基本概念和会用到的命令,以便大家在阅读文章时能更加轻松地理解。
- BLOB (binary large object):二进制大对象,是一个可以存储二进制文件的容器。
- three git status:
- Untracked:文件还未被
git add
,对应工作区(Working Directory)。 - Staged:
git add
后所处的状态, 对应暂存区(Stage)。 - Committed:
git commite
后所处的状态,对应版本库(Commit History)。
git cat-file
:查看 git 对象的瑞士军刀,能转译二进制文件为可读文件。git hash-object -w filename
:查看到 git 已存储的数据hexdump -C filename
:查看二进制文件的十六进制编码
.git 内幕
当你在一个新目录或已有目录内执行 git init
时,git 会创建一个 .git 目录,几乎所有 git 存储和操作的内容都位于该目录下。记得在刚接触 git 时,因为对 stage
、branch
等概念不够了解,还做出过备份整个项目的蠢事。其实如果真要做备份,备份当前的 .git 文件即可。
下图为刚初始化的 .git 的目录结构(不同版本的 git 会有所不同),几乎所有的 git 操作和存储都位于该目录下。本次真要介绍四个核心文件或目录:HEAD 及 index 文件,objects 及 refs 目录。
简而言之,git 从核心上来看不过是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。
git add
先抛出一个问题:一个空的文件夹是否能添加到 git 项目中?很多人可能都知道答案,但是为什么呐?通过对 git add
背后原理的解析,相信你可以得出一个确切的答案。
- 初始化
$ mkdir alpha && cd alpha
$ git init
$ mkdir data
复制代码
通过执行上面的命令,我们创建了一个名为 alpha
的文件夹,并且将其初始化为一个 git 项目,再新建一个 data
空文件夹。通过执行 git add data
命令希望把 data
空文件夹添加到暂存区中,发现执行后并没有发生任何变化,执行 git status
查看仓库的状态,抛出这样一段提示:
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
复制代码
如果在 data
文件夹内再新建一个空文件夹又会如何呐,发现还是没有任何变化。这样就得出了开头那个问题的答案,空的文件夹是无法添加到 git 项目中的。再查看一下 .git
目录,也没有发生任何变化。
- 添加文件夹到暂存区
$ echo 'a' > data/letter.txt
$ git status
Untracked files:
(use "git add <file>..." to include in what will be committed)
data/
nothing added to commit but untracked files present (use "git add" to track)
复制代码
在 data
目录下新建一个名为 letter.text
的文件,写入 a
。发现仓库的状态发生了变化,但是 .git
目录并没有发生变化,说明在工作区的操作并不会产生 git 历史记录。 执行 git add data
命令,发现 .git
目录终于发生变化了。 分别多了 index
文件和 objects/78/...
文件。
之前说过,git 的存储其实是以键值对的形式存在的。index
是一个列表,每一行维护一个文件名到 BLOB 哈希值的映射。objects
目录下存放的就是 value,这个BLOB文件包含了data/letter.txt
文件压缩后的内容,并以内容的哈希值作为文件名。哈希是一段算法,它将给定内容转换为更小的,且能唯一确定原内容的值。 我曾经尝试直接打开这些文件,但是打开都是一串乱码。可以通过十六进制编码的方式打开这些文件,但是毕竟不是机器,想要看明白还是有一些难度的🤣。
幸好 git 提供了一把瑞士军刀(git cat-file
)给我们使用,可以将数据内容取回。 打印出 data/letter.txt
中的内容为 a
。
$ git cat-file -p 78981922613b2afb6025042ff6bd878ac1994e85
a
复制代码
至于 git/index
中的内容,可以通过 git ls-file
查看。
$ git ls-file --stage
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 data/letter.txt
复制代码
正如之前所说的,index
文件存放的是一个列表,记录了哈希值对应的文件。7898
这个 BLOB 文件存储的是 data/letter.txt
中的内容。objects
目录下的结点是不可变的。这意味着它的内容可以编辑,但不能删除。添加的文件内容和创建的提交都保存在 objects
目录下。(注:git prune
删除所有不能被ref访问到的对象,执行此命令可能会丢失数据。)
git commit
$ git commit -m 'a1'
[master (root-commit) b83b660] a1
1 file changed, 1 insertion(+)
create mode 100644 data/letter.txt
$ git status
On branch master
nothing to commit, working tree clean
复制代码
执行 git commit
命令后,文件被保存到了版本库中,仓库处于 clean
的状态。再看看 .git
目录,发现多了 logs
目录和 objects
下的三个文件。
提交命令其实包含了三个步骤:
- 创建提交版本对应文件的 tree 对象 什么是 tree 对象,tree 对象可以存储文件名,同时也允许存储一组文件。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 BLOB 或子 tree 对象的哈希值。 创建新提交后,对应data目录的tree对象如下:记录了
data/letter.txt
文件的所有信息,我们可以使用这些信息来恢复data/letter.txt
文件。空格分隔的第一部分表示该文件的权限,表明是一个普通文件;第二部分表示该记录对应的是一个 BLOB 对象,第三部分是该BLOB 的哈希值,第四部分记录了文件名。
$ git cat-file -p e908cfc6e086c91a073c55a6882adebfc9c4520c
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 letter.txt
复制代码
用图示表式即为:data
目录对应的 tree 对象指向对应data/letter.txt
那么问题来了,data 并非根目录,文件的指向肯定是从根目录开始。
$ git cat-file -p master^{tree}
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
$ git cat-file -p 9745002f161a1be75bf65f869ab16743da2a6fda
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
复制代码
master^{tree}
表示 master (当前)分支上最新提交指向的 tree 对象。从给出的结果中可以看到根目录(root)指向了 data
目录,图示如下:
- 创建一个提交对象 我们在提交时输入了
commit message
等信息,这些信息自然也存放在objects
目录下:
$ git cat-file -p b83b66096fb5b5225ec0c5741f45d334ceb94046
tree 9745002f161a1be75bf65f869ab16743da2a6fda
author Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
committer Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
a1
复制代码
第一行指向一个tree对象。通过这里的哈希值,我们可以找到对应工作区根目录(即 alpha 目录)的 tree 对象,最后一行是提交信息。
- 将当前分支指向新提交 到了这里还存在最后一个问题,分支。 git 怎么知道现在的提交应该指向哪个分支?答案在
HEAD
文件中:ref: refs/heads/master
。HEAD 指向 master,master 就是我们当前分支。因为是第一个提交,代表master引用的文件还不存在。git 会自动创建.git/refs/heads/master
,并写入提交对象的哈希值:b83b66096fb5b5225ec0c5741f45d334ceb94046
。
最后,所有 BLOB 文件所存储的信息都通过 tree 对象串联了起来,就好比 key-value
的形式。
再来回答下开头提出的问题吧,一个空的文件夹是否能添加到 git 项目中? 答:不可以。因为 git 使用的索引机制,是以文件为最小单位存储内容,跟踪变化的。 那么怎么做才可以使这个文件夹存在呐?通常的做法是在里面新建一个名为 .gitkeep
的文件。