Git探秘
Table of Contents
前言
TODO 为什么写?
适合哪些人看?
适合使用过Git,对Git有了一些了解,但是想进一步向底层探究Git怎么存储提交的内容、不同操作到底对存储的内容做了什么操作,以便在以后使用Git的过程中更得心应手、从容安心的那些人。
Git仓库内容
让我们在空目录下新建一个仓库,看一下Git都会生成什么内容。
$ git init . $ ls -F1 .git/ branches/ config description HEAD hooks/ info/ objects/ refs/
各目录/文件用途分别是:
description
仅在GitWeb程序中使用。config
本项目相关的配置。info
有一个全局排除文件,用来保存不希望放到.gitignore文件的忽略模式。hooks
保存客户/服务侧的hook脚本。objects
保存所有的数据内容。refs
保存指向提交对象(commit object)数据(分支,标签,远程仓库等)的指针。HEAD
指向当前检出的分支。
后面随着在仓库下工作,Git还会生成别的文件,例如 index
,Git用来保存暂存区信息。
Git对象和引用
Git是一个内容寻址的文件系统,意思是它的核心是一个简单的 键值对数据存储
。 你可以插入任意类型的信息到Git仓库,Git会为此数据返回一个唯一键,后续可以用这个唯一键来获取插入的数据。
Git里的对象有下面几种类型。这篇文章就是要探究在不同的操作下,这些对象会不会有什么改变,以及怎么改变。
Blob对象
用来存储保存在Git仓库里的每个文件的内容的对象类型。Blob本身没有文件名信息,文件名是在tree对象中记录的。
Tree对象
Tree对象类似文件系统的目录树,保存Git仓库中目录和文件的关系。每个tree对象包含一个或者多个项,每一项对应一个blob对象或者一个子tree,及其模式,类型和文件名。
Commit对象
Commit对象是Git仓库的一个快照,记录了当时的文件层次结构以及文件内容。
TODO Tag对象
TODO 引用
Git常用操作探秘
现在开始探究Git的一些常用操作,看看这些操作会给仓库内容带来什么改变。
Git暂存区
暂存区暂存了什么
在空目录下初始化Git仓库,然后添加一个内容为"hello staging area"的文件test1.txt,此时仓库状态如下:
$ git status 位于分支 master 尚无提交 未跟踪的文件: (使用 "git add <文件>..." 以包含要提交的内容) test1.txt 提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
然后我们执行 git add test1.txt
将这个文件添加到暂存区。再看.git目录,发现多了个 index
文件,
$ ls -F1 .git/ branches/ config description HEAD hooks/ index info/ objects/ refs/
并且, .git/objects
多了 一个 文件,其sha1哈希值为78d3b5bd4c0bd6a28fc3760c8d48021355e9334e。
$ find .git/objects/ .git/objects/ .git/objects/78 .git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e .git/objects/info .git/objects/pack
而这个文件的内容就是test1.txt的内容,可以用下面的方式确认:
$ git cat-file -p 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e hello staging area
index
文件的内容可以用 git ls-files
查看:
$ git ls-files -s 100644 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e 0 test1.txt
可以看到,index文件的内容就是上面我们添加的文件 test1.txt
以及模式,对应的blob对象等信息。
下面我们看一下取消文件暂存,index文件会怎么变化。
$ git rm --cached test1.txt rm 'test1.txt' $ git status 位于分支 master 尚无提交 未跟踪的文件: (使用 "git add <文件>..." 以包含要提交的内容) test1.txt 提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
看下 .git/objects
目录,
$ find .git/objects/ .git/objects/ .git/objects/78 .git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e .git/objects/info .git/objects/pack
git add
添加的文件并不会因为取消暂存而被删除,但index文件中的记录会删除(但此时index文件并不会被删除,文件长度也不是0),可以再执行 git ls-files -s
来验证。
还有一个需要的点,因为暂存区是下次要提交的仓库快照,所以它会包含仓库中git记录的所有文件,而不仅仅是上次提交之后通过 git add
添加到暂存区的文件。
删除暂存区文件会发生什么
如果 git add test1.txt
之后不提交,而是把index文件删除,会发生什么呢?
删除之前,我们看下仓库的状态。
$ git status
位于分支 master
尚无提交
要提交的变更: (使用 "git rm --cached <文件>..." 以取消暂存) 新文件: test1.txt
现在我们把 .git/index
文件删除,再次查看仓库的状态。
$ git status 位于分支 master 尚无提交 未跟踪的文件: (使用 "git add <文件>..." 以包含要提交的内容) test1.txt 提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
又变为刚创建 test1.txt
文件时的状态了。
Git提交
接着暂存区暂存了什么,继续执行 git commit
,把test1.txt提交到仓库里,看会发生什么。
$ git add test1.txt
$ git commit -m 'first commit - test1.txt'
[master(根提交) fe655fb] first commit - test1.txt
1 file changed, 1 insertion(+)
create mode 100644 test1.txt
此时, .git/objects
一共会有三个文件(会多出来两个文件):
fe655fb1d5de1e068a1dbcb05ca628fa6076f643 :commit: 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e :blob: 788d51e06017d18f7e13400805014bb62bf0fa4e :tree:
多出来的两个,一个是commit对象,另一个是commit对应的tree对象。查看tree对象的内容,看看是什么。
$ git cat-file -p 788d51e0 100644 blob 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e test1.txt
是test1.txt的信息,就是我们上面说过的模式、类型、哈希值以及文件名。
下面再添加一个子目录sub,并在其中创建一个文件test2.txt后提交看看仓库内容有什么变化。
$ mkdir sub $ echo "hello test2" > sub/test2.txt $ git add . $ find .git/objects/ .git/objects/ .git/objects/fe .git/objects/fe/655fb1d5de1e068a1dbcb05ca628fa6076f643 .git/objects/78 .git/objects/78/d3b5bd4c0bd6a28fc3760c8d48021355e9334e .git/objects/78/8d51e06017d18f7e13400805014bb62bf0fa4e .git/objects/info .git/objects/pack .git/objects/e6 .git/objects/e6/94444553266392e753e1fd4b168b3f04feb92a $ git commit -m 'second commit - sub/test2.txt' [master 6087737] second commit - sub/test2.txt 1 file changed, 1 insertion(+) create mode 100644 sub/test2.txt
此时, .git/objects
下有如下这些文件:
608773728b3c86b5ea7f41e551895c96e60b9b91 :commit: 33a5993d35e13e62cc430ab0c3f842147f1d17e6 :tree: fe655fb1d5de1e068a1dbcb05ca628fa6076f643 :commit: 51abbe01a27c194753550a3feadc49cac946d00a :tree: 78d3b5bd4c0bd6a28fc3760c8d48021355e9334e :blob: 788d51e06017d18f7e13400805014bb62bf0fa4e :tree: e694444553266392e753e1fd4b168b3f04feb92a :blob:
它们之间的指向关系如下:
* 60877372 :commit: ** 33a5993d :tree: *** 51abbe01 :tree: sub **** e6944445 :blob: test2.txt *** 78d3b5bd :blob: test1.txt ** fe655fb1 :commit: *** 788d51e0 :tree: **** 78d3b5bd :blob: test1.txt
层级关系对不同类型的对象来说含义不同。对于commit来说,内层commit是外层commit的父/祖先commit,例如 fe655fb
就是 60877372
的父commit;commit的下级tree表示此commit对应的tree对象;tree就好理解了,可以看成是目录树。