本文使用claude code编写,旨在提供一个详尽的 git-filter-repo 使用指南,涵盖安装、基础概念、路径和内容过滤、实用场景、高级功能、安全机制、完整工作流示例、故障排除、性能优化、常见错误与陷阱、与其他工具配合以及最佳实践总结等方面的内容。
什么是 git-filter-repo?
git-filter-repo 是一个快速、功能强大的 Git 历史重写工具,是 git filter-branch 的官方推荐替代品。它由 Git 项目贡献者 Elijah Newren 开发,速度比 filter-branch 快 10-100 倍以上。
核心原理:
git fast-export <options> | filter | git fast-import <options>
git-filter-repo 同时扮演管道编排者和中间过滤器的角色,基于 fast-export 和 fast-import 实现高效的历史重写。
为什么选择 git-filter-repo?
vs git filter-branch
| 特性 | git filter-branch | git-filter-repo |
|---|---|---|
| 速度 | 极慢(大型仓库几乎不可用) | 快 10-100 倍 |
| 安全性 | 易出错,可能静默损坏数据 | 内置多重安全检查 |
| 易用性 | 复杂的 shell 语法 | 简洁的命令行参数 |
| 跨平台 | 依赖 shell,跨平台问题多 | 纯 Python,跨平台一致 |
| 官方态度 | 已废弃,不推荐使用 | Git 官方推荐 |
Git 官方警告: filter-branch 的问题无法向后兼容地修复,强烈建议停止使用。
filter-branch 的主要问题
- 性能问题: 对于非平凡的仓库,慢到几乎无法使用
- 安全陷阱: 充满各种陷阱,容易静默损坏仓库
- 使用困难: 即使是简单的重写也需要复杂的命令
- 跨平台问题: 依赖 shell 命令,在不同 OS 上行为不一致
- 容易混合新旧历史: 默认不处理所有分支,容易造成混乱
vs BFG Repo Cleaner
BFG 的限制:
- 只能基于文件名操作,无法区分不同路径的同名文件
- 不支持文件/目录重命名等复杂重构
- 默认不处理 HEAD,导致新旧历史不一致
- 功能相对单一
git-filter-repo 的优势:
- 操作完整路径,支持精确控制
- 支持重命名、移动、合并仓库等复杂操作
- 默认处理所有引用,避免新旧历史混合
- 提供完整的历史编辑能力
安装
方法 1: 直接下载(推荐)
# 下载单个 Python 脚本
wget https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo
chmod +x git-filter-repo
sudo mv git-filter-repo /usr/local/bin/
# 或者只放在当前项目
./git-filter-repo --version
方法 2: 包管理器
# macOS
brew install git-filter-repo
# Ubuntu/Debian
apt-get install git-filter-repo
# Python pip
pip install git-filter-repo
验证安装
git filter-repo --version
# 输出: git-filter-repo 2.x.x
系统要求:
- Git >= 2.22.0
- Python >= 3.5
基础概念
新鲜克隆检查 (Fresh Clone Check)
重要: git-filter-repo 默认只在"新鲜克隆"的仓库上运行,这是为了防止意外覆盖未备份的数据。
什么是新鲜克隆?
- 刚用
git clone克隆的仓库 - 没有未推送的本地提交
- 没有工作目录修改
- 没有配置多个远程仓库
- packed-refs 文件处于预期状态
绕过检查 (谨慎使用):
git filter-repo --force
⚠️ 强烈建议: 操作前先备份仓库!
默认行为
git-filter-repo 默认会:
- ✅ 处理所有分支和标签
- ✅ 删除
origin远程仓库引用(防止意外推送) - ✅ 将
refs/remotes/origin/*转换为refs/heads/* - ✅ 立即清理 reflog 和旧对象
- ✅ 运行垃圾回收 (gc) 缩小仓库
- ✅ 更新工作目录和索引(非裸仓库)
这些默认行为旨在防止混合新旧历史,提供更安全的操作环境。
路径过滤
1. 删除文件或目录
# 删除单个文件
git filter-repo --path secrets.txt --invert-paths
# 删除目录(注意末尾的斜杠)
git filter-repo --path logs/ --invert-paths
# 删除多个路径
git filter-repo \
--path passwords.txt \
--path config/database.yml \
--path temp/ \
--invert-paths
说明: --invert-paths 表示"反向选择",即删除指定路径,保留其他所有内容。
2. 只保留特定路径
# 只保留特定目录
git filter-repo --path src/ --path docs/
# 提取子目录作为新仓库根目录
git filter-repo --subdirectory-filter my-module/
3. 使用通配符
# 删除所有 .log 文件
git filter-repo --path-glob '*.log' --invert-paths
# 删除所有 node_modules 目录
git filter-repo --path-glob '**/node_modules/**' --invert-paths
# 删除所有 .DS_Store 文件
git filter-repo --path-glob '**/.DS_Store' --invert-paths
# 删除多种文件类型
git filter-repo \
--path-glob '*.log' \
--path-glob '*.tmp' \
--path-glob '*.cache' \
--invert-paths
4. 使用正则表达式
# 删除所有以 temp 开头的文件
git filter-repo --path-regex '^temp.*' --invert-paths
# 只保留 Java 源文件
git filter-repo --path-regex '.*\.java$'
# 删除特定模式的路径
git filter-repo --path-regex '^(build|dist|target)/' --invert-paths
5. 从文件读取路径列表
# 创建要保留的路径列表
cat > paths-to-keep.txt << EOF
src/
docs/
README.md
LICENSE
EOF
# 使用路径列表
git filter-repo --paths-from-file paths-to-keep.txt
# 创建要删除的路径列表
cat > paths-to-delete.txt << EOF
temp/
*.log
build/
EOF
# 删除列表中的路径
git filter-repo --invert-paths --paths-from-file paths-to-delete.txt
内容过滤
1. 替换文本
创建替换规则文件:
cat > replacements.txt << EOF
# 字面替换
password123==>***REMOVED***
old_api_key==>***REMOVED***
# 正则表达式替换(使用 regex: 前缀)
regex:api[_-]?key\s*=\s*['"][^'"]+['"]==>api_key="***REMOVED***"
# 全局替换(不区分大小写,使用 regex: 和 (?i))
regex:(?i)secret==>***REDACTED***
# 带捕获组的替换
regex:user_(\w+)@example\.com==>user_\1@redacted.com
EOF
应用替换:
git filter-repo --replace-text replacements.txt
2. 删除大文件
# 删除超过 50MB 的文件
git filter-repo --strip-blobs-bigger-than 50M
# 删除超过 10MB 的文件
git filter-repo --strip-blobs-bigger-than 10M
# 结合路径过滤
git filter-repo \
--strip-blobs-bigger-than 10M \
--path-glob '*.mp4' --invert-paths
3. 删除指定 Blob
# 1. 先分析仓库找出大 blob
git filter-repo --analyze
# 2. 查看分析结果
head -20 .git/filter-repo/analysis/blob-shas-and-paths.txt
# 3. 创建要删除的 blob ID 列表
cat > blob-ids.txt << EOF
a1b2c3d4...
e5f6g7h8...
EOF
# 4. 删除指定的 blob
git filter-repo --strip-blobs-with-ids blob-ids.txt
日常实用场景
场景 1: 误上传大文件
这是最常见的问题。假设误提交了 videos/demo.mp4 (500MB):
# 1. 备份(非常重要!)
git clone https://github.com/user/repo.git repo-backup
# 2. 新克隆一份用于清理
git clone https://github.com/user/repo.git repo-clean
cd repo-clean
# 3. 删除大文件
git filter-repo --path videos/demo.mp4 --invert-paths
# 4. 验证结果
du -sh .git
git log --all --oneline | head -10
# 5. 重新添加远程仓库(filter-repo 会删除 origin)
git remote add origin https://github.com/user/repo.git
# 6. 强制推送
git push origin --force --all
git push origin --force --tags
场景 2: 同时删除多个大文件(大文件和代码混合)
# 1. 先分析仓库,找出问题文件
git filter-repo --analyze
# 2. 查看最大的文件
echo "=== 最大的 20 个文件 ==="
head -20 .git/filter-repo/analysis/path-all-sizes.txt
# 3. 批量删除大文件,保留代码
git filter-repo \
--path videos/training_video.mp4 \
--path data/dataset.csv \
--path backups/old_backup.tar.gz \
--path-glob '*.zip' \
--path-glob '*.tar.gz' \
--strip-blobs-bigger-than 10M \
--invert-paths
# 4. 再次验证
git filter-repo --analyze
head -10 .git/filter-repo/analysis/path-all-sizes.txt
场景 3: 删除敏感信息
# 方法 1: 完全删除包含密码的文件
git filter-repo --path config/secrets.yml --invert-paths
# 方法 2: 替换敏感内容
cat > sensitive.txt << EOF
admin_password==>***REMOVED***
api_key_prod_12345==>***REMOVED***
regex:password\s*=\s*['"][^'"]+['"]==>password="***REMOVED***"
regex:secret_key\s*:\s*.+==>secret_key: "***REDACTED***"
EOF
git filter-repo --replace-text sensitive.txt
# 方法 3: 删除文件并替换内容(组合使用)
git filter-repo \
--path .env --invert-paths \
--replace-text sensitive.txt
场景 4: 清理依赖目录
# 删除所有 node_modules
git filter-repo --path-glob '**/node_modules/**' --invert-paths
# 删除多种依赖目录
git filter-repo \
--path-glob '**/node_modules/**' \
--path-glob '**/vendor/**' \
--path-glob '**/.venv/**' \
--path-glob '**/venv/**' \
--path-glob '**/target/**' \
--path-glob '**/__pycache__/**' \
--invert-paths
场景 5: 提取子项目
# 将 services/auth 提取为独立仓库
git filter-repo --subdirectory-filter services/auth/
# 提取子目录但保持在特定路径下
# 例如:保留 src/components/ 但将其移到 old/legacy/
git filter-repo \
--path src/components/ \
--path-rename src/components/:old/legacy/components/
场景 6: 批量删除文件列表
# 创建要删除的文件列表
cat > files-to-delete.txt << EOF
temp/cache.db
logs/debug.log
uploads/test-image.jpg
data/old-export.sql
videos/demo.mp4
EOF
# 批量删除
git filter-repo --invert-paths --paths-from-file files-to-delete.txt
从其他工具迁移
从 filter-branch 迁移
示例 1: 删除文件
# filter-branch 方式(慢且危险)
git filter-branch --index-filter \
'git rm --cached --ignore-unmatch secrets.txt' \
--tag-name-filter cat -- --all
# filter-repo 方式(快速且安全)
git filter-repo --path secrets.txt --invert-paths
示例 2: 修改提交作者
# filter-branch 方式
git filter-branch --env-filter '
if test "$GIT_AUTHOR_EMAIL" = "old@example.com"
then
GIT_AUTHOR_EMAIL="new@example.com"
GIT_COMMITTER_EMAIL="new@example.com"
fi' -- --all
# filter-repo 方式
git filter-repo \
--email-callback 'return email.replace(b"old@example.com", b"new@example.com")'
# 或使用 mailmap 文件
cat > mailmap.txt << EOF
New Name <new@example.com> <old@example.com>
EOF
git filter-repo --mailmap mailmap.txt
示例 3: 移动所有内容到子目录
# filter-branch 方式(慢且跨平台问题)
git filter-branch --index-filter \
'git ls-files -s |
sed "s-\t\"*-&newsubdir/-" |
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info &&
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD
# filter-repo 方式(简单快速)
git filter-repo --to-subdirectory-filter newsubdir/
从 BFG Repo Cleaner 迁移
示例 1: 删除大文件
# BFG 方式
java -jar bfg.jar --strip-blobs-bigger-than 100M repo.git
# filter-repo 方式
git filter-repo --strip-blobs-bigger-than 100M
示例 2: 删除特定文件
# BFG 方式
java -jar bfg.jar --delete-files passwords.txt repo.git
# filter-repo 方式
git filter-repo --path passwords.txt --invert-paths
示例 3: 替换文本
# BFG 方式
java -jar bfg.jar --replace-text replacements.txt repo.git
# filter-repo 方式
git filter-repo --replace-text replacements.txt
关键区别
| 操作 | BFG | filter-repo |
|---|---|---|
| 默认行为 | 不处理 HEAD | 处理所有提交 |
| 路径识别 | 仅基于文件名 | 完整路径 |
| 重命名 | 不支持 | 完全支持 |
| 提交消息 | 添加 Former-commit-id | 使用 replace refs |
重要: BFG 默认不处理 HEAD,这会导致新旧历史不一致。filter-repo 会处理所有提交,避免这个问题。
高级功能
1. 重命名和移动
# 重命名目录
git filter-repo --path-rename old-name/:new-name/
# 移动文件到子目录(所有文件前加前缀)
git filter-repo --path-rename '':'subdir/'
# 移动子目录到根目录
git filter-repo --to-subdirectory-filter my-module
# 多个重命名操作
git filter-repo \
--path-rename src/:lib/ \
--path-rename tests/:test/ \
--path-rename README.txt:README.md
# 复杂路径重组
git filter-repo \
--path old/location/module/ \
--path-rename old/location/module/:new/place/
2. 修改提交信息
# 替换提交消息中的文本
git filter-repo \
--message-callback 'return message.replace(b"JIRA-123", b"ISSUE-456")'
# 在每个提交消息前添加前缀
git filter-repo \
--message-callback 'return b"[MIGRATED] " + message'
# 删除提交消息中的特定行
git filter-repo \
--message-callback '
lines = message.split(b"\n")
lines = [l for l in lines if not l.startswith(b"Signed-off-by:")]
return b"\n".join(lines)
'
3. 修改作者和邮箱
# 使用 mailmap 文件
cat > mailmap.txt << EOF
Correct Name <correct@email.com> <old@email.com>
Correct Name <correct@email.com> Old Name <old@email.com>
Another Dev <dev@company.com> <dev@oldcompany.com>
EOF
git filter-repo --mailmap mailmap.txt
# 使用回调函数修改邮箱
git filter-repo \
--email-callback 'return email.replace(b"@oldcompany.com", b"@newcompany.com")'
# 修改名字
git filter-repo \
--name-callback 'return name.replace(b"OldName", b"NewName")'
4. 修改标签
# 为所有标签添加前缀
git filter-repo --tag-rename '':'v'
# 重命名特定模式的标签
git filter-repo --tag-rename 'release-':'v'
# 删除所有标签
git filter-repo --tag-rename '':''
5. 使用回调函数 (Callbacks)
文件名回调
# 删除所有 .DS_Store 文件
git filter-repo --filename-callback '
import os
return None if os.path.basename(filename) == b".DS_Store" else filename
'
# 将所有文件名转为小写
git filter-repo --filename-callback 'return filename.lower()'
# 标准化文件名编码(NFD to NFC)
git filter-repo --filename-callback '
import unicodedata
return unicodedata.normalize("NFC", filename.decode("utf-8")).encode("utf-8")
'
Blob 回调
# 在所有 Python 文件开头添加版权声明
git filter-repo --blob-callback '
if filename.endswith(b".py"):
copyright = b"# Copyright 2024 Your Company\n# Licensed under MIT\n\n"
blob.data = copyright + blob.data
'
# 替换文件中的特定内容
git filter-repo --blob-callback '
if filename == b"config.yml":
blob.data = blob.data.replace(b"localhost", b"production.server.com")
'
提交回调
# 在第一个提交中添加 LICENSE 文件
git filter-repo --commit-callback '
if not commit.parents:
from git_filter_repo import FileChange, Blob
license_content = open("LICENSE", "rb").read()
license_blob = Blob(license_content)
commit.file_changes.append(
FileChange(b"M", b"LICENSE", license_blob.id, b"100644")
)
'
# 修改提交时间
git filter-repo --commit-callback '
# 将所有提交时间延后一年
commit.author_date = commit.author_date + b" +31536000"
commit.committer_date = commit.committer_date + b" +31536000"
'
6. 部分历史重写 (–partial)
# 只处理特定分支
git filter-repo --refs refs/heads/feature-branch --partial
# 只处理特定提交范围
git filter-repo --refs main~10..main --partial
# 处理多个分支
git filter-repo \
--refs refs/heads/feature-a \
--refs refs/heads/feature-b \
--partial
⚠️ 注意: --partial 会保留新旧历史的混合,使用需谨慎。
7. 组合多个操作
# 复杂的清理操作
git filter-repo \
--path secret.key --invert-paths \
--path-glob '*.log' --invert-paths \
--strip-blobs-bigger-than 10M \
--replace-text replacements.txt \
--path-rename old/:new/ \
--mailmap mailmap.txt
# 分步执行(推荐复杂操作)
git filter-repo --path secret.key --invert-paths
git filter-repo --strip-blobs-bigger-than 10M --force
git filter-repo --replace-text replacements.txt --force
真实用户案例
以下是从 GitHub Issues 中收集的真实用户场景和解决方案。
案例 1: 在第一个提交中添加文件
场景: 需要在历史的第一个提交中添加 README.md 和 .gitignore。
# 方法 1: 使用 commit-callback
git filter-repo --commit-callback "
if not commit.parents:
from git_filter_repo import FileChange
import subprocess
readme_id = subprocess.check_output(['git', 'hash-object', '-w', 'README.md']).strip()
gitignore_id = subprocess.check_output(['git', 'hash-object', '-w', '.gitignore']).strip()
commit.file_changes += [
FileChange(b'M', b'README.md', readme_id, b'100644'),
FileChange(b'M', b'.gitignore', gitignore_id, b'100644')
]
"
# 方法 2: 使用 contrib 脚本
mv /path/to/README.md README.md
mv /path/to/.gitignore .gitignore
insert-beginning --file README.md
insert-beginning --file .gitignore
案例 2: 处理包含特殊字符的文件名
场景: 文件名包含重音符号等特殊字符。
# Mac 的 UTF-8 标准化问题(NFD to NFC)
git filter-repo --filename-callback '
import unicodedata
normalized = unicodedata.normalize("NFC", filename.decode("utf-8"))
return normalized.encode("utf-8")
'
案例 3: 保留子目录但移到新位置
场景: 保留 src/some-folder/some-feature/,但将其移到 new/path/。
git filter-repo \
--path src/some-folder/some-feature/ \
--path-rename src/some-folder/some-feature/:new/path/
案例 4: 拆分单体仓库
场景: 将单体仓库拆分成多个独立仓库。
# 提取 service-a
git clone monorepo.git service-a
cd service-a
git filter-repo --subdirectory-filter services/service-a/
git remote add origin https://github.com/company/service-a.git
git push origin --force --all
# 提取 service-b
git clone monorepo.git service-b
cd service-b
git filter-repo --subdirectory-filter services/service-b/
git remote add origin https://github.com/company/service-b.git
git push origin --force --all
案例 5: 合并多个仓库
场景: 将多个仓库合并到单一仓库,避免路径冲突。
# 准备 repo-a
git clone repo-a.git
cd repo-a
git filter-repo --to-subdirectory-filter project-a/
cd ..
# 准备 repo-b
git clone repo-b.git
cd repo-b
git filter-repo --to-subdirectory-filter project-b/
cd ..
# 合并
git clone repo-a.git merged-repo
cd merged-repo
git remote add repo-b ../repo-b
git fetch repo-b
git merge --allow-unrelated-histories repo-b/main
git remote remove repo-b
案例 6: 移动文件但保留部分在原位置
场景: 将所有文件移到 engine/,但保留 .gitignore 和 .gitattributes 在根目录。
# 第一步:移动所有文件到 engine/
git filter-repo --to-subdirectory-filter engine/
# 第二步:移回特定文件
git filter-repo \
--path-rename engine/.gitignore:.gitignore \
--path-rename engine/.gitattributes:.gitattributes \
--force
# 如果遇到 replace ref 问题
git replace -d <problematic-ref-id>
案例 7: 删除文件类型但保留特定实例
场景: 删除所有 .xsa 文件,但保留 important.xsa。
# 创建路径列表文件
cat > keep-files.txt << EOF
important.xsa
EOF
# 删除其他 .xsa 文件
git filter-repo \
--path-glob '*.xsa' \
--paths-from-file keep-files.txt \
--invert-paths
案例 8: 修正错误的作者信息
场景: 历史中有多个错误的邮箱地址需要统一修正。
# 创建 mailmap 文件
cat > mailmap.txt << EOF
John Doe <john.doe@company.com> <john@old-company.com>
John Doe <john.doe@company.com> <j.doe@freelance.com>
Jane Smith <jane.smith@company.com> <jane@gmail.com>
EOF
git filter-repo --mailmap mailmap.txt
案例 9: 移除整个子模块及其历史
场景: 移除 Git 子模块及其所有历史记录。
# 删除子模块目录和 .gitmodules 文件
git filter-repo \
--path third-party/submodule/ --invert-paths \
--path .gitmodules --invert-paths
案例 10: 标准化行尾符
场景: 将仓库中所有文本文件的行尾符统一为 LF。
git filter-repo --blob-callback '
if not filename.endswith(b".jpg") and not filename.endswith(b".png"):
blob.data = blob.data.replace(b"\r\n", b"\n")
'
分析功能
生成分析报告
git filter-repo --analyze
这会在 .git/filter-repo/analysis/ 目录生成多个报告:
主要报告文件
- blob-shas-and-paths.txt - 所有 blob 的 SHA 和路径
- path-all-sizes.txt - 按大小排序的所有路径(含历史版本)
- path-deleted-sizes.txt - 已删除文件的大小统计
- extensions-all-sizes.txt - 按扩展名统计的大小
- directories-all-sizes.txt - 按目录统计的大小
- renames.txt - 检测到的文件重命名
实用分析命令
# 查看最大的 20 个文件
head -20 .git/filter-repo/analysis/path-all-sizes.txt
# 查看已删除的最大文件
head -20 .git/filter-repo/analysis/path-deleted-sizes.txt
# 查看最占空间的扩展名
head -10 .git/filter-repo/analysis/extensions-all-sizes.txt
# 查看最大的目录
head -10 .git/filter-repo/analysis/directories-all-sizes.txt
# 找出超过 10MB 的文件
awk '$1 > 10485760' .git/filter-repo/analysis/path-all-sizes.txt
这些报告帮助你决定要过滤什么内容。
安全机制
1. 新鲜克隆检查
git-filter-repo 检查多个条件来判断是否为新鲜克隆:
- 是否有未推送的本地提交
- 是否有工作目录修改
- 是否有多个远程仓库
- packed-refs 文件的状态
- 是否有未跟踪的重要文件
为什么需要这个检查?
防止你在没有备份的仓库上运行,导致数据无法恢复。
2. 防止混合新旧历史
# 默认删除 origin 引用(防止意外推送)
# 如果需要保留 origin
git filter-repo --preserve-commit-hashes
# 或者使用 --partial 但要小心
git filter-repo --refs main --partial
3. 强制推送前的确认
# 推荐使用 --force-with-lease 而不是 --force
git push origin --force-with-lease --all
# 这可以防止覆盖别人的推送
4. 备份策略
# 方法 1: 镜像克隆
git clone --mirror original-repo.git backup.git
# 方法 2: 导出 bundle
git bundle create backup.bundle --all
# 方法 3: 简单克隆
git clone original-repo.git backup-repo
# 恢复备份
git clone backup.git restored-repo
完整工作流示例
清理大文件和敏感数据的完整流程
#!/bin/bash
# 完整的仓库清理脚本
# === 第 1 步: 准备工作 ===
REPO_URL="https://github.com/user/repo.git"
BACKUP_DIR="repo-backup"
CLEAN_DIR="repo-clean"
# 1.1 创建镜像备份
echo "=== 创建备份 ==="
git clone --mirror $REPO_URL $BACKUP_DIR.git
# 1.2 新克隆用于清理
echo "=== 克隆仓库用于清理 ==="
git clone $REPO_URL $CLEAN_DIR
cd $CLEAN_DIR
# === 第 2 步: 分析仓库 ===
echo "=== 分析仓库 ==="
git filter-repo --analyze
echo ""
echo "=== 最大的 20 个文件 ==="
head -20 .git/filter-repo/analysis/path-all-sizes.txt
echo ""
echo "=== 按扩展名统计 ==="
head -20 .git/filter-repo/analysis/extensions-all-sizes.txt
echo ""
echo "=== 当前仓库大小 ==="
du -sh .git
# === 第 3 步: 准备清理规则 ===
# 创建敏感信息替换规则
cat > replace.txt << 'EOF'
# API Keys
api_key_12345==>***REMOVED***
prod_secret_abc==>***REMOVED***
# 正则表达式
regex:password\s*=\s*['"][^'"]+['"]==>password="***REMOVED***"
regex:token\s*:\s*.+==>token: "***REDACTED***"
EOF
# 创建要删除的路径列表
cat > paths-to-delete.txt << 'EOF'
uploads/presentation.pptx
data/training-data.zip
backups/old_backup.tar.gz
EOF
# === 第 4 步: 执行清理 ===
echo ""
echo "=== 执行清理操作 ==="
git filter-repo \
--invert-paths --paths-from-file paths-to-delete.txt \
--path-glob '*.mp4' --invert-paths \
--path-glob '*.mov' --invert-paths \
--path-glob '**/node_modules/**' --invert-paths \
--path-glob '**/.venv/**' --invert-paths \
--strip-blobs-bigger-than 50M \
--replace-text replace.txt
# === 第 5 步: 验证结果 ===
echo ""
echo "=== 清理后的仓库大小 ==="
du -sh .git
echo ""
echo "=== 再次分析验证 ==="
git filter-repo --analyze --force
echo "最大文件:"
head -10 .git/filter-repo/analysis/path-all-sizes.txt
# === 第 6 步: 推送到远程 ===
echo ""
echo "=== 准备推送 ==="
read -p "确认要推送到远程仓库吗? (yes/no): " confirm
if [ "$confirm" == "yes" ]; then
git remote add origin $REPO_URL
echo "推送所有分支和标签..."
git push origin --force --all
git push origin --force --tags
echo ""
echo "=== 完成! ==="
echo "请通知所有团队成员:"
echo "1. 重新克隆仓库: git clone $REPO_URL"
echo "2. 或重置现有克隆: git fetch origin && git reset --hard origin/main"
else
echo "取消推送。可以继续在本地验证。"
fi
# === 清理说明 ===
cat << 'EOF'
=== 团队成员需要执行的操作 ===
选项 1: 重新克隆(推荐)
cd /path/to/projects
rm -rf old-repo
git clone https://github.com/user/repo.git
选项 2: 重置现有克隆
cd existing-repo
git fetch origin
git reset --hard origin/main
git clean -fd
git gc --prune=now --aggressive
EOF
故障排除
问题 1: “not a fresh clone” 错误
# 解决方案 1: 新克隆仓库(推荐)
cd ..
git clone <repo-url> fresh-clone
cd fresh-clone
# 解决方案 2: 使用 --force (确保有备份!)
git filter-repo --force ...
# 解决方案 3: 检查是否有未推送的提交
git log @{u}.. # 查看未推送的提交
git push # 先推送再操作
问题 2: 推送被拒绝
# 问题: remote: error: denying non-fast-forward refs/heads/main
# 解决方案 1: 确保有权限
# 检查分支保护规则,可能需要临时禁用
# 解决方案 2: 使用 --force-with-lease(更安全)
git push origin --force-with-lease --all
git push origin --force-with-lease --tags
# 解决方案 3: 如果确定要覆盖
git push origin --force --all
git push origin --force --tags
问题 3: 仓库大小没有减小
# 问题: 删除文件后 .git 目录仍然很大
# 解决方案: 确保完全清理
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# 删除 remote tracking 分支
git for-each-ref --format="%(refname)" refs/remotes/ | \
xargs -n 1 git update-ref -d
# 删除 filter-repo 的 replace refs
git for-each-ref --format="%(refname)" refs/replace/ | \
xargs -n 1 git update-ref -d
# 强制重新打包
rm -rf .git/refs/original/
git -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 \
-c gc.rerereresolved=0 -c gc.rerereunresolved=0 \
-c gc.pruneExpire=now gc --aggressive
问题 4: replace refs 警告
# 问题: WARNING: Ref 'refs/replace/...' is a replace ref. Skipping...
# 原因: 多次运行 filter-repo 产生的 replace refs
# 解决方案 1: 删除所有 replace refs
git for-each-ref --format="%(refname)" refs/replace/ | \
xargs -n 1 git update-ref -d
# 解决方案 2: 使用 --replace-refs delete
git filter-repo --replace-refs delete-no-add ...
问题 5: 某些分支没有被处理
# 问题: 只有部分分支被重写
# 原因: 默认只处理所有本地分支
# 解决方案: 明确指定要处理的引用
git filter-repo --refs refs/heads/ --refs refs/tags/
# 或处理所有引用
git filter-repo --refs refs/
问题 6: 文件名编码问题
# 问题: 包含特殊字符的文件名在不同系统上显示不同
# Mac 的 NFD vs NFC 问题
git filter-repo --filename-callback '
import unicodedata
return unicodedata.normalize("NFC", filename.decode("utf-8")).encode("utf-8")
'
# Windows 路径分隔符问题
git filter-repo --filename-callback '
return filename.replace(b"\\\\", b"/")
'
性能优化
1. 大型仓库优化
# 使用 --prune-empty 删除空提交
git filter-repo --prune-empty=always ...
# 使用 --prune-degenerate 删除退化的合并提交
git filter-repo --prune-degenerate=always ...
# 组合使用
git filter-repo \
--prune-empty=always \
--prune-degenerate=always \
...
2. 减少内存使用
# 对于非常大的仓库,分批处理
# 第一步:删除最大的文件
git filter-repo --strip-blobs-bigger-than 100M
# 第二步:删除中等文件
git filter-repo --strip-blobs-bigger-than 50M --force
# 第三步:删除小文件
git filter-repo --strip-blobs-bigger-than 10M --force
3. 加速 CI/CD
# 在 CI 环境中使用浅克隆
git clone --depth 1 repo.git
# 如果需要完整历史但想加速
git clone --filter=blob:none repo.git
常见错误和陷阱
❌ 错误做法
# 1. 不要在主仓库上直接运行
cd production-repo
git filter-repo ... # 危险!
# 2. 不要忘记 --invert-paths
git filter-repo --path secret.txt # 这会只保留 secret.txt,删除其他!
# 3. 不要混合新旧历史
git filter-repo --refs feature-branch # 没有 --partial 会很危险
# 4. 不要在有本地修改时运行
git filter-repo ... # 有未提交的更改
# 5. 不要忘记推送标签
git push origin --force --all # 忘记了标签
✅ 正确做法
# 1. 总是先备份
git clone --mirror original.git backup.git
# 2. 使用新克隆
git clone original.git clean-repo && cd clean-repo
# 3. 先分析后操作
git filter-repo --analyze
# 查看分析结果,制定清理计划
# 4. 验证结果
git filter-repo ...
git log --all --oneline | head -20
du -sh .git
# 5. 完整推送
git remote add origin <url>
git push origin --force --all
git push origin --force --tags
与其他工具配合
配合 Git LFS
# 场景:已经使用 Git LFS,现在要清理历史
# 1. 识别孤立的 LFS 对象
git filter-repo --analyze
cat .git/filter-repo/analysis/lfs-objects.txt
# 2. 清理后,处理孤立的 LFS 对象
git filter-repo ...
git lfs prune
# 3. 如果要将大文件迁移到 LFS
# 先清理历史,再用 LFS track
git filter-repo --strip-blobs-bigger-than 10M
git lfs track "*.psd"
配合 GitHub/GitLab
# GitHub: 禁用分支保护后清理
# 1. Settings -> Branches -> 临时禁用保护
# 2. 清理并推送
git filter-repo ...
git push origin --force --all --tags
# 3. 重新启用保护
# GitLab: 使用 Housekeeping
# 清理后在 GitLab UI 执行:
# Settings -> Repository -> Repository maintenance -> Run housekeeping
配合 Monorepo 工具
# 从 Monorepo 提取子项目配合 Bazel/Nx 等工具
# 提取 packages/ui
git filter-repo --subdirectory-filter packages/ui/
# 保留构建配置
git filter-repo \
--path packages/ui/ \
--path-rename packages/ui/: \
--path BUILD.bazel
最佳实践
✅ 操作前
- 备份: 使用
git clone --mirror创建完整备份 - 分析: 运行
--analyze了解仓库结构和大文件 - 计划: 制定详细的清理计划,列出所有要删除/修改的内容
- 测试: 在小仓库或测试分支上先试验
- 通知: 提前通知所有协作者,选择合适的时间窗口
✅ 操作中
- 新克隆: 在新克隆的仓库中操作,不要在工作仓库
- 一次性: 尽量一次完成所有过滤,避免多次重写历史
- 验证: 每步操作后检查结果是否符合预期
- 记录: 保存使用的命令,方便问题排查和团队参考
✅ 操作后
- 再次分析: 确认大文件已删除,敏感信息已清理
- 功能测试: 验证仓库可以正常 clone、build、test
- 完整推送: 推送所有分支和标签
- 团队协调: 确保所有人重新克隆或重置仓库
- 文档更新: 更新项目文档,说明历史已重写
✅ 长期维护
- 预防为主: 使用 .gitignore 防止大文件和敏感信息入库
- Git Hooks: 配置 pre-commit hooks 检查文件大小
- 定期检查: 定期运行
--analyze检查仓库健康度 - 教育团队: 培训团队成员正确使用 Git
参考资源
官方文档
- Git Filter-Repo 主页: https://github.com/newren/git-filter-repo
- 手册页:
man git-filter-repo - 在线文档: HTML Manual
转换指南
社区资源
快速参考
常用命令速查
# === 分析 ===
git filter-repo --analyze
# === 删除路径 ===
# 删除文件
git filter-repo --path FILE --invert-paths
# 删除目录
git filter-repo --path DIR/ --invert-paths
# 使用通配符
git filter-repo --path-glob '*.log' --invert-paths
# 使用正则
git filter-repo --path-regex '^temp' --invert-paths
# === 保留路径 ===
git filter-repo --path DIR/
git filter-repo --subdirectory-filter DIR/
# === 内容处理 ===
# 删除大文件
git filter-repo --strip-blobs-bigger-than 10M
# 替换文本
git filter-repo --replace-text FILE
# === 重命名 ===
git filter-repo --path-rename OLD:NEW
git filter-repo --to-subdirectory-filter DIR
# === 修改元数据 ===
git filter-repo --mailmap FILE
git filter-repo --email-callback 'CODE'
git filter-repo --message-callback 'CODE'
# === 组合操作 ===
git filter-repo \
--path secret.txt --invert-paths \
--strip-blobs-bigger-than 10M \
--replace-text replacements.txt \
--mailmap mailmap.txt
推送模板
# === 标准推送流程 ===
# 1. 重新添加 origin
git remote add origin <URL>
# 2. 验证远程
git remote -v
# 3. 强制推送(谨慎!)
git push origin --force --all
git push origin --force --tags
# 4. 更安全的推送
git push origin --force-with-lease --all
git push origin --force-with-lease --tags
团队协作模板
# === 团队成员操作指南 ===
# 方案 A: 重新克隆(最简单最安全)
cd ~/projects
rm -rf old-repo
git clone https://github.com/company/repo.git
cd repo
# 方案 B: 重置现有克隆
cd existing-repo
git fetch origin
git reset --hard origin/main
git clean -fd
git gc --prune=now --aggressive
# 方案 C: 保留本地更改
git stash
git fetch origin
git reset --hard origin/main
git stash pop
故障恢复模板
# === 如果出错需要恢复 ===
# 从备份恢复
cd /path/to/backup.git
git push --mirror https://github.com/company/repo.git
# 或从 bundle 恢复
git clone backup.bundle restored-repo
cd restored-repo
git remote add origin https://github.com/company/repo.git
git push origin --force --all --tags
结语
git-filter-repo 是一个强大而精密的工具。记住:
- 历史重写是不可逆的 - 务必备份
- 影响所有协作者 - 充分沟通和协调
- 需要强制推送 - 确保有权限
- 一次做对 - 仔细计划,避免多次重写
当正确使用时,git-filter-repo 能够:
- 有效清理大文件和敏感信息
- 重组仓库结构
- 拆分或合并仓库
- 修正历史错误
掌握这个工具,让你的 Git 仓库保持健康、高效!