方案对比
subtree
使用命令 git subtree split -P dirPath -b branchName
将目标文件夹的代码都保存到指定分支。试了下,该方案虽然保留了 commit,但是所有分支全都没了
filter-branch
git filter-branch --prune-empty --subdirectory-filter dir1 -- --all
--prune-empty
:表示如果修改后的提交为空则扔掉不要--subdirectory-filter
:指定子目录路径-- --all
:针对所有的分支
当上述命令执行完毕后,就可以看到本地的新仓库已经是原仓库子目录中的内容了,且保留了关于该子目录所有的提交历史。看了下仓库大小和操作前没有变化,因为 .git 目录里还保留着无用的 object。还需要清理一下无用文件。下面来实操一下 filter-branch
怎么拆分仓库。
实战
模拟 git 仓库
为了比较好追踪问题,我们先模拟出一个 git 仓库,这样数据量小,排查起来会比较方便。
看一下 objects 文件夹:
在操作之前,先看一下仓库大小,使用 git count-objects -v
命令可以计算仓库大小:
1 | count: 27 |
查找仓库里的大文件
使用下面的命令可以查找仓库中的大文件:
1 | git rev-list --all --objects | \ |
大概说明一下上面脚本使用到的命令:
git rev-list
:查看指定对象的文件路径、文件名
1 | git rev-list --objects --all |grep a6d75f |
git verify-pack
:查看 git 打包文件的信息,输出 SHA-1、size 等字段。这里用到 $ 先保存 git verify-pack
的输出。
如果直接使用上述脚本会发现什么也没输出,因为脚本首先是分析 git 打包文件,然后再进行大小排序的。所以我们需要先使用命令 git gc
打包 git 本地目录存储的文件。
执行 gc 命令后,blob 对象会被打包,再查看 objects 目录可以看到创建了一个包文件(pack 文件)和一个索引文件。Git 会对大文件进行打包,生成 .pack
格式的文件以及同名的 .idx
格式的索引文件,存放在 .git/object/pack 目录中。通常来说,Git仓库的大文件都是.pack格式的,存放在这个目录中。
gc 后,我们再看一下仓库大小:
1 | count: 0 |
可以看到比 gc 之前稍稍少了一点。现在可以列出 idex 索引文件中存储的文件:
1 | git verify-pack -v .git/objects/pack/pack-e3b9f038d3df8b6214fef04e37477f98a0b48911.idx |
上述每行里各项值分别对应着:
SHA-1, type, size, size-in-packfile, offset-in-packfile
执行 filter-branch
filter-branch
需要指定目录,拆分仓库时可能需要同时保留多个目录,可以使用以下脚本(这里仅保留 aaa,如果填的是 aaa bbb,即可同时保留 aaa 和 bbb 文件夹):
1 | git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- aaa' --prune-empty -- --all |
执行完命令后,git 图表变成:
可以看到,与 aaa 文件夹下文件无关的 commit 都被移除了。分支也都被保留了,棒棒的!正是我们想要的效果。再看保留下来的各个 commit 的内容:
可以看到,虽然 commit 的提交信息没有变,但是内容却变了,first commit 原先新增了 aaa/Main.java 和 bbb/Main.java,但是操作后,提交记录里只剩下了 aaa/Main.java。内容变化了,同样的 SHA1 值也变化了。
再查看一下仓库大小:
1 | count: 10 |
额,size-pack 的大小没有变化,我们直接查看一下 objects 目录。
虽然工作目录中不需要的文件已经被清除了,但是 git/objects/pack 目录里存储的 pack 文件和索引文件却没有被删除,重新读取一下 idx 文件的索引:
1 | git verify-pack -v .git/objects/pack/pack-e3b9f038d3df8b6214fef04e37477f98a0b48911.idx |
和执行 filter-branch
之前是一模一样的。要不再 gc 一下看看?再次 gc 后,blob 文件又被打包,并生成了两个新的 idx 文件和 pack 文件:
再次查看索引:
1 | git verify-pack -v .git/objects/pack/pack-af08fc2fbea78dfae1503ae4b03578a4113da969.idx |
690898
这个 commit 对象是最初添加大文件时的 commit,可以看到该 commit 对象依然存在在最新的 idx 文件中。实锤了,一番操作实际上只是工作目录看起来空旷了,git 仓库里不该有的文件还是一样没落下。
移除无用文件
执行到这一步目标很清楚,就是把 690898
这类已经不可达的 commit 和 pack-af08fc2fbea78dfae1503ae4b03578a4113da969.pack
这些已经没有任何历史提交引用的文件都删除掉。
谷歌一搜,网络上流传的方法试一下:
1 | rm -Rf .git/refs/original |
可以看到,执行到这里,git/refs/original
里空空如也,并不需要删什么东西,而且 git gc 我们刚也试过了,并没有什么卵用。git prune
也是一样的,因为 gc 实际上就是调用 git prune
。
事已至此,不妨用 Java 垃圾回收的思想来理解:文件之所以没有被删掉,肯定是哪里还存在这引用,找出引用应该是解决问题的关键。
引入的大文件,从上面可以看出就是 04ddd8
这个,使用命令查看以下文件路径
1 | git rev-list --objects --all |grep 04ddd8 |
再随便翻翻 git 本地目录,查看 packed-refs
文件,refs
= references
,感觉有点东西啊,打开一看果然发现文本里记录着 refs/original 相关的东西。
1 | # pack-refs with: peeled fully-peeled sorted |
690898
这个 SHA1 值很眼熟了,就是最开始的一个 commitID。再回看最初的 git 图表,可以看到 690898
和 c4111b
分别指向操作前的 master 分支和 test 分支。
filter-branch
后,整个 git commit 树都变了,大清都亡了,之前的这两个引用肯定是没用的旧引用了,删删删!然后再使用命令检查可达性 :
git fsck --full --unreachable
:验证数据库中对象的连通性和有效性。
1 | Checking object directories: 100% (256/256), done. |
可以看到我们最想删掉的大文件 04ddd8
被列出来,并且是不可达的。
再执行命令 git repack -A -d
,确保不可达的对象被解压并保持解压。然后再调用 git prune
重新计算项目大小:
1 | count: 0 |
可以看到,无用的旧文件已经被清除了~
相关命令
这里记录以下调研过程中遇到的命令,git 接触很久了,但是很多命令却还是第一次见,深感自己之渺小。
保存镜像
git clone --mirror xxx地址
查看仓库大小
git count-objects -v