git | 原理揭秘 - 实践

这篇简单说一下git的工作原理

当我们使用 git init 初始化一个git仓库时,会在当前目录下生成一个 .git 目录,如下

我们主要关注三个目录:

- object 目录 用来存放历史文件,目录文件和提交commit 文件

- refs 用来存储引用信息,比如分支头节点,远程分支remote的头节点,tag 节点等

-logs 用来存放指针ref的移动路径,我们在git在使用 git reflog 就是查看的这个文件

还需要关注HEAD文件,其中存储了全局HEAD指针当前所处的位置!

object目录

object 目录用来存放3种文件

blob历史版本文件: 其类型为 blob 即二进制大数据,我们通过git管理的文件都会被以blob的形式存储在object目录下

tree 目录文件: 对于每个目录都会生成一个tree文件,里面包含了当前目录所包含的所有tree和blob文件

commit 提交文件: 对于创建的每个提交commit,都会生成一个对应的 commit文件来记录版本信息

我们可以把object 理解为一个类似于Redis的key,value 数据库,其中的每个文件,不论是blob tree 还是commit,其保存的文件名称都来源于对其内容的hash。 如下:

所以对于未变化的版本信息,git通过这种方式可以直接复用。

我们举个例子来说明这几种文件,首先介绍2个命令

git cat-file -p 文件名称(hash) 用来查看object中文件的内容
git cat-file -t 文件名称(hash) 用来查看object中文件的类型

通过这两个命令,我们可以查看object目录下文件的类型和内容!

我们在空目录下创建 src目录/index.js文件,里面写一个简单的add函数,并且进行第一次 first commit 提交:

此时,object目录下新增的目录如下:

我们依次查看其中object文件的类型和内容, 其目录名称+内部文件名称 组合成为object文件的name ,如下:

我们查看6d736c5f0d73da2e5cc623ae133ef2e5402e0c1f这个文件

发现这个文件就是我们新增的index.js文件,其内容为我们写的add函数,文件类型为blob。其名称通过内容hash生成

我们继续查看8ff5c4886c7631d717e7d5c408bd317b1ca527a3这个文件

其类型为 tree 目录,其内容包含了src下的所有文件的名称,即 index.js对应的hash,其名称通过内容hash生成

我们继续看dfec7fa751f2978f5dd1d117a549dac2f89d9cb7

其类型为 tree 内容为 src对应的tree文件,可以看出这个是整个目录的tree,包含了src目录,其名称通过内容hash生成

最后再来看一下 d569fdab504b6146d25aae03bafd5932bd4f5c6f

其类型为 commit 内容包含了本次commit的提交信息,当前版本的目录 tree信息,以及提交时的时间戳。其名称通过内容hash生成

第一次提交后,object内部文件及其关系如下所示:

我们给项目生成一个 package.json文件,并且进行 second commit 

可以发现,增加了三个文件,如下:

其中,adfb03d9cd073afce4425d3b4d916934b8245b05对应的是package.json生成的blob文件

再来看5fdb0008012bb6acfe5fde8c89de7e85e584665d,其类型为tree,对应全局的目录,其中包含了src目录的tree文件和package.json对应的blob文件,如下

可以发现,由于本次的src目录没有发生任何变化,所以其对应的name也没有变化,第二次commit生成的全局tree可以直接复用src的tree文件。

最后就是 bbbef0c55841da0260ed9861a4c08dbc76cc057f 文件,其类型为 commit 包含了第二次提交的提交信息

其中,多了一个parent,其指向上一次 first commit提交的commit节点,其关系如图

我们继续 把index.js 拷贝到根目录,并且改名为add.js 进行third commit 

可以发现 2个文件被新增:

其中3a0b0138f7aa39b03080cffb29aa103930fb53a8 为tree文件,其内容包含了 add.js, src ,package.json 对应的blob和tree,但是由于add.js 和 index.js 的内容一致,所以这次提交不会新生成add.js的blob文件,而是直接复用index.js的 blob

cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 为commit文件,引用tree,parent指向second commit! 

三次更新后,object文件夹内文件关系如下

我们通过分离HEAD的方式,把HEAD移动到第一次提交的位置,并且创建新的分支 test

此时分支结构如下:

我们把master分支的package.json文件直接拷贝过来,并且使用和master第二次提交相同的提交信息 “second commit” 如下

此时 test的第二次提交应该和master的第二次提交一摸一样
可以看到,此时只有 5f 目录下新增了一个文件

我们观察5fbc535bb508f73d87a3fea4e82fce1e422c9f53 其类型为commit,由于文件结构和master分支的second commit 一摸一样,所以其tree parent 都指向了复用的内容

但是为什么commit节点却没有被复用,明明提交信息都一样?

是因为commit文件在每次生成的时候都会添加时间戳,这个时间戳会保证每一次的提交生成的commit节点内容都不一样,节点名称自然也不一样 ! 此时object结构如下

看完上面, 你大概就知道 git是如何借助 KV数据库来存储版本信息了。

refs 目录

刚才的demo我们创建了2个分支 branch,那么branch是什么? 我们大概 .git/refs/目录

可以看到,其中包含 heads文件夹,打开之后就对应我们创建的2个分支名

我们打开文件,其内容就是当前2个分支头指针的对应的commitId

所以我们可以看到,分支Branch是一个很 轻量级 的操作,当我们创建一个分支,本质上就是在refs/head目录下创建一个文件保存当前指针指向的commitId

我们继续通过 git tag 创建几个tag,如下:

可以在 refs/tags 目录下,看到我们创建的 tag,其内容也是指向commit节点

当我们使用 git remote add origin url 添加远程仓库后,和本地节点绑定的远程节点会被保存在 refs/remotes 下

refs/origin/

logs 目录

在git中,我们可以通过以下命令查看 分支头节点和全局HEAD的轨迹

git log branchName 查看分支头节点的移动路径
git reflog 查看HEAD的移动路径

比如,我们查看test分支头节点的移动路径如下:

这个路径信息就保存在 .git/logs下

可以看到,logs/refs中 保存了当前所有分支头节点的移动路径

我们打开 test 其内容如下,其中记录了test分支头节点的轨迹

0000000000000000000000000000000000000000 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763535864 +0800	branch: Created from HEAD
d569fdab504b6146d25aae03bafd5932bd4f5c6f 5fbc535bb508f73d87a3fea4e82fce1e422c9f53 liuze  1763536186 +0800	commit: second commit

main 分支的:

0000000000000000000000000000000000000000 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763533232 +0800	commit (initial): fix: first commit
d569fdab504b6146d25aae03bafd5932bd4f5c6f bbbef0c55841da0260ed9861a4c08dbc76cc057f liuze  1763534450 +0800	commit: second commit
bbbef0c55841da0260ed9861a4c08dbc76cc057f cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 liuze  1763535326 +0800	commit: third-commit

全局头节点 HEAD 

git中包含一个全局的头节点,其默认状态下和某个分支头节点的移动保持一致,我们可以通过 

git show head 查看当前头节点的位置

这个位置,保存在.git/HEAD 文件中

当头节点和分支头节点一致,head文件的内容为分支头节点的文件路径,即refs下的头节点文件地址

当我们使用 git switch commitId --detach 分离头指针的时候,此时HEAD会和分支头节点分离,此时的HEAD文件会指向所处commit的ID

当我们使用 git reflog时,可以查看HEAD指针的路径信息,如下

这些信息会被保存在 .git/logs/HEAD 文件中,如下:

0000000000000000000000000000000000000000 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763533232 +0800	commit (initial): fix: first commit
d569fdab504b6146d25aae03bafd5932bd4f5c6f bbbef0c55841da0260ed9861a4c08dbc76cc057f liuze  1763534450 +0800	commit: second commit
bbbef0c55841da0260ed9861a4c08dbc76cc057f cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 liuze  1763535326 +0800	commit: third-commit
cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763535852 +0800	checkout: moving from main to d569fdab504b6146d25aae03bafd5932bd4f5c6f
d569fdab504b6146d25aae03bafd5932bd4f5c6f d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763535864 +0800	checkout: moving from d569fdab504b6146d25aae03bafd5932bd4f5c6f to test
d569fdab504b6146d25aae03bafd5932bd4f5c6f cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 liuze  1763536041 +0800	checkout: moving from test to main
cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763536048 +0800	checkout: moving from main to test
d569fdab504b6146d25aae03bafd5932bd4f5c6f cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 liuze  1763536055 +0800	checkout: moving from test to main
cb796ca9bce8aa7cdaae75dfd1eab15f329e4502 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763536087 +0800	checkout: moving from main to test
d569fdab504b6146d25aae03bafd5932bd4f5c6f 5fbc535bb508f73d87a3fea4e82fce1e422c9f53 liuze  1763536186 +0800	commit: second commit
5fbc535bb508f73d87a3fea4e82fce1e422c9f53 d569fdab504b6146d25aae03bafd5932bd4f5c6f liuze  1763538332 +0800	checkout: moving from test to d569fdab504b6146d25aae03bafd5932bd4f5c6f

posted @ 2025-12-17 13:26  gccbuaa  阅读(4)  评论(0)    收藏  举报