git 是一种程序员几乎天天都会用到的工具,给咱们代码管理带去了极大的方便。以往的 git 介绍,可能是介绍git 的高级命令,如git rebse
、git cherry-picker
、git bisect
等,少有看到剖析git 内部原理的。缘由也很简单,即便对 git 的原理不甚了解,也并不会影响咱们熟练使用 git。可是不少事咱们不光要知其然,更要知其因此然,方能触类旁通。php
在介绍 git 的原理以前,先介绍几个基本概念和会用到的命令,以便你们在阅读文章时能更加轻松地理解。git
git add
,对应工做区(Working Directory)。git add
后所处的状态, 对应暂存区(Stage)。git commite
后所处的状态,对应版本库(Commit History)。git cat-file
:查看 git 对象的瑞士军刀,能转译二进制文件为可读文件。git hash-object -w filename
:查看到 git 已存储的数据hexdump -C filename
:查看二进制文件的十六进制编码当你在一个新目录或已有目录内执行 git init
时,git 会建立一个 .git 目录,几乎全部 git 存储和操做的内容都位于该目录下。记得在刚接触 git 时,由于对 stage
、branch
等概念不够了解,还作出过备份整个项目的蠢事。其实若是真要作备份,备份当前的 .git 文件便可。程序员
下图为刚初始化的 .git 的目录结构(不一样版本的 git 会有所不一样),几乎全部的 git 操做和存储都位于该目录下。本次真要介绍四个核心文件或目录:HEAD 及 index 文件,objects 及 refs 目录。算法
简而言之,git 从核心上来看不过是简单地存储键值对(key-value)。它容许插入任意类型的内容,并会返回一个键值,经过该键值能够在任什么时候候再取出该内容。shell
先抛出一个问题:一个空的文件夹是否能添加到 git 项目中?不少人可能都知道答案,可是为何呐?经过对 git add
背后原理的解析,相信你能够得出一个确切的答案。bash
$ mkdir alpha && cd alpha
$ git init
$ mkdir data
复制代码
经过执行上面的命令,咱们建立了一个名为 alpha
的文件夹,而且将其初始化为一个 git 项目,再新建一个 data
空文件夹。经过执行 git add data
命令但愿把 data
空文件夹添加到暂存区中,发现执行后并无发生任何变化,执行 git status
查看仓库的状态,抛出这样一段提示:ide
$ 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/...
文件。ui
以前说过,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 -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
下的三个文件。
提交命令其实包含了三个步骤:
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 对象,最后一行是提交信息。
HEAD
文件中:ref: refs/heads/master
。HEAD 指向 master,master 就是咱们当前分支。由于是第一个提交,表明master引用的文件还不存在。git 会自动建立 .git/refs/heads/master
,并写入提交对象的哈希值:b83b66096fb5b5225ec0c5741f45d334ceb94046
。最后,全部 BLOB 文件所存储的信息都经过 tree 对象串联了起来,就比如 key-value
的形式。
再来回答下开头提出的问题吧,一个空的文件夹是否能添加到 git 项目中? 答:不能够。由于 git 使用的索引机制,是以文件为最小单位存储内容,跟踪变化的。 那么怎么作才可使这个文件夹存在呐?一般的作法是在里面新建一个名为 .gitkeep
的文件。