一:背景描述
问题起源于生产环境上,每次升级上线采用的是全量的安装包替换的方式升级,然后每次打的新的安装包体积越来越大,导致负责升级的运维同事下载分发安装包的时间过长,因此研发侧分析到是git本身自带的微型的文件管理系统数据过于臃肿导致,故有了此文,目的在于对git项目的文件,分支管理做到简单的透析并试图清理object目录,做到项目瘦身。
二:问题分析
为了分析git项目的基本的逻辑,特意搞了一个简单的git项目以求结构清晰简洁,进入到.git目录下,整个目录的结构如下: 接下来将对上述主要目录或文件做简要解析。
1:branches
这里,branches是一个目录,但是在我经手的项目中,这个目录都是空的,暂时不知用途,本地的git base中甚至不存在这个目录,暂时忽略其作用,评论区有明白的或者后续有时间我研究研究再补充上。
2:config
这个是文件,是可以直接编辑的文件,其中这个新建的项目内容如下(已做匿名化处理):
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = http://name@url/test.git <---------记录了你从remote拉取项目时的身份信息
[branch "master"]
remote = origin
merge = refs/heads/master
当然,这个文件还存储着调用git global --name/emain 的账号信息,以及每次本地存储的从remote拉取的分支信息。
3:description
这个是用来对当前git仓库的描述的,无实际用途,当前的这个文件内容如下
Unnamed repository; edit this file 'description' to name the repository.
4:HEAD
这个文件记录着当前工作区所处的分支,例如当前的项目所处的是master分支上,如果在该分支上开发提交也会push到remote的该分支上
ref: refs/heads/master
这个文件是很有用的,例如命令:git reset --hard HEAD ,表示回退到当前分支的没有任何变更状态(注意会清空当前分支上所有工作区变更)由于git命令不是本文主要内容,这里只是举例。当然使用命令
git branch -l
结果显示的就是这个HEAD分支
[root@leel info]# git branch -l
* master
5:hooks
这是一个目录,见名思意,钩子,这个目录下是一些钩子方法,通俗的讲就是一系列的中间件方法,这些方法都是用shell的脚本,以其中commit-msg.sample为例
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
6:index
这是一个文件,内容是二进制格式书写的,是暂存区(stage),文件内容是文件名等信息 例如我这里更新一个文件,注意更新前index暂存区的时间如下 当我变更一个文件,并且git add 将文件添加到暂存区后,这个时间就变更了,这个文件存储着暂存区的内容。
7:info
这是个目录,其中有一个文件exclude,见词知意,是排除的意思,这个文件中用来排除掉在.gitignore文件中声明的意外的文件,可以免于被忽略提交,实际中没有使用过,可以忽略这个功能。
8:logs
这是一个目录,这个目录存储着commit日志信息,例如执行命令git log ,所返回的结果就是这里获得的。这个logs是很重要的,这里着重介绍一下: 由于自己是新建的项目,没有进行过复杂的操作,所以这个目录下的内容较少
├── logs
│ ├── HEAD <---------记录着所有当前本地仓的能够引起commit日志变更的命令,pull/rebase/reset/commit等命令
│ └── refs <-----------记录着本地仓库所有分支,commit日志及其关联关系
│ └── heads <----------所有分支的能引起日志变更的操作和
│ └── master <----------分支名,查看该分支文件记录内容,得到该分支上的变更
由于上述是自己新克隆下来的项目,很干净,所以信息不多,为此,特意找了一个在用的项目,脱敏后如下:
.
├── HEAD
└── refs
├── heads
│ ├── qa
│ ├── version_4.4.0
│ ├── version_4.5.0
│ ├── version_4.5.0.1
│ ├── version_4.5.1
│ └── update_bulk
├── remotes <---------记录了当前机器上所有人在该项目仓库中添加的远程路径(公用的机器上更新各自代码的产物)
│ ├── xiaoming <---------该用户名以及该用户的远程分支
│ │ ├── 2019_master
│ │ ├── 4.3.0_vip6 <--------远程分支名,可以查询该分支的变更,`cat 分支名`
│ │ └── yz_vip
│ ├── leel
│ │ ├── 4.3.0_add_az
│ │ ├── 4.3.0_vip6
│ │ └── yz_vip
│ ├── leel_ez
│ │ ├── 4.3.0_add_az
│ │ ├── 4.3.0_vip6
│ │ └── yz_vip
│ └── xiaohong
│ ├── 2019_master
│ ├── 4.3.0_add_az
│ ├── 4.3.0_vip6
│ └── yz_vip
└── stash <---------git stash执行后保存的记录
经过复杂操作的洗礼的logs目录就复杂多了,新增了stash文件以及remotes目录, 这里需要注意的是,如果当前的代码仓只有自己一个人使用,是不会在remotes中出现多个用户的记录,同时,这里stash细心的人就会发现没有按照用户名来做分类,说明stash是一个全局的栈存储,不相信的话可以自行查看该文件,这里就不演示了(脱敏费事),相关的文章具体可以移步我的其他资源https://blog.csdn.net/qq_21583139/article/details/123571869
9:objects
这个目录是此次安装包瘦身的关键,‘万物皆对象’,git也不例外,对每次的commit都会建立一个‘档案’,保证每次的checkout 都能立即切换到其他的分支,每次git reset HEAD^ 都能够回退到上一次的commit状态,这是因为git整体维系了一个微型的文件管理系统,分支/commit就是以文件的形式存储在这个目录下的。 接着我们继续看objects目录及其下面的文件。
.
├── 7f <----------- 每次commit对象的ID的前两位
│ └── a09fafe8441d1f3b6b9fdd1dd2247e5e1725e3 <------------commit id除去前两位的剩余字符,和前两位拼接就是完整的commit id,存储着二进制格式数据
├── info
└── pack <------------git 项目自动打包的文件,瘦身的主要对象
├── pack-06431b47ca3c88c64b1b8c91e44550942a8584e1.idx <-------------二进制索引文件
└── pack-06431b47ca3c88c64b1b8c91e44550942a8584e1.pack <-------------二进制pack文件
注意,这里的7f是一个目录,也就是说所有经过git处理的commit经过hash后的id,只要前两位是7f,就会被放置在这个对象目录下,而且使用二进制存储也是git的考量的,这是因为 git 出于内存考虑,会使用 zlib 压缩算法 对存储内容都进行了压缩处理,为了能获取可读的内容我们可以通过如下命令,看看这个二进制文件内容到底是什么,
git cat-file -t <key>
git cat-file -p <key>
git cat-file -s <key>
[root@leel 7f]
这个是研究生毕业的系统项目,django2.1.7,bootstrap4,jquery
因此通过上边的命令实现了解析,记住这个解析后的内容。 为了和上面二进制内容对比,查看这个commit到底做了哪些改变
git show commit_id
[root@leel 7f]
这个是研究生毕业的系统项目,django2.1.7,bootstrap4,jquery
结果一致,因此,我们就知道了每次我们想要查看每个commit到底做了哪些变更所执行的git show id 以及切换某个commit状态时的git checkout commit_id 时git是怎么做到的了。 此外,我们知道git中对象的直接引用是通过一个固定的40位的hash来做键的,比如我新建一个文件,并进行如下操作, 即新增了一个文件,并且git add添加到暂存区后,就会生成一个commit id为18开头的, 我们可以通过如下命令检验
git hash-object test.txt
[root@leel smartlog]
1844877d0b1be2236e42bbb92434220b4d24b5b0 <------------结果显示确实是这个commit_id
当然,objects目录下的这个新的文件是二进制,需要通过git cat-file -p <key> 查看,这里就不赘述。
接下来,就是重头戏pack目录,此时瘦身的主要对象,将在本文第三部分展开。这里简要介绍一下git中的pack对象。 在git中,pack文件可以有效的使用磁盘缓存,并且为常用命令读取最近引用的对象提供访问模式;git会将多个指定的对象打包成一个成为包文件(packfile)的二进制文件,用于节省空间和提高效率。
git 默认的存储方式使用的是松散对象格式。但是当push代码,或者手动调用git gc时,git 会将这些文件打包到 packfile 文件中,packfile 中对于同一个文件只存储一份完整的内容(在最近的提交中,因为 git 默认用户更常查看最近的提交),而之前的提交只需要保存两者之间的差异,真正达到只保存修改的效果。
10:packed-refs
这是一个文件,文件名已经告知我们很多内容了,是packed-references,表示打包-引用。建议了解这个文件前先看11。因为这个文件的内容如下:
013bee1a32dd6d9684374cae71f5ad8fa725dccd refs/remotes/origin/master <------分支最新的commit_id remote分支名称
67cf8463e591bdd4d8f37838b69a62b08e6d24c5 refs/remotes/origin/dev
很明显这个文件的打包的路径是refs下的非heads下所有分支命名的,如果remotes下有用户名还会有用户名进行拼接。
11:refs
git目录下最后的结构,这是个目录,存储着引用的信息。细心的话会发现3:logs中也有一个refs,目录结构一致,但是实际上文件存储的内容是不同的。确实是我们在使用branch、tag时大多数都是引用到该目录下,然后再指向具体的objects。属于对用户友好的一种文件寻址方式,可以和objects中的对象存储对比,
项目 | objects | refs |
---|
交互性 | 计算机友好 | 用户友好 | 命令对象 | commit | branch | 存储方式 | hash | branch_name/tag | 存储内容 | 每次commit提交内容的二进制数据 | 每个分支最新commit的id |
从上表中能够看出端倪,git工具为commit和branch的切换提供了两种文件存储和索引方式,前者存储在objects,后者存储在refs目录下。知道了二者的差别,我们接下来进入refs目录下,详细看看内容。
.
├── heads <-----------------目录,存储着本地的所有分支,git checkout branch_name的基础,其中回到HEAD,就是显示的当前的branch_name
│ └── master
├── remotes <-------------目录,存储远程的所有分支,同样也区分不同用户和3logs一样
│ └── origin
│ └── HEAD <-----------这里要和3中的logs/refs中的内容区分,这里的分支文件内容不是变更记录,而是commit_id,从而使用git checkout branch_name实现与objects中commit_id的联动
└── tags <----------通过tag号引用方式
结合以上的分析知道,refs这种用户友好的分支引用方式实际上是一种间接引用方式,本质上还是需要通过objects中的commit_id来切换(因为refs中分支文件中存储的是commit id)这种方式就如同机器语言与高级语言之间的区别。 前面提到了logs下也有一个refs,结构和这个refs相似,为了透明展示,特选择了refs中一个分支文件的数据与logs中的refs中的数据最对比,这里我都选取了master文件进行展示。
路径:logs/refs/heads/master
0000000000000000000000000000000000000000 013bee1a32dd6d9684374cae71f5ad8fa725dccd root <root@leel> 1667271551 +0800 clone: from http://leel@url/hackathon/smartlog.git
路径:refs/heads
013bee1a32dd6d9684374cae71f5ad8fa725dccd
结果呼之欲出,logs侧重变更,refs是引用,侧重寻址,存储的是commit_id。 备注:前面提到refs目录结构和logs/refs一致,所以这个模块中也是有stash文件的,自然也是存储着commit_id,由于篇幅原因,没有列出在refs中。
3:git目录瘦身
通过第二部分的分析,我们知道整个git目录下最占据空间的就是objects目录了,每个commit对象都存储在这个目录下,在项目瘦身中,我直接使用需要瘦身的项目为例,显然直接干掉这个目录是愚蠢的行为,因为会导致git对象丢失,git一系列功能失效报错,甚至后续的对该项目的生产环境打patch也无法实现(直接替换文件的方法除外哈) 在瘦身前,pack有如下数量和占据空间大小,
[root@leel .git] ls objects/pack/ | wc -l && du -lh --max-depth=1
78 <--------gc处理前的pack目录下文件数量,29个idx和29个pack
90M ./objects
7.3M ./logs
7.3M ./refs
68K ./rebase-apply
128K ./info
4.0K ./branches
56K ./hooks
105M . <------------gc处理前git目录空间大小
显示文件数78个 先执行git gc 操作,关于gc操作读者请自行百度,这里不赘述(这里不探讨是否自动执行git gc问题)。 再次查询pack目录下文件数以及所占空间,
[root@leel .git] ls objects/pack/ | wc -l && du -lh --max-depth=1
2 <---------------pack目录下经过压缩后的数量为2个,一个是idx,一个是pack
57M ./objects
2.4M ./logs
324K ./refs
68K ./rebase-apply
260K ./info
4.0K ./branches
84K ./hooks
60M . <----------------经过gc处理后的git目录空间大小
对比后显而易见,文件数量减少了,磁盘空间也得到了释放,这么一看似乎我们已经实现了效果,在整个项目中,整个git目录也由105M变为了60M,但是根据参考文献5可知,gc后只是对原有的commit做了整合,无效的commit(分支已经删除)暂时不会清除,本文未完待续。
参考文献: 1:https://zhuanlan.zhihu.com/p/471591319 2:http://t.zoukankan.com/linhaostudy-p-8349912.html 3:https://www.ysext.com/articleinfo/1599.html 4:https://blog.csdn.net/lihuanshuai/article/details/37345565 5:https://zhuanlan.zhihu.com/p/492086828
|