习惯于文件系统和基于目录的VCS(比如CVS)的人,可能以为git中版本的也是基于目录的,其实上git的版本管理基本上和项目目录(工作目录)没有什么关系,而整个git仓库的所有信息都是保存毫不起眼.git目录
今天虫虫就带领大家来通过探究.git目录来揭示git的底层原理。
概述
前面说了git所有的版本和变化都是保存在.git目录(默认是不可见)下的,工作目录只保存了你工作环境和未提交的文件变化,如果你的所有变化均已经提交了的话,那工作目录中除了.git目录下所有都是可以删掉的。
为了性能、空间、安全和完整性上的考虑,git对.git文件夹做了优化和算法处理,除了个别文件外,我都无法直接无法浏览其内容。目标文件均以guid命名,并且数据是zlib压缩的。但结构和组织是有记录的并且是可以理解的。
简单总结一下,.git文件夹可以分为五类:
objects-(蓝色)这些代表文件和更改。对象可以进一步分为提交、树和 blob类型。
refs引用文件-(红色)这些是组织对象的人类可读文件
logs日志-(绿色)这些用于快速生成显示给用户的日志。
config配置-(浅灰色)有用于配置git行为的文件,可以手动修改。
Temp-(灰色)git在命令行操作之间需要保存的信息的临时文件。
基本git操作
初始化
在任何git项目创建时候,首先要做的就是初始化,即git init:
执行该操作后,会开启git版本管理,即建立一个.git的目录,其结构如下:
可以看到初始化创建创建了一堆文件和文件夹。具体为:
config文件是一个文本文件,其中包含当前存储库的git配置。存储库的一些基本设置,如作者、文件模式等,后续添加的git add remote添加的要关联远程地址等都在该配置文件中,这是.git目录下可以直接手动修改的文件之一。
HEAD包含存储库的当前头。根据设置的“默认”分支指向,其值可能为 refs/heads/master或者refs/heads/main或其他。其中refs/heads可以在下面看到的文件夹,并进入一个名为的文件master目前还不存在。这个文件 master仅在第一次提交后才会显示。
hooks包含可以在git执行任何操作之前/之后运行的任何脚本的示例文件,可以根据需要修改,这是.git目录下唯二可以手动修改的文件(还有一个是config文件)。
objects包含git对象,即存储库中有关文件、提交等的数据,这是git版本管理的底层目录,也是其系统精妙设计的地方。
Refs为存储引用(指针)。refs/heads包含指向分支的指针和refs/tags包含指向标签的指针。
添加变化
为了展示.git系统的变化,我们给工作目录添加一个文件。
echo 'hello git!' >hello
可以看到.git目录并没有任何变化,然后让我们添加该文件到git
git add hellonew file mode 100644index 0000000..d1c6469--- /dev/null+++ b/hello@@ -0,0 +1 @@+hello git!
该操作下,.git目录下有如下变化:
index文件内容变更了,该文件主要存储有关当前暂存内容的信息,变化为将新add的hello文件添加到索引中。
另外再objects下添加了一个新文件夹objects/d1和一个名为
objects文件内容
objects目录下的文件是git存储内容的文件,我们用file工具查看其性质:
file ./.git/objects/d1/c64694584cf480b01273f2c729fd8b6b7c320c./.git/objects/d1/c64694584cf480b01273f2c729fd8b6b7c320c: zlib compressed data
File提示该文件是一个zlib 压缩数据,但是该数据是什么呢?
用zlib-flate工具试试
zlib-flate -uncompress blob 11hello git!
可以看到,其为一个包含名为的文件的类型、大小和内容信息,我们git add添加信息为一个blob类型,大小为11,其内容为尺寸的9内容是hello git!。
objects文件的名解析
实际上objects目录下git对象的文件名都是,对其内容的sha1哈希值。可以对其数据sha1sum,就得到了文件名
zlib-flate -uncompress d1c64694584cf480b01273f2c729fd8b6b7c320c -
git取要写入的内容的sha1哈希取前两个字符为名字创建一个文件夹,然后使用其余哈希部分作为文件名。Git之所以要取哈希德前两个字符创建文件夹,是为了确保单个objects文件夹下不会保存太多的文件。
git cat-file
事实上,git也提供了一个底层的工具来显示git对象的内容,而无需用zlib-flate那么麻烦的解压缩。我们可以使用git cat-file
其参数-t显示类型,-s求大小和-p显示内容。
git cat-file -t d1c64694584cf480b01273f2c729fd8b6b7c320cblobgit cat-file -s d1c64694584cf480b01273f2c729fd8b6b7c320c11git cat-file -p d1c64694584cf480b01273f2c729fd8b6b7c320chello git!
commit提交
add实际上只是将变化添加到了git暂存,要真正提交git版本,则需要commit提交。
git commit -m 'hello'[master (root-commit) 868d295] hello1 file changed, 1 insertion(+)create mode 100644 hello
该操作下.git目录发生的变化:
看来,变化不少,我们一个一个来看下。首先是,新增加了一个COMMIT_EDITMSG文件。和其文件名暗示的一样是,是它包含(最后的)提交消息。
还新增加了一个全新的文件夹logs。这是git记录存储库中所有提交更改的一种方式。所有refs和HEAD头的commit的变化都在此处显示。
objects目录也也增加了两个对象文件(及其对应目录)有变化。
来查看refs目录:
cat ./.git/refs/heads/master868d2956b01a865bd12ec9e3b0af12ae4decb446
应该,是一个对象名称(哈希),用来指向一个git对象(objects目录新建对象之一)。用git cat-file看看他的属性:
git cat-file -t 868d2956b01a865bd12ec9e3b0af12ae4decb446
commit
git cat-file -p 868d2956b01a865bd12ec9e3b0af12ae4decb446tree f0856ed7bdf85cdfae83207f5d18b3435f4e720cauthor chongchong 1696776332 +0800committer chongchong 1696776332 +0800hello
实际上可以直接,这样查看:
git cat-file -t refs/heads/master
信息显示,这是一个新的git对象,其类型为了commit,其内容指向一个tree对象f0856ed7bdf85cdfae83207f5d18b3435f4e720c,恰恰是此次objects目录创建的另一个git对象。
然后是作者,提交时间,提交的消息(hello)等信息。
查看一下这个tree对象:
git cat-file -t f0856ed7bdf85cdfae83207f5d18b3435f4e720ctreegit cat-file -p f0856ed7bdf85cdfae83207f5d18b3435f4e720c100644 blob d1c64694584cf480b01273f2c729fd8b6b7c320c hello
对象类型为tree,其内容为指向我们之前创建的hello文件的blob对象。
这是更成熟的仓库的树的样子。
继续变更
下面来修改下hello文件内容,看看.git目录如何变更。
echo 'by chongchong' > hellogit commit -am '第二次提交'1 file changed, 1 insertion(+), 1 deletion(-)
-am在commit中直接包含了add操作,将两个操作并为一个。该操作后.git目录的变化如下:
新添加了3个git对象。其中之一是blob文件包新内容的对象,其中一个是tree对象,和一个commit对象。来查看refs/heads/master
git cat-file -p refs/heads/mastertree 94667246335fd2296936c4b1c0a53c53dee0cebfparent 868d2956b01a865bd12ec9e3b0af12ae4decb446author chongchong 1696777676 +0800committer chongchong 1696777676 +0800第二次提交git cat-file -p 94667246335fd2296936c4b1c0a53c53dee0cebf100644 blob 37dabb2d48b7c7144b48d97a2a7b231660b01d62 hellogit cat-file -p 37dabb2d48b7c7144b48d97a2a7b231660b01d62by chongchong
另外,commit对象中,多了一行parent键,指向第一次的commit对象的链接,表示,这个commit是在上一个提交之上创建的。
创建分支
我们再来创建一个分支,使用git branch new
该操作在refs/heads文件夹下添加一个以分支为文件名的文件(new),内容作为最新commit的ID值。
cat ./.git/refs/heads/new88a163162bd276257c2a20cff25c017274871ca1
所以,git创建分支在git下是一个非常轻量级的东西,仅仅是一个指向commit ID的指针,这是git和SVN最大区别之一,以及git为啥如此高效的原因。
创建标签(tag)行为和创建分支一样,也是在refs/tags创建一个指向commit的文件指针。
最后,还在logs目录下新添加了一个文件存储提交历史数据的目录类似于master分支。
checkout分支
tree提交的对象并更新工作树中的文件以匹配其中记录的状态。在这种情况下,由于要从 master到new,两者都指向同一个commit和底层tree目录,git在工作树基本无任何变化。
git checkout new
在checkout发生的唯一变化的是.git/HEAD文件现在将指向new。
cat .git/HEADref: refs/heads/new
我们在这提交一个变化,以用来后续的合并操作。
echo 'the third addition'>>newgit add newgit commit -m "第三次提交-分支提交"
合并操作
主要有3种合并方式。
最简单也是最容易的就是快进合并。在这种情况下,只需更新一个分支指向另一个分支指向的提交。只需将哈希复制到refs/heads/new到 refs/heads/master。
第二种是变基合并。在这种情况下,首先将变化应用到main当前一次指向一个提交的内容之上,然后执行类似于快进合并的操作。
第三种使用单独的合并提交来合并两个分支。这有点不同,因为它有两个parent其提交对象中的条目。
首先让看看合并之前的图示:
git log --graph --oneline -all
现在执行合并:
git merge newUpdating 88a1631..88e5367Fast-forwardnew | 1 +1 file changed, 1 insertion(+)create mode 100644 new
截止目前我们都是在本地进行的操作,实际git最常用的操作是push推送到远程仓库中和其他人协作。
为了展示这一点,首先我们在gitee平台上创建一个新的gitee项目作为远程库。
回到仓库目录,添加远程库地址
git remote add origin git@giteeDOTcom:ijz/hellogit.git
添加后,会在仓库的.git/config目录添加如下信息和远程库进行关联。
顺便说一句,和前面提到一样,我们手动修改.git/config文件进行地址和远程库名称修改(比如修改origin为gitee)。
git push origin master
由于我们还没有添加ssh的私钥,所以此处会报如下错误:
ssh-keygen生成密钥对,将公钥~/.ssh/id_rsa.pub,添加到gitee的个人公钥中
然后,通过
ssh -T git@giteeDOTcom
测试,显示如下说明密钥添加成功。
git push origin master
在该步骤中,git目录变化如下:
在refs和log目录下分辨添加了remotes目录用来保存指向远程库的commitID,用来本地和网络间的同步。
总结
本文我们通过探究git项目.git目录下的各种git操作后导致的变化追踪git底层的运作方式,可以更深入的了解git底层的架构和运行原理。
git底层最重要就是文件内容的sha1加密快照和以其哈希为文件名,以及各种指针引用原理,希望一点带面本文能帮助大家了解git基本的原理。
领取专属 10元无门槛券
私享最新 技术干货