.git里面保存了一个git工程的全部要素。
本质上,git通过Blob、Tree、Commit、Tag,这四种基本对象(Object)工作。
举个例子,假设现在在someDir下执行以下命令
$ git init
$ echo "hello, 5xRuby" > index.html
$ mkdir config
$ touch config/database.yml
$ git add --all
$ git commit -m "init commit"
则会生成这样一个目录结构
(someDir)
├── config
│ └── database.yml
└── index.html
而对于此时.git/object,可以看到这样的结构
(someDir/.git/objects)
├── 25
│ └── 32115fab19c7ce70ff78a4929fb5df2f6131c4
├── 30
│ └── ab28d3acb37f96ad61ad8be82c8da46d0a7307
├── a6
│ └── 18ce33da8d21bca841f18e6432fcabf15d4477
├── 45
│ └── 493e229a1985f2ca7a6877c90301be298bcf0e
├── e6
│ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
├── info
└── pack
现在引入一个命令cat-file,可以分别结合-t或-p查看object的类型或内容,比如
$ git cat-file -t 45493e229a1985f2ca7a6877c90301be298bcf0e
commit
$ git cat-file -p 45493e229a1985f2ca7a6877c90301be298bcf0e
tree 2532115fab19c7ce70ff78a4929fb5df2f6131c4
author Ryan <saky571@163.com> 1682420828 +0800
committer Ryan <saky571@163.com> 1682420828 +0800
init commit
可以看到这个commit对象中保存了一个tree对象的SHA-1,进一步的,查看该tree的内容
$ git cat-file -p 2532115fab19c7ce70ff78a4929fb5df2f6131c4
040000 tree a618ce33da8d21bca841f18e6432fcabf15d4477 config
100644 blob 30ab28d3acb37f96ad61ad8be82c8da46d0a7307 index.html
写明了它包含一个blob对象(index.html)和另一个tree对象(config),此时先来看看这个blob对象的内容试试
$ git cat-file -p a618ce33da8d21bca841f18e6432fcabf15d4477
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 database.yml
$ git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
<empty file>
于是这里大胆猜测,使用index.html的SHA-1可以查出“hello, 5xRuby”
$ git cat-file -p 30ab28d3acb37f96ad61ad8be82c8da46d0a7307
hello, 5xRuby
所以总的来说,此时git内部的是这样的(commit的SHA1不同有误)
现在再回过头来分析具体发生了什么:
- 对于add到暂存区的每个文件,git会以其内容为输入,计算一个SHA1值,并且以这个值的后38个字母为文件名,前2个字母为文件夹名,存储在.git/objects中,这个文件的内容将会是add到暂存区的文件的内容的压缩版本,可以通过解读这个压缩内容,知道原本的文件内容
- 在commit之后,每次commit本身会作为一个object也会以类似的逻辑存储在objects中
- 在commit之后,涉及blob的文件夹也会作为一个节点(tree object),存储在objects中
以上就是一次commit的本质。假如此时修改了index.html,然后提交,则此时这个图可能是变成这样
其中master是指向一次commit的指针,本质上也是一个branch(commit的贴纸)。而HEAD则是指向branch的指针,表示了当前工作目录使用哪个分支。
如果再在项目根目录下新建一个README.md的话,则可能会变成下图这样。注意到README.md和database.yml都是空内容,所以其SHA-1校验值是一样的,且共用e69de2的object文件。
根据上图,假设HEAD目前不处于master分支上,然后执行了git checkout master,则git可能是这样根据上图重建工作区的:从4bf提交开始,顺着指针“提葡萄”。
至于一直没有提到的Tag,其本质和branch一样,也是一个指向commit的指针,相关内容见标签部分。
PS:即使改变一个文件的一个字母,当add这个新文件到暂存区的时候,也会基于整个文件的内容存储为一个新的blob,所以本质上git不是基于差异的版本控制
PS:git在切换分支(或者说版本回滚)的时候,也不对文件进行增删,而是直接顺着commit将当时的版本整个重现出来,这也说明了git本质上不是差异的备份。