Git 历史清理实战:从 99MB 到 4.5MB
背景
有一个老项目仓库,clone 下来要 99MB,但实际代码没多少。原因是历史提交里塞进了大量二进制文件(微信协议相关的 .exe、.dll),后来虽然删掉了,但 Git 的历史记录还保留着这些文件——git clone 会把所有历史 blob 都下载下来。
另外,之前的 commit 里硬编码了数据库密码、IP 地址、内部账号等信息,需要全部替换掉。
第一步:镜像克隆
普通 clone 只拉当前分支,镜像克隆拉取所有 refs,适合做历史重写:
git clone --mirror https://gitee.com/用户名/项目.git 项目-mirrorcd 项目-mirror第二步:分析仓库
找大文件
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。
找敏感信息
# 搜索密码相关字符串git log --all -p -- '*.json' '*.py' '*.env' | grep -E '(password|secret|token)' | head -30发现了数据库密码、内部 IP、账号名等。
第三步:清理大文件
用 BFG Repo-Cleaner(比 git-filter-branch 快得多):
# 下载 BFGwget -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 的 blobjava -jar bfg.jar --strip-blobs-bigger-than 10M .
# 清理git reflog expire --expire=now --allgit gc --prune=now --aggressive第四步:替换敏感文本
把密码、IP 等替换成占位符:
# 创建替换规则文件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 --allgit gc --prune=now --aggressive第五步:验证
# 检查仓库大小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 "原密码"第六步:强制推送
git push --force --allgit push --force --tags⚠️ 强制推送会重写远程历史。所有协作者必须重新 clone,不能 merge 旧分支。
清理后的状态
| 指标 | 之前 | 之后 |
|---|---|---|
| 仓库大小 | 99MB | 4.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:
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 subprocesssecret = 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 → 换密码。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!