git对象
当我们使用git进行版本管理时,git会将我们的文件和目录结构转化成git方便操作的数据(git对象),然后再对这些git对象进行管理,从而实现版本管理的目的,这些git对象存放在git的对象库中。
我们眼中的文件会被git转化成"块"(blob)
我们眼中的目录会被git转化成"树"(tree)
我们眼中的状态会被git转化成"提交"(commit)
blob、tree、commit都是git对象,是三种不同类型的git对象
一个blob就是由一个文件转换而来,blob对象中只会存储文件的数据,而不会存储文件的元数据。
一个tree就是由一个目录转化而来,tree对象中只会存储一层目录的信息,它只存储它的直接文件和直接子目录的信息,但是子目录中的内容它并不会保存。
一个commit就是一个我们所创建的提交,它指向了一个tree,这个tree保存了某一时刻项目根目录中的直接文件信息和直接目录信息,也就是说,这个tree会指向直接文件的blob对象,并且指向直接子目录的tree对象,子目录的tree对象又指向了子目录中直接文件的blob,以及子目录的直接子目录的tree,依此类推。
每个git对象都有一个哈希码,这个哈希码通过SHA1算法得出,如果git对象的内容相同,那么他们的哈希码就是相同的,如果git对象的内容不同,那么他们的哈希码必定不同(理论上来说,即使内容不同,也有可能产生相同的哈希码,不过几率非常之小,我们可以忽略这种可能性),一个git对象的哈希码通常长成如下模样:
875925683e755d94e26a2dc1a1bc4c645a91acbe
它是一个40位的十六进制数。
通常情况下,我们只会使用哈希码的前几位来表示一个提交,只要这个哈希码的前几位与别的哈希码的前几位不同,能体现出唯一性,我们就能用这个哈希码的前几位来表示这个提交,比如,刚才示例的哈希码如下
875925683e755d94e26a2dc1a1bc4c645a91acbe
我们可以使用8759256来表示这个哈希码。
例:
下图中的圆形代表commit(即前文中的"小圆球"),三角形代表tree(由目录转化成的git对象),长方形代表blob(由文件转化成的git对象)。假设,第一次提交之前,目录中只有两个文件,file1和file2,file1的内容为f1,file2的内容为f2,那么当第一个提交创建以后,git的对象库中会存在如下图的git对象
也就是说 ,当我们创建第一个提交以后,项目当时的状态已经被转化成了上图中的git对象,我们创建的第一个提交的哈希为8759256,它指向一个tree,这个tree就是当时根目录的状态,这个tree的哈希为e890df4,从上图可以看出,当时的根目录中只有两个文件,也就是两个blob,这个tree指向了这两个blob,这两个blob就是由file1和file2转化而来的。
如果此时,我们修改了file2,我们将file2的内容从f2修改成f22,并且在根目录中创建一个新的子目录dir1,在dir1中又添加了一个文件d1file1,d1file1的内容是df1,但是我们并没有对file1进行任何修改,那么,当我们再次提交以后,git对象库中会存在如下对象。
如上图所示,我们修改了file2,将其内容从f2修改成了f22,当第二个提交创建以后,git会将file2的新状态转化成一个新的blob对象,file2之前的状态对应的blob对象仍然保存在git对象库中,并且被初始提交引用,以便我们随时能够通过初始提交找到file2当时的状态,file2新的状态被新的提交引用,我们并没有修改file1,也就是说,file1的状态一直没有发生改变,所以,新的提交只是通过tree对象指向了之前file1对应的blob,由于我们在根目录中创建了一个子目录dir1,所以,在新的根目录的tree对象中,也包含了它的直接子目录信息,并且指向了新子目录对应的tree对象,子目录tree对象中又保存了自己目录中的信息,也就是d1file1文件对应的blob对象。
也就是说,在两个commit中(在整个项目的两个状态中)file1的状态是相同的,于是git并没有对file1重复的创建blob,而是通过引用的方式,指向了file1对应的blob,即两个副本复用了同一个file1的状态,所以,当我们使用git进行版本管理时,只会牺牲最小的磁盘空间,来实现版本管理。
git add 与git commit的实际过程
当我们执行"git add"命令以后,工作目录中文件的状态就已经转换成blob对象了,当我们使用"git commit"命令以后,才会创建出commit对象。
上图描述了第一次提交产生以后,各个区域的状态,从上图可以看出,工作区有两个文件,file1和file2,文件内容分别为f1和f2,在对象库区域中,第一次提交对应的commit对象(圆形)已经指向了对应的tree对象(三角形),tree对象又指向了直接子目录的blob对象,而此时,索引的结构与对象库其实是一样的,索引也指向了file1和file2对应的blob,或者说,索引中记录的file1和file2的哈希就是上图中那两个blob的哈希。
当file2的内容变化以后,工作区file2文件的状态已经发生了改变,工作区中file2的新状态已经与索引区和对象库中的状态不一致了,索引和对象库中指向的仍然是file2的内容为f2时的状态(即上图中哈希为9de77c1的blob),这时,file2的新状态对应的blob对象还没有生成,file2的新状态只存在于工作目录中
"git add"命令会做两件事,第一件要做的事就是为file2的新状态创建blob对象,也就是上图中浅灰色曲线所表示的步骤,当新的blob对象(即上图中哈希为a8319的blob对象)创建完成后,"git add"命令就会做第二件事,即更新索引,将索引中的file2指向新创建的blob对象,即上图中橘黄色曲线所表示的步骤,橘黄色的区线表示索引中的file2原来指向9de77c1,当"git add"命令执行后,索引中的file2指向了a8319f8。
提交命令执行后,会进行如下图中的操作
当执行提交命令以后,git会根据索引中的结构,在对象库中创建出对应的tree对象,也就是上图中灰色曲线所表示的步骤,之后,git会创建一个commit对象,并且将新创建的commit对象指向刚才新创建的tree,于是,一个新的提交产生了,它记录了一个状态,我们可以随时通过这个提交回到对应的状态,而且这个时候,索引的结构和最新的提交所对应的结构是一致的。
如上图所示,这个新创建的提交也会指向前一个提交,每个提交都会指向自己的父提交。
git哈希值获取对象信息
每个git对象都有一个身份证号,也就是其对应的哈希值,只要我们能够获取到git对象的哈希值,就能通过哈希值获取到git对象的一些信息,比如,通过哈希值判断git对象的类型,或者通过哈希值来查看git对象的内容。
首先,通过git log找到我们最近创建的提交,如下:
$ git log --oneline
136146b (HEAD -> master) File 1 and file 2 have been modified
0e33636 add file1 and file2
从上述信息可以看出,最近的提交的哈希值为136146b,我们可以借助"git cat-file"命令,通过哈希值判断哈希对应的git对象类型,并且查看其内容,虽然我们已经知道136146b 这个哈希值对应的是一个commit对象,但是当你只知道哈希值的时候,可以通过如下命令获取到哈希对应的对象类型:
$ git cat-file -t 136146b
commit
如上例所示,使用"git cat-file -t 哈希值"命令即可。
"git cat-file"命令的"-t"选项可以查看哈希对应的对象类型,而"git cat-file"命令的"-p"选项可以帮助我们查看git对象的相关内容。
比如,使用"git cat-file -p 哈希值"命令查看最近的提交对象的内容信息,示例如下:
$ git cat-file -p 136146b
tree 22bb0fd9a518ff8cd695d9e08c89029d55836ed2
parent 0e3363697eacbd43d7bc111b0a63c1cf1b6e4604
author zsythink <zsy@zsythink.net> 1558666805 +0800
committer zsythink <zsy@zsythink.net> 1558666805 +0800File 1 and file 2 have been modified
从上述返回信息可以发现,最新的commit对象指向了一个tree对象,这个tree对象的哈希是22bb0fd…,同时,这个commit对象还指向了一个叫做parent的东西,这个叫做parent的东西也有一串哈希,那么我们来看看,这个哈希到底对应的是个什么东西,如下:
$ git cat-file -t 0e3363697eacbd43d7bc111b0a63c1cf1b6e4604
commit
通过上述命令,我们看出这个哈希对应的其实是一个commit对象,那么我们再来看看这个对象的内容,如下:
$ git cat-file -p 0e33636
tree e890df4b61259ae013926f478db558ec0098e2d5
author zsythink <zsy@zsythink.net> 1558664921 +0800
committer zsythink <zsy@zsythink.net> 1558664921 +0800add file1 and file2
从上述信息可以发现,这个提交的注释信息是"add file1 and file2",原来这个commit对象就是我们之前创建的第一个提交,这时我突然想起来,之前说过,一般情况下,每个提交对象都会指向自己的父提交,当然,第一个提交没有父提交。
最新的提交中除了自己的父提交,还指向了一个tree对象,那么我们来看看这个tree对象中都有什么,如下:
$ git cat-file -p 22bb0fd
100644 blob 26cd2781d622faeb05993d00535f1bdd31080c28 file1
100644 blob a8319f8f9473c45a71ff86c4037a3b60a0bd1b1f file2
如上述信息所示,这两个blob对象都有自己的哈希值,这两个blob对象就是由file1文件和file2文件的状态转换而来的,你快通过"git cat-file"命令查看一下这两个git对象的内容吧,不正是f11和f22吗?
我们通过提交的哈希,层层剥离,一直找到file1和file2对应的blob的过程,其实与之前图示中的对象库部分的对象指向关系不谋而合。
还有一个常用命令,通过简短的哈希值获取到整个哈希值 git rev-parse commitid
$ git rev-parse 13614
136146b8f1278019470291ec3c4cedc11075b31e
通过git目录来了解过程
$ git init test1
Initialized empty Git repository in D:/workspace/git/test1/.git/
$ cd test1
如上所示,我创建了一个新的测试repo,并且进入了仓库目录,不过这时候仓库目录空空如也,我们先来创建一些测试数据,并且将它们保存为第一个提交吧,过程如下
$ echo f1 > file1
$ echo f2 > file2
$ git status
On branch masterNo commits yetUntracked files:
(use "git add <file>..." to include in what will be committed)file1
file2nothing added to commit but untracked files present (use "git add" to track)
现在,我们只将file1加入暂存区,如下:
$ git add file1
$ git status
On branch masterNo commits yetChanges to be committed:
(use "git rm --cached <file>..." to unstage)new file: file1Untracked files:
(use "git add <file>..." to include in what will be committed)file2
file1的状态其实已经被转化成git对象,存放在git的对象库中了,说到对象库,就能引出我们的".git"目录了,其实,git对象就存放在".git/objects/"目录中。先别急,我们先别跑的太快,先把第一个提交创建出来,如下:
$ git commit -m "commit 1"
[master (root-commit) 4b3dfc8] commit 1
1 file changed, 1 insertion(+)
create mode 100644 file1
提交变更后,使用"git status"命令,信息如下:
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)file2nothing added to commit but untracked files present (use "git add" to track)
可以看到,由于file2的状态并没有添加到暂存区,所以上次提交并没有操作file2的状态,它是红色的,它仍然未被跟踪,我们先不理会它,先来看看刚才创建的提交:
$ git log
commit 4b3dfc8acb902ae15e32b167b009cc03330f54b1 (HEAD -> master)
Author: zsythink <zsy@zsythink.net>
Date: Fri May 24 16:58:26 2019 +0800commit 1
第一个提交的哈希码是4b3dfc8acb902ae15e32b167b009cc03330f54b1 。
现在我们进入".git/objects/目录下"看看
$ find .git/objects/
.git/objects/
.git/objects/4b
.git/objects/4b/3dfc8acb902ae15e32b167b009cc03330f54b1
.git/objects/7e
.git/objects/7e/4cccb4a643b0d9cb6ac9263779147c0937d0c4
.git/objects/8e
.git/objects/8e/1e71d5ce34c01b6fe83bc5051545f2918c8c2b
.git/objects/info
.git/objects/pack
看看上述信息,是不是觉得有一个文件的文件路径跟刚才创建的提交的哈希码特别像,没错,.git/objects/4b/3dfc8acb902ae15e32b167b009cc03330f54b1这个文件其实就是刚才创建的提交所对应的git对象,那剩下的.git/objects/7e/4cccb4a643b0d9cb6ac9263779147c0937d0c4文件和.git/objects/8e/1e71d5ce34c01b6fe83bc5051545f2918c8c2b文件又是什么呢?使用"git cat-file"命令看看不就知道了(注意,git对象的哈希码的前两位以目录的形式存在,前两位以后的哈希码作为文件名)。
$ git cat-file -t 7e4cccb4a643b0d9cb6ac9263779147c0937d0c4
tree
$ git cat-file -t 8e1e71d5ce34c01b6fe83bc5051545f2918c8c2b
blob
从上述信息可以看出,这两个git对象分别一个tree对象和一个blob对象,通过git cat-file -p 层层剥离,发现正是刚才那个提交指向的tree以及tree指向的blob。
".git/objects/“目录中存放了git对象,那么之前所描述的"索引"信息,存放在哪里了呢?索引的信息其实存放在” .git/index"文件中,我们无法直接查看这个文件内容,如果想要查看这个文件中的索引信息,可以使用如下命令:
$ git ls-files -s
100644 8e1e71d5ce34c01b6fe83bc5051545f2918c8c2b 0 file1
上述信息就是当前索引中的信息,可以看到,目前只有一个文件file1被索引记录了。
现在 ,我们继续一些其他操作,然后再来查看这些信息。
操作如下:
$ mkdir dir1
$ echo d1f3 > dir1/file3
我们创建了一个目录,并且在其中创建了新文件file3,为了方便,此时一次性将当前目录的所有变更加入到暂存区
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)new file: dir1/file3
new file: file2
如上述信息所示,所有变更都已加入到暂存区,那么我们看看索引文件有没有更新
$ git ls-files -s
100644 c3b53d6ceb6ab66a0595442d70f30b33917adb18 0 dir1/file3
100644 8e1e71d5ce34c01b6fe83bc5051545f2918c8c2b 0 file1
100644 9de77c18733ab8009a956c25e28c85fe203a17d7 0 file2
可以看到,索引文件已经更新了,file2以及dir1/file3都已经存在于索引列表中了,那么对象库中肯定也已经生成了对应的blob对象了,注意,此时tree对象还没有生成在对象库中,之前说过,tree对象是在提交命令执行后才创建的,我们看看对象库中的文件,如下。
$ find .git/objects/
.git/objects/
.git/objects/4b
.git/objects/4b/3dfc8acb902ae15e32b167b009cc03330f54b1
.git/objects/7e
.git/objects/7e/4cccb4a643b0d9cb6ac9263779147c0937d0c4
.git/objects/8e
.git/objects/8e/1e71d5ce34c01b6fe83bc5051545f2918c8c2b
.git/objects/9d
.git/objects/9d/e77c18733ab8009a956c25e28c85fe203a17d7
.git/objects/c3
.git/objects/c3/b53d6ceb6ab66a0595442d70f30b33917adb18
.git/objects/info
.git/objects/pack
可以看出,对象库中的blob文件也已经生成了,现在我们要做的就是提交了
$ git commit -m "add dir1 and file2"
[master e327059] add dir1 and file2
2 files changed, 2 insertions(+)
create mode 100644 dir1/file3
create mode 100644 file2
再次查看对象库
$ find .git/objects/
.git/objects/
.git/objects/4b
.git/objects/4b/3dfc8acb902ae15e32b167b009cc03330f54b1
.git/objects/7e
.git/objects/7e/4cccb4a643b0d9cb6ac9263779147c0937d0c4
.git/objects/8e
.git/objects/8e/1e71d5ce34c01b6fe83bc5051545f2918c8c2b
.git/objects/9d
.git/objects/9d/e77c18733ab8009a956c25e28c85fe203a17d7
.git/objects/c3
.git/objects/c3/b53d6ceb6ab66a0595442d70f30b33917adb18
.git/objects/c3/fddd7a9ef2818fbb7197e83d546f81cc79c1b6
.git/objects/dd
.git/objects/dd/03f30d2cac901d3e52dbc19897c88cb269805d
.git/objects/e3
.git/objects/e3/27059a441af2e80afcb5d36d22ce46e08c58ff
.git/objects/info
.git/objects/pack
你会发现多出了几个对象,多出的对象分别是新创建的commit对象,根目录的tree对象,以及dir1对应的tree对象。
最后,这里记录一个小问题,如果你跟我一样,喜欢在windows的git bash中使用vim编辑文本文件,那么,在你每次使用git add命令可能都会出现类似如下warning
$ git add .
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
这是由于换行符冲突引起的报警,因为git bash默认使用vim作为文件编辑器,vim默认使用LF作为换行符,与linux中的换行符一致,它们都是用LF换行符,但是windows默认使用CRLF作为换行符,大多数程序员都会使用IDE或者文本编辑器来编辑文本,这些编辑器通常能够自动识别换行符,除了windows自带的记事本等文本编辑器,记事本只会使用CRLF作为换行符,由于我习惯使用vim编辑文本,所以文件中的换行符都是LF,当git检测到时,它会贴心的帮我装换一下,但是其实我并不是特别需要,因为我在bash中不会使用记事本编辑文件,所以,我们可以禁用自动转换的功能,使用如下设置,禁用自动转换换行符:
git config --global core.autocrlf false
当然,如果你的习惯就是使用CRLF换行符的编辑器,那么目前你是不会遇到上述问题的,所以你可以根据需要选择是否进行上述设置。
HEAD是什么
假设有现在这样一个仓库,现在有两个分支,master分支和test分支,从上图可以看出,目前我们处于黄色的提交,也就是test分支的"add D to m2"
如果我们想要切换回master分支,则可以使用前文中总结的如下命令
/d/workspace/git/test_repo1 (test)
$ git checkout master
Switched to branch 'master'/d/workspace/git/test_repo1 (master)
$
如上述信息所示,我们已经从test分支切换到了master分支。
假设,我们现在关闭git bash和工作目录,当我们下次再次进入工作目录并且打开git bash时,仍然会显示为当前处于master分支,因为我们上次关闭工作目录之前,已经切换到了master分支,当然,如果你之前处于test分支,那么当你再次打开工作空间,仍然会显示你处于test分支。
那么问题来了,git是怎么知道我们当前该处于哪个分支呢?
git其实就是靠HEAD知道我们该处于哪个分支的,你可以把HEAD理解成一个指针,HEAD指针通常会指向一个分支(或者说指向一个分支指针)
如上图所示,由于我们当前处于master分支,所以,HEAD这个指针指向了master分支指针,如果我们现在检出test分支,那么HEAD指针就会指向test指针,也就是说,当我们从master分支检出到test分支时,HEAD指针会由上图中的状态变成下图中的状态:
所以说,git只要找到HEAD,就能找到我们当前所处的分支(因为我们在切换分支时,会将HEAD指向所在的分支)。
我们可以直观的查看当前仓库的.git目录中的HEAD文件的内容,你会发现**其实.git/HEAD文件的内容就是HEAD指针所指向的分支,**如下所示:
/d/workspace/git/test_repo1 (master)
$ cat .git/HEAD
ref: refs/heads/master
从上述返回信息可以看出,当前HEAD指针指向了另一个文件,这个文件就是.git/refs/heads/master,那么我们顺藤摸瓜,看看.git/refs/heads/master这个文件的文件内容
/d/workspace/git/test_repo1 (master)
$ cat .git/refs/heads/master
7406a10efcc169bbab17827aeda189aa20376f7f
可以看到,这个文件的内容是一串哈希码,可以看到,这个文件的内容是一串哈希码,而这个哈希码正是master分支上最新的提交所对应的哈希码。
聪明如你,肯定已经看出来了,.git/HEAD文件和.git/refs/heads/master文件正是上图中的HEAD指针和master分支指针
为了证明我们的想法,我们切换几次分支, 看看.git/HEAD文件内容的变化
/d/workspace/git/test_repo1 (master)
$ git checkout test
Switched to branch 'test'/d/workspace/git/test_repo1 (test)
$ cat .git/HEAD
ref: refs/heads/test/d/workspace/git/test_repo1 (test)
$ git checkout master
Switched to branch 'master'/d/workspace/git/test_repo1 (master)
$ cat .git/HEAD
ref: refs/heads/master
嗯嗯,看来是没错了,跟我们想的一样,HEAD指针通常指向我们所在的分支
HEAD指针 --------> 分支指针 --------> 最新提交
也就是说,通常情况下,HEAD指针总是通过分支指针,间接的指向了当前分支的最新提交。
那么现在,我们来尝试在test分支上创建一个新的提交,看看HEAD指针和test分支指针会有哪些变化,操作如下:
/d/workspace/git/test_repo1 (test)
$ echo E >> m2/d/workspace/git/test_repo1 (test)
$ git add m2/d/workspace/git/test_repo1 (test)
$ git commit -m "add E to m2"
[test 35cff8c] add E to m2
1 file changed, 1 insertion(+)/d/workspace/git/test_repo1 (test)
$ cat .git/HEAD
ref: refs/heads/test/d/workspace/git/test_repo1 (test)
$ cat .git/refs/heads/test
35cff8cabb71d553ab1abceaf33fa5a046a17bdb
如上所示,我们在test分支上创建了一个新的提交35cff8c,然后查看了.git/HEAD,发现HEAD指针仍然指向了test分支指针,而test分支指针已经指向了最新创建的提交,也就是35cff8c,如下图所示:
所以说,通常情况下,HEAD指针总是指向了当前分支的最新提交(通过分支指针间接的指向)。
摘自朱双印大佬博客链接