git仓库清理--"保姆级"教程
几个小问题
- 你的git仓库已经创建多久了? (使用命令
git log --reverse
可以查看 ) - 你的git仓库有多大? (使用命令
git count-objects -vH
可以查看 ) - 你在使用git的中过程中会出现"卡"的感觉吗?
前2个小问题,大家可以根据笔者提示查看一下;第三个问题呢,可以主观评价一下;欢迎在评论区一起分享一下你们git库的健康状况.
那我先介绍一下这篇文章的背景吧, 我们部门的项目是2017年7月份的一个项目,至今4年多了;在2-21年8月的时仓库候体积已经达到了20多G;由于git仓库体积过大,给我们开发带来了很多的不便.所以我们做了一下彻底的优化; 现在我把过程中遇到的很多细节和技巧,对给位做一个详细的介绍.希望各位有所收获;
git仓库过大会导致哪些问题?
- git仓库体积过大,占用电脑本地闪存的存储空间;
- clone git仓库时,耗时过长,甚至完全clone不下来导致git报错;
git pull
时会由于引用对象过多会报错,导致本地代码无法更新;- 在切换分支的时候经常会出现cpu占满,内存占满的情况导致电脑死机;
(由于远程仓库的git对象数据量过多50w+个, 仓库数据过大21+G, 导致的下载报错)
以上的随便哪一个问题都会影响开发者的工作体验; 故git库体积过大是一个必须得解决的棘手问题; 处理git库过大无从下手的最主要的原因是, 我们平时大量操作的git命令都是git的高级命令, 对于git为什么会这么大都不甚了解; 哪些文件能删,哪些文件不能动也是没有很好的判断;
又因为一个项目的git仓库之于一个项目的重要, 更是不言而喻; 担心经过一系列操作会影响之前的提交记录和后续的开发进展,所以行动起来总是畏首畏尾,施展不开手脚;
针对以上总总疑惑和担心,本篇文章都会给出具体的原理解释和操作步骤;
git仓库里面存的什么?
从根本上来讲git是一个内容寻址(content-addressable)文件系统, 并在此之上提供了一个版本控制系统的用户界面.
所有的git仓库的根目录下面都有个.git 文件, 它默认是隐藏的.
.git文件夹结构如下:
各文件里面存储的内容如下表介绍:
文件夹 | 类型 | 文件夹里面的内容 |
---|---|---|
hooks | 文件夹 | 目录包含客户端或服务端的钩子脚本;我们最常用的就是pre-commit钩子了; |
info | 文件夹 | 包含一个全局性排除文件; |
logs | 文件夹 | 保存日志信息; git reflog 展示的内容; |
objects | 文件夹 | 目录存储所有数据内容, 这就是实际意义上的 git数据库, 存数据的地方; 并且存了所有的历史记录; |
refs | 文件夹 | 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针; |
config | 文件 | 包含项目特有的配置选项; |
description | 文件 | 用来显示对仓库的描述信息,文件仅供 GitWeb 程序使用,我们无需关心; |
HEAD | 文件 | 它文件通常是一个符号引用(symbolic reference),指向目前所在的分支。某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。 |
index | 文件夹 | 文件保存暂存区信息; |
FETCH_HEAD | 文件 | git fetch; 这将更新git remote 中所有的远程repo 所包含分支的最新commit-id, 将其记录到.git/FETCH_HEAD文件中 |
packed-refs | 文件 | 对refs打包后(git gc)的存储文件, 与底层命令git pack-refs; |
以上文件夹中objects是git变大的"罪魁祸首", 我们要清理git仓库,其实就是要清理掉objects中没有太大价值的内容,那么我们先介绍一下神秘的objects文件夹里面究竟存了些什么; objects文件夹里面就是存了一系列的git对象:
git对象
git对象一共有4种; 他们分别是:数据对象(blob), 树对象(tree), 提交对象(commit), 标签对象(tag);
由于此处的知识点涉及到大量底层命令和实战操作;并且官方文档写的十分的细致和透彻,推荐给位看官直接移步官方<<git pro>>中文版书籍进行直接的学习;我这边只做一些重点知识的汇总;
-
数据对象会把二进制文件进行压缩存储,并输出一个唯一对应的40位hash值作为key进行一一对应;
-
git会把压缩内容存在objects文件夹下,并以对应的key值40位的前两位hash值作为文件夹名,后38位作为文件名;(数据对象就是git清理的重点内容);
-
git是全量快照存储, 而非增量存储;
-
数据对象的hash值与文件名无关; 故数据对象无法存储文件信息;
-
为了解决问题4, git提供了树对象;它可以解决文件名保存的问题, 并且使得项目的多个文件也组织到一起;(树对象就是git清理的第二个重点目标,项目文件过多,每次树对象就会较大);
-
树对象虽然可以表示我们想要跟踪的快照,但是无法记录快照的信息,操作者信息;并且需要git使用者记住hash值;
-
为了解决问题6, git提供了提交对象;
-
提交对象还可以指明它的父提交对象;从而形成一系列的log记录; 我们执行
git log
查看的就是一条条互相链接的提交对象;
下图是一张简单的数据对象(blob),树对象(tree),提交对象(commit)三者之间的关系:
- 灰色的是数据对象(blob); 它可以存储二级制文件的内容;
- 绿色的是树对象(tree); 与数据对象相连箭头上的文字表示, 树对象和把文件信息和数据对象关联了起来;
- 黄色的是提交对象(commit); 它指向一个树对象, 并且它可以和父提交相关联, 还能可以描述本次提交的信息(提交信息, 作者信息,时间等);
git引用
为了减少hash值对git使用者的负担, git提供了各种各样的引用. 比如: 分支、HEAD引用、标签引用和远程引用; 由于引用对git仓库的体积影响面不是很大, 相关内容还是推荐各位读者直接对官方<<git pro>>中文版书籍相关章节进行阅读.
下图是一张最基本的git中4种对象和各引用的关系:
图的讲解顺序由下往上, 由左往右;
- 最下层, 黄色空心椭圆; 是一个数据对象(blob);
- 倒数二层, 红色实心椭圆; 是一个树对象(tree);
- 倒数第三层, 青色实心椭圆; 是一个提交对象(commit);
- 左侧倒数第四层, 绿色实心椭圆; 是一个标签对象(tag);
- 左侧倒数第五层, 绿色实心矩形; 是一个标签名, 是一个引用;
- 中间倒数第四层, 蓝色实心矩形; 是一个分支引用;
- 中间倒数第五层, 蓝色实心矩形; 是一个HEAD引用;
- 右侧倒数第四层, 灰色实心矩形; 是一个远程引用 (remote ref);
git库到底为什么会越来越大呢?
svn等其他的版本控制系统, 都是对文件版本的理念, 是以文件为水平维度, 记录每个文件在每个版本下的delta (改变)值. 而git对文件版本的管理理念却是以每次提交为一次快照, 提交时对所有文件做一次全量快照, 然后存储快照的引用. 如果文件没有修改,git不再重新存储该文件, 而是只保留一个链接指向之前存储的文件.
快照流如下:
(图中虚线就表示,此文件在当前这一次快照中是没有发生改变的, 复用的上次的数据对象;)
我们执行的git操作,几乎都只会往git数据库中添加数据. 我们很难使用命令从git数据库中删除数据,也就是说 git几乎不会执行任何可能导致文件不可恢复的操作. 哪怕是我们在项目中删除某些文件, 对于git都是在新增新存储对象(树对象, 提交对象). --这边是理解难点;
同别的版本控制系统一样, 未提交更新时有可能丢失或弄乱修改的内容. 但是一旦你提交快照到git中,就难以再丢失数据,特别是如果你定期推送到其它git数据仓库的话.
因为git存的是全量的数据对象的快照; 也就是说一个文件(100kb)你只修改了一行, git仓库都会把这份文件存储两份(201kb);
git就会这样越来越大,而git却什么事情都不做吗?
答案是: 显然不会;
git最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式. 但是,git会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率. 这个命令就是:
git gc
“gc” 代表垃圾回收, 这个命令会做以下事情: 收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件, 移除与任何提交都不相关的陈旧对象.
git gc
执行后会创建一个包文件和一个索引.
包文件包含了刚才从文件系统中移除的所有对象的内容. 索引文件包含了包文件的偏移信息, 我们通过索引文件就可以快速定位任意一个指定对象.
(上图中, packed-refs 是对引用的压缩打包, objects/pack文件下pack-hash值开头的两个文件: 以idx结尾的就是索引文件, 以pack结尾的就是包文件它里面存储了真正的数据对象, 需要索引文件才能解析获取到;)
但是, 就算git提供了完善压缩优化机制也无法修复我们git仓库过大的问题. 因为git clone
会下载整个项目的历史, 包括每一个文件的每一个版本(数据对象, 树对象). 如果所有的东西都是源代码那么这很好, 因为git被高度优化来有效地存储这种数据. 然而, 如果某个人在之前向项目添加了一个大小特别大的文件, 即使你将这个文件从项目中移除了, 每次克隆还是都要强制的下载这个大文件. 之所以会产生这个问题, 是因为这个文件在历史中是存在的git对象(数据对象, 树对象), 它会永远在那里(.git objects 文件夹下).
至此我们需要找到两类文件, 它们是我们重点需要处理的文件:
- 本可以存在cdn上的资源如: 视频, 图片, icon, 等;
- 文件虽然有工程意义, 需要保留在项目里面, 但我们却没有必要了解他们的commit记录的文件如: 编译后供给cdn服务器编译后的文件, 后端接口生成的类型定义文件(pont工具), 打包生成的镜像文件, 等;
至此我们了解到, .git究竟存量哪些内容,他们分别有什么用. 也明确了我们要清理掉哪些东西--它们就是git数据对象和重新整合的树对象和提交对象; 接下来的工作就是找到上述整理的2类文件进行清理了.
那我们到底用什么方法来清理呢? 下面我们来介绍2个工具, 一个是官方提供的filter-branch, 二就是本篇文章的主角BFG;
用filter-branch去做git仓库的清理(纯了解!无需实战!)
介绍两个本次清理需要多次用到的git底层命令:
- verify-pack 可以查看git pack包中存储的数据对象的相关信息;
git verify-pack -v .git/objects/pack/[pack包的hash].idx
本条命令返回的格式如下:
SHA-1 type size size-in-packfile offset-in-packfile
或
SHA-1 type size size-in-packfile offset-in-packfile depth base-SHA-1
其中第一列是hash值, 第二列是git对象的类型,第三列是原本的大小, 第四列是打包后的大小 ....
我们需要的是前3列的信息, 并且用linux命令以第三列进行排列; 取最大的一些文件进行分析;
2. rev-list 可以根据hash值查看到,对应的文件名信息;
git rev-list --objects --all | grep [blob对象hash值]
使用步骤如下:
- 查找占用空间大的文件是哪些. 执行以下命令行:
# 使用verify-pack命令查看, pack包里面的最大的10个文件对应的hash值
git verify-pack -v .git/objects/pack/[pack包的hash].idx | sort -k 3 -nr | head -n 10
# 根据rev-list命令来查看, 最大的文件的 文件名是什么
git rev-list --objects --all | grep [blob对象hash值]
# 以下为真实操作的demo
git verify-pack -v .git/objects/pack/pack-20e0b86c39b603c14f134768b583fb7d052b44d9.idx| sort -k 3 nr | head
# 查看hash值为d8ffa048ad87ee8fcb8eada24e3ecc36f7eb2fc3 它对应的文件名是什么
git rev-list --objects --all | grep d8ffa048ad87ee8fcb8eada24e3ecc36f7eb2fc3
以上两步在整个清理中我们需要多次用到, 且需要多次查看 hash类别和查找对应的文件名故我们可以写一个名为largelist.sh的shell脚本存放在.git文件下用以快速查找体积较大的对象内容; (清理后需要删除largelist.sh脚本);
#!/bin/bash
#set -x
IFS=$'\n';
# 默认值是 10个 可以一次性展示更多 就修改第15行代码 | head -n 30 或者更多
objects=`git verify-pack -v objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`
echo "All sizes are in kB. The pack column is the size of the object, compressed, inside the pack file."
output="size,pack,SHA,location"
for y in $objects
do
# extract the size in bytes
size=$((`echo $y | cut -f 5 -d ' '`/1024))
# extract the compressed size in bytes
compressedSize=$((`echo $y | cut -f 6 -d ' '`/1024))
# extract the SHA
sha=`echo $y | cut -f 1 -d ' '`
# find the objects location in the repository tree
other=`git rev-list --all --objects | grep $sha`
#lineBreak=`echo -e "\n"`
output="${output}\n${size},${compressedSize},${other}"
done
echo -e $output | column -t -s ', '
创建完成后需要给 largelist.sh 文件赋予执行权限; 并执行文件
chmod +x largelist.sh
./largelist.sh
结果格式如下:
2. 根据文件名查找到对应的提交记录, 找到对应文件的第一次commit对象的hash值;
git log --oneline --branches -- [文件名]
3. 使用git filter-branch 命令
git filter-branch --index-filter \
'git rm --ignore-unmatch --cached [第1步找到需要清理的文件名]' -- [第2步找到第一次提交的hash值]^..
# 以下为demo值, 删除和名为 w.gif 相关的文件记录, 它首次操作的commit对象的hash值为d90c646 注意 ^ 符号和 .. 符号
git filter-branch --index-filter \
'git rm --ignore-unmatch --cached w.gif' -- d90c646^..
4. 重复第2, 3两步,删掉所有有影响的文件, 然后再重复1,2,3三步, 继续查看是否还有有问题的资源需要清除;
5. 清理相关的历史 log, 和清理相关的git dead的对象和无引用的对象;
rm -Rf .git/refs/original
rm -Rf .git/logs/
git gc
git prune --expire now
6. 此刻本地git仓库以及清理完毕.
7. 后续的怎么同步到所有开发本地, 以及如何避免被污染后续再讲
亲测git filter-branch是可以做git仓库清理的; 但是它非常非常的慢! 官方文档也有强调. 这个命令非常耗资源,而且效率不高,容易出错;整个文档 warning占了大半篇幅; 那么还有别的方法了吗? 那就要祭出万众瞩目的 BFG 了!
用BFG去做git仓库的清理(最佳工具)
BFG是用Scala语言写的一个工具库, 它做的就是git仓库清理的工作,它的特点就是快, 还有就是好用, 功能非常强大! 那为什么不直接介绍BFG呢? 还是希望大家能够通过比对, 能感受BFG的带来的提效;
BFG的使用条件
- 得有本地的 java 环境;
- 得下载好bfg的jar包 (官网右上角 download 按钮进行下载);
BFG使用的几种方法
-
bfg --delete-files id_{dsa,rsa} my-repo.git
- 作用: 按照文件名来删除指定的文件
-
bfg --delete-folders folders --no-blob-protection my-repo.git
- 作用: 删除指定的文件夹
- --no-blob-protection 表明需不需要在master分支进行文件的保留
-
bfg --strip-blobs-bigger-than 50M my-repo.git
- 作用: 删除体积大于多少体积的的文件
- 这个命令其实是没有实战价值, 因为我们不能仅因为它大就判断它是无价值的;
-
bfg --replace-text passwords.txt my-repo.git
- 替换指定的文件
BFG的使用步骤
-
git clone --mirror git://example.com/some-big-repo.git
-
java -jar bfg.jar--strip-blobs-bigger-than 100M some-big-repo.git
- 这一步就是根据bfg提供的几种方法,去定制化删除需要删除的文件.上述的4个方法都可以多次组合使用;
- 哪些文件是需要删除的? 下文会讲解到;
-
cd some-big-repo.git && git reflog expire --expire=now --all && git gc --prune=now --aggressive
- 清理完指定的对象后,需要对git的reflog记录进行清空, 并且进行git仓库的垃圾处理删除无意义的git对象;
-
git push --mirror
bfg基本关键步骤就是上面4步, 但是具体在项目实战中还有很多坑, 下文会补上更为详细的保姆级的实战操作记录;
Dataphin前端仓库清理的BFG实战操作记录
Dataphin是阿里巴巴集团OneData数据治理方法论内部实践的云化输出,一站式提供数据采、建、管、用全生命周期的大数据能力,以助力企业显著提升数据治理水平,构建质量可靠、消费便捷、生产安全经济的企业级数据中台。
1. 根据项目结构分析整理出哪些是需要处理的文件
文件 | 文件夹描述 | 处理原因 | 处理方式 | |
---|---|---|---|---|
src/services | 由pont生成的接口类型定义,和接口请求对象; | 1. pont自动生成,无查看修改记录的意义; | 1. 文件过多,内容过大; | 保留文件,仅删除commit记录 |
devBuild | 之前的DLL文件, 用来做本地模块缓存; | 1. 文件多, 体积大; 2.升级webpack5后也无需此资源了 | 1. 没有阅读查看价值无查看修改记录的意义 | 删除文件和commit记录 |
2. 利用largelist.sh脚本去查看其它可以被治理的文件
由于分支众多, 且历史悠久, 无法单单通过查看当前几个主分支的项目结构就能找到所有的有文件的问题; 所以这个时候还是需要使用git verify-pack 和 git rev-list 这两个git底层命令去查找需要治理的文件; 上文filter-branch章节有详细介绍; 这边我就直接贴一下largelist.sh脚本的内容, 以及执行步骤;
因为BFG需要使用的是git镜像所有我们得把largelist.sh存放到同级目录下, 清理完成后得删除掉largelist.sh脚本;
#!/bin/bash
#set -x
# Shows you the largest objects in your repo's pack file.
# Written for osx.
#
# @see http://stubbisms.wordpress.com/2009/07/10/git-script-to-show-largest-pack-objects-and-trim-your-waist-line/
# @author Antony Stubbs
# set the internal field spereator to line break, so that we can iterate easily over the verify-pack output
IFS=$'\n';
# list all objects including their size, sort by size, take top 10
# 默认值是 10个 可以一次性展示更多 就修改第15行代码 | head -n 30 或者更多
objects=`git verify-pack -v objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`
echo "All sizes are in kB. The pack column is the size of the object, compressed, inside the pack file."
output="size,pack,SHA,location"
for y in $objects
do
# extract the size in bytes
size=$((`echo $y | cut -f 5 -d ' '`/1024))
# extract the compressed size in bytes
compressedSize=$((`echo $y | cut -f 6 -d ' '`/1024))
# extract the SHA
sha=`echo $y | cut -f 1 -d ' '`
# find the objects location in the repository tree
other=`git rev-list --all --objects | grep $sha`
#lineBreak=`echo -e "\n"`
output="${output}\n${size},${compressedSize},${other}"
done
echo -e $output | column -t -s ', '
创建完成后需要给 largelist.sh 文件赋予执行权限; 并执行文件
chmod +x largelist.sh
./largelist.sh
结果格式如下:
通过查看返回结果, 前置的都是视频文件, 所以我们需要把所有 .mp4 相关的文件都删除掉; 还有一些压缩包, 一些报告文件. 因为,这些可能都是开发者自己的开发分支, 然后提交到远程; 所以这些问题在实际项目中还是比较难避免的;
最后我们删除的文件有: services, devBuild,*.mp4, welcome.gif, welcome2.gif, app.js,app.js.map, report.html, test.tar.gz, editor.main.js.map, editor.main.js, public.tgz, log0000, guiji.fbx ...
3. 处理好后的干净库如何同步到所有参与开发的同学?
经过上述所有的操作得到一个干净的git库后, 还需要避免其他有历史库的同学执行push操作, 因为一旦执行push操作就会前功尽弃,导致陈旧的历史库再次污染到新的远程仓库中; 所以我们需要设计一个全流程的操作的步骤, 去避免这样的污染问题;
我这边将提供我们实际的操作每一步的操作,并说明原因和考虑点;
4. 步骤详细版:
特殊说明: 因为我们项目的原仓库地址关联了许多阿里内部的平台,所以我采用的是保留原项目地址的方案,这个方案相对复杂一点; 当然读者也可以采用废弃旧库地址,直接重启一个新库的方案来进行迁移, 那么步骤和下面列出来会有稍许不同; 欢迎大家在评论中补充创建新库的具体步骤;
安装 java 环境;
下载bfg-1.14.0.jar;
执行
it clone --mirror xxx.git xxx-mirror.git
- 提前下载好两个镜像 (一个用来操作的:xxx-mirror.git ,一个用来做安全备份:xxx-mirror-copy.git);
设置gitlab库为私有库, 保存之前的库成员列表;
删除所有当前库用户的权限, 仅留自己, 确保所有人不能push(gitlab中浏览者也可以进行push);
- 因为优化的结果, 一旦被之前开发者本地旧仓库一进行push操作就会有污染;
- 为了防止旧库的push污染操作,在优化前关闭所有的push通道;
- 选择在周日执行git库的优化, 是一个很好的时间窗口;
找其他同学check权限设置是否生效;
在上述的两个镜像仓库执行
git remote update
, 进行镜像的更新;备份库需要删除remot引用,断绝污染:
git remote remove origin
;开始执行 BFG 的命令
java -jar ~/utils/bfg-1.14.0.jar --delete-folders {services,build,devBuild} --no-blob-protection dataphin-mirror.git
java -jar ~/utils/bfg-1.14.0.jar --delete-files "*.mp4" --no-blob-protection dataphin-mirror.git
java -jar ~/utils/bfg-1.14.0.jar --delete-files {xxxxx.gif,xxxxx.gif} dataphin-mirror.git
java -jar ~/utils/bfg-1.14.0.jar --delete-files "{app.js,app.js.map,guiji.fbx,report.html,test.tar.gz, editor.main.js.map, editor.main.js, public.tgz, log0000}" dataphin-mirror.git
- 支持 *.mp4 这样的语法, 但是得加上 " 引号包裹;
cd xxx-mirror.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
- 清理完指定的对象后,需要对git的reflog记录进行清空, 并且进行git仓库的垃圾处理删除无意义的git对象;
删掉largelist.sh脚本;
git push --mirror
由于这边
git push --mirror
推送的引用 较多, 需要关掉gitlab的邮件检查否则会报错;报错文案如下:
gitlab仓库中邮箱检测的关闭路径: Settings => Features => Check Email 一定要点击SAVE CHNAGES按钮才会生效!
在git push的时候由于引用较多往往需要集团gitlab服务的同学帮忙远程处理(执行 git gc);
各位同学切换原库的remote到一个备份的bak git仓库上. 并改老库仓库名为 xxx-bak
git remote remove origin
;
git remote add origin git@gitlab.alibaba-inc.com:xxx-bak.git
;
cd .. && mv xxx xxx-bak
;
- 防呆处理, 防止万一有人误操作;
- 当然删除本地的就仓库也行,也能避免污染,但是不推荐这么去做, 因为本地的旧仓库在短期内还是有价值的; 保存旧库在本地, 会让开发者感觉到踏实;
确认参与开发的同学第14步的切换操作成功, 并删除本地多余的的老仓库后, 给予开通权限;
参与开发同学克隆新库到本地,
git clone git@gitlab.alibaba-inc.com:xxx.git
;完成! 后续操作就和之前开发的步骤一样了!
此时你得到两个库: 1个是旧库 xxx-bak 它存着你之前的代码; 一个是新库它是优化瘦身后的新库;
- 老库过段时间可以删除.
- 优化的后的镜像xxx-mirror.git也可以删除.
- 备份镜像xxx-mirror-copy.git建议保留一段时间.
5. 本次Dataphin前端优化的效果对比
- 优化前:
- 无法clone 项目, 经常报错;
- 无法git pull项目;
- 体积过大 21.48G;
- 切换分支电脑卡死;
- 优化后:
- 体积变为144M;
- 减少克隆库的时间;
- git pull流畅;
- 切换分支不会死机;
- 其他:
- 所有的src下面的业务代码没有任何的影响, 修改记录得以保持;
- 所有的分支和tag都得以保存;
其他
什么? 看完文章后还是没有体验到"保姆级"的感受; 那就欢迎多多评论,来沟通, 一起来共同学习git库的清理吧;
推荐视频:Git初版原理与源码解读
转载自:https://juejin.cn/post/7024922528514572302