本文使用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-exportfast-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 的主要问题

  1. 性能问题: 对于非平凡的仓库,慢到几乎无法使用
  2. 安全陷阱: 充满各种陷阱,容易静默损坏仓库
  3. 使用困难: 即使是简单的重写也需要复杂的命令
  4. 跨平台问题: 依赖 shell 命令,在不同 OS 上行为不一致
  5. 容易混合新旧历史: 默认不处理所有分支,容易造成混乱

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/ 目录生成多个报告:

主要报告文件

  1. blob-shas-and-paths.txt - 所有 blob 的 SHA 和路径
  2. path-all-sizes.txt - 按大小排序的所有路径(含历史版本)
  3. path-deleted-sizes.txt - 已删除文件的大小统计
  4. extensions-all-sizes.txt - 按扩展名统计的大小
  5. directories-all-sizes.txt - 按目录统计的大小
  6. 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

最佳实践

✅ 操作前

  1. 备份: 使用 git clone --mirror 创建完整备份
  2. 分析: 运行 --analyze 了解仓库结构和大文件
  3. 计划: 制定详细的清理计划,列出所有要删除/修改的内容
  4. 测试: 在小仓库或测试分支上先试验
  5. 通知: 提前通知所有协作者,选择合适的时间窗口

✅ 操作中

  1. 新克隆: 在新克隆的仓库中操作,不要在工作仓库
  2. 一次性: 尽量一次完成所有过滤,避免多次重写历史
  3. 验证: 每步操作后检查结果是否符合预期
  4. 记录: 保存使用的命令,方便问题排查和团队参考

✅ 操作后

  1. 再次分析: 确认大文件已删除,敏感信息已清理
  2. 功能测试: 验证仓库可以正常 clone、build、test
  3. 完整推送: 推送所有分支和标签
  4. 团队协调: 确保所有人重新克隆或重置仓库
  5. 文档更新: 更新项目文档,说明历史已重写

✅ 长期维护

  1. 预防为主: 使用 .gitignore 防止大文件和敏感信息入库
  2. Git Hooks: 配置 pre-commit hooks 检查文件大小
  3. 定期检查: 定期运行 --analyze 检查仓库健康度
  4. 教育团队: 培训团队成员正确使用 Git

参考资源

官方文档

转换指南

社区资源


快速参考

常用命令速查

# === 分析 ===
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 是一个强大而精密的工具。记住:

  1. 历史重写是不可逆的 - 务必备份
  2. 影响所有协作者 - 充分沟通和协调
  3. 需要强制推送 - 确保有权限
  4. 一次做对 - 仔细计划,避免多次重写

当正确使用时,git-filter-repo 能够:

  • 有效清理大文件和敏感信息
  • 重组仓库结构
  • 拆分或合并仓库
  • 修正历史错误

掌握这个工具,让你的 Git 仓库保持健康、高效!