git本质上是一个内容寻址的文件系统,并在此基础上提供了一个版本控制系统的界面。
一个已经初始化后的git目录结构如图:
其中我们需要重点关注以下四个文件:
1. HEAD:保存了当前被检出的分支
2. objects:存储所有的数据内容
3. index:存储暂存区信息
4. refs:存储指向commit对象的指针
1. Git对象
objects目录下存储了git中所有的数据对象,其目录结构如下:
objects 0d 7f012a2181ab54c95b4078674da7b31b580bff 1c c469fe42468071e2b1823650fb0a3206985281
objects目录中存储的对象拥有一个40位的id,其中前2位作为文件夹名,后38位作为文件名,文件内容即为该对象的值。git中拥有四种对象类型:blob,tree,commit和tag。
1.1 blob对象
blob对象存储了用户工作区域内的文件内容。当执行git add命令时,objects下会生成新的blob对象,保存了add命令的文件内容。
使用git cat-file命令可以从指定的对象中取得数据,加上-p选项
git构造一个对象时,首先添加一个header,header以对象类型开头。下面的例子时git构造了一个内容为字符串"blob",类型为blob的对象。header首先以blob开头,然后添加一个空格,随后是数据内容的长度,最后是一个空字节。因此一个blob对象包含一个blob header和原始数据内容,其形式如下:
>> header = "blob #{content.length}\0"
=> "blob 16\u0000"
Git会将上述header和原始数据内容为这个blob对象计算出SHA-1校验和。此SHA-1校验和就是前文所说的40位长度的对象id。然后git根据此id值确定此对象的存储位置(前2位作为文件夹,后38位作为文件名),使用zlib压缩此blob对象然后存储到相应位置。
commit对象,tree对象的文件header则以"commit"和"tree"开头。
1.2 tree对象
tree对象,顾名思义是一棵树,它的每个节点被称为tree entry。每个tree entry有一个指向blob对象或子树对象的SHA-1指针,以及相应的模式,类型,文件名信息等。例如,某个tree对象的信息如下:
每一行代表此tree对象的一个节点,即tree entry。第一列代表文件模式,其中040000表示这是一个目录,而100644表示这时一个普通文件。第二列表示此节点的对象类型。第三类是此节点的对象SHA-1值,通过此SHA-1值来定位数据内容,可以通过git cat-file -p <SHA-1>来查看。第四列则代表了文件名。因此,此tree对象的树形结构图如下:
tree对象同blob对象一样,拥有一个SHA-1值,并且通过SHA-1值来确定文件位置,并将添加了header的tree对象内容写入到objects目录下。
1.3 commit对象
一个commit对象保存了一次commit的信息,包括作者,提交注释,并且指向父commit对象和一个tree对象。这个tree对象是当前项目快照。如图所示:
2. 再谈git文件夹结构
2.1 index文件
index文件实际上就代表了git中暂存区。它存储了一个tree对象,表示当前项目的快照。当运行git add命令时,首先在objects目录下生成一个新的blob对象,然后更新index文件中的tree对象。
当运行git status命令时,git会对比工作区和暂存区的文件,具体流程如下:
1. 首先比较文件的时间戳和长度,如果相同则认为文件没有改变。
2. 如果发现时间戳不同,则比较文件内容,如果内容相同,则将工作区文件的时间戳更新到index文件(暂存区)中。
3. 如果工作区文件和暂存区文件内容不相同,则提示有changes
当运行git commit命令时,首先使用index文件中tree对象在objects目录下创建一个tree对象,然后在objects目录下创建一个commit对象,commit对象指向此tree对象以及父commit对象,最后更新.git/refs/heads/master(commit时的分支名)文件内容为此commit的SHA-1值。
2.2 refs文件夹
refs文件夹结构如下:
其中子文件夹heads目录为每一个分支创建了一个文件,文件中只保存了在该分支下最近一次提交的commit id。
打开master文件,内容只是一个commit的SHA-1值。
2.3 HEAD文件
HEAD文件指向当前活跃的分支。打开HEAD文件,文件内容如下:
可以看到文件内容是本地的某一个代表了feture2分支的文件,而该分支文件的内容是最近的一次commit id。因此,我们常用的命令诸如 git reset HEAD等命令中出现的HEAD,能够正确的定位到具体的commit。