Git 历史清理实战:从 99MB 到 4.5MB

1038 字
5 分钟
Git 历史清理实战:从 99MB 到 4.5MB

背景#

有一个老项目仓库,clone 下来要 99MB,但实际代码没多少。原因是历史提交里塞进了大量二进制文件(微信协议相关的 .exe、.dll),后来虽然删掉了,但 Git 的历史记录还保留着这些文件——git clone 会把所有历史 blob 都下载下来。

另外,之前的 commit 里硬编码了数据库密码、IP 地址、内部账号等信息,需要全部替换掉。


第一步:镜像克隆#

普通 clone 只拉当前分支,镜像克隆拉取所有 refs,适合做历史重写:

Terminal window
git clone --mirror https://gitee.com/用户名/项目.git 项目-mirror
cd 项目-mirror

第二步:分析仓库#

找大文件#

Terminal window
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectsize) %(objectname) %(rest)' \
| sed -n 's/^blob //p' \
| sort -rnk2 \
| head -20 \
| awk '{printf "%.2f MB %s %s\n", $2/1048576, $3, $4}'

结果:最大的 blob 是一个 31MB 的 .exe 文件,前 20 个全是二进制文件,加起来几十 MB。

找敏感信息#

Terminal window
# 搜索密码相关字符串
git log --all -p -- '*.json' '*.py' '*.env' | grep -E '(password|secret|token)' | head -30

发现了数据库密码、内部 IP、账号名等。


第三步:清理大文件#

用 BFG Repo-Cleaner(比 git-filter-branch 快得多):

Terminal window
# 下载 BFG
wget -O bfg.jar https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar
# 删除指定目录(整个文件夹从历史中彻底移除)
java -jar bfg.jar --delete-folders "wx859/" .
# 删除大于 10MB 的 blob
java -jar bfg.jar --strip-blobs-bigger-than 10M .
# 清理
git reflog expire --expire=now --all
git gc --prune=now --aggressive

第四步:替换敏感文本#

把密码、IP 等替换成占位符:

Terminal window
# 创建替换规则文件
cat > passwords.txt << 'EOF'
原数据库密码==>***REDACTED***
原内网IP==>x.x.x.x
原账号名==>***REDACTED***
EOF
# 执行替换
java -jar bfg.jar --replace-text passwords.txt .
# 再次清理
git reflog expire --expire=now --all
git gc --prune=now --aggressive

第五步:验证#

Terminal window
# 检查仓库大小
du -sh .
# 之前: 99MB
# 之后: 4.5MB
# 确认大文件已清除
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectsize) %(objectname) %(rest)' \
| sed -n 's/^blob //p' \
| sort -rnk2 \
| head -5
# 最大的文件应该已经不在了
# ⚠️ 不要用 git log -p | grep 来验证敏感信息是否清除
# Git diff 输出会转义特殊字符(如 \"),可能误判
# 用 blob-level 检查才准确
git cat-file -p <sha> | grep "原密码"

第六步:强制推送#

Terminal window
git push --force --all
git push --force --tags

⚠️ 强制推送会重写远程历史。所有协作者必须重新 clone,不能 merge 旧分支。


清理后的状态#

指标之前之后
仓库大小99MB4.5MB
大文件多个 30MB+ 的二进制
敏感信息明文密码/IP全部替换为 REDACTED
refs/replace/251 个(BFG 备份,正常)

踩过的坑#

1. refs/replace/ 会泄露旧 commit SHA#

BFG 会在 refs/replace/ 下创建备份 ref,指向被替换的旧 commit。虽然正常 git clone 不会下载这些 ref,但 git clone --mirror 会。旧 commit 的 SHA 本身不泄露内容,但知道 SHA 的人理论上可以从服务器拉取到旧对象。

解法:清理前在本地删掉 replace refs:

Terminal window
git for-each-ref --format='%(refname)' refs/replace/ | while read ref; do
git update-ref -d "$ref"
done

但即使不清理,replace ref 指向的是新(已清理的)commit,不是旧内容。旧 commit 变成悬空对象,正常 clone 拿不到。

2. git log -p 看到的不等于实际内容#

Git 的 diff 输出会转义引号(" 变成 \"),用 git log -p | grep 可能误判敏感信息是否还在。

验证一定要用 blob-level 检查

import subprocess
secret = b"原密码"
result = subprocess.run(['git', 'rev-list', '--objects', '--all'], capture_output=True, text=True)
for line in result.stdout.strip().split('\n'):
sha = line.split()[0]
content = subprocess.run(['git', 'cat-file', '-p', sha], capture_output=True).stdout
if secret in content:
print(f"LEAK: {sha}")

3. 远程旧对象可能长期存在#

Force push 后,旧 commit 变成悬空对象。GitHub/Gitee 不一定会自动 GC。正常 clone 拿不到旧对象,但如果有人知道旧 SHA,理论上可以从服务器拉取。

最保险的做法:清理后换一套密码/密钥。即使旧对象被拿到,凭证也已失效。


总结#

场景工具
删除大文件/目录BFG --delete-files / --delete-folders
按大小清理BFG --strip-blobs-bigger-than 50M
替换敏感文本BFG --replace-text
更精细的控制git-filter-repo

核心原则:备份 → 清理 → 验证(blob-level)→ force push → 通知协作者 re-clone → 换密码。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Git 历史清理实战:从 99MB 到 4.5MB
https://yfd5224.github.io/posts/git-history-cleanup/
作者
ddd
发布于
2026-05-01
许可协议
CC BY-NC-SA 4.0
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
270
分类
5
标签
960
总字数
210,898
运行时长
0
最后活动
0 天前

目录