跳转到内容
关系图谱

git filter-repo

4,667 字 18 分钟

属性 1
status
学习中

git filter-repo

git filter-repo 是 Git 官方推荐的历史重写工具,用于替代已弃用的 Git filter-branch 详解 和 BFG Repo Cleaner。它基于 git fast-export / git fast-import 管道工作,速度极快,且在设计上规避了 filter-branch 的众多陷阱。

官方推荐

Git 项目官方在 git filter-branch 文档中明确推荐使用 git filter-repo 作为替代方案。

安装

系统要求

  • Git ≥ 2.36.0
  • Python ≥ 3.6

安装方式

bash
# pip / pipx(推荐)
pip install git-filter-repo
# 或
pipx install git-filter-repo

# macOS(Homebrew)
brew install git-filter-repo

# Windows(Scoop)
scoop install git-filter-repo

# 手动安装:下载单文件脚本放入 $PATH
# https://github.com/newren/git-filter-repo/releases
单文件脚本

git-filter-repo 本质是一个单独的 Python 脚本文件,你也可以直接下载放到 PATH 中即可使用。

核心概念

Git filter-branch 详解 逐个检出提交不同,filter-repo 通过 fast-export 将整个仓库历史序列化为流,在内存中高效处理后,再通过 fast-import 写入新历史。这使得它的速度比 filter-branch 快 10-100 倍

路径过滤

保留指定路径

bash
# 只保留特定文件和目录
git filter-repo --path README.md --path guides/ --path tools/releases

# 保留匹配 glob 模式的路径
git filter-repo --path-glob '*.py'

# 保留匹配正则表达式的路径
git filter-repo --path-regex '^src/.*\.ts$'

排除指定路径

bash
# 删除指定文件(反转路径选择)
git filter-repo --path config/secrets.yml --invert-paths

# 删除匹配模式的文件
git filter-repo --path-glob '*.log' --invert-paths

从文件读取路径

bash
# paths.txt 每行一个路径
git filter-repo --paths-from-file paths.txt
对比 filter-branch

等效于 git filter-branch --index-filter 'git rm --cached ...',但语法更简洁、速度更快、无需担心边界情况。

内容替换

替换文本

bash
# 将所有文件中的旧文本替换为新文本
git filter-repo --replace-text expressions.txt

expressions.txt 格式:

# 字面替换
literal:old_password==>***REMOVED***

# 正则替换
regex:password\s*=\s*"[^"]*"==>password="***REMOVED***"

# 删除匹配行(替换为空)
literal:SECRET_KEY==>

删除大文件

bash
# 删除所有大于 100MB 的文件
git filter-repo --strip-blobs-bigger-than 100M

# 删除所有大于 10MB 的文件
git filter-repo --strip-blobs-bigger-than 10M

作者信息重写

使用 mailmap 文件

bash
# 应用 mailmap 文件重写作者信息
git filter-repo --mailmap my-mailmap

# 使用仓库中已有的 .mailmap
git filter-repo --use-mailmap

my-mailmap 文件格式:

# 新名字 <新邮箱> <旧邮箱>
Zhang San <zhangsan@newmail.com> <root@localhost>

# 只改名字
Zhang San <zhangsan@newmail.com> Old Name <zhangsan@newmail.com>

# 只改邮箱
<zhangsan@newmail.com> <old@email.com>
对比 filter-branch

等效于 git filter-branch --env-filter '...',但只需一个简单的 mailmap 文件,无需编写复杂的 shell 脚本。

高级回调

filter-repo 提供了 Python 回调函数,可以对 Git 对象进行任意处理。

--blob-callback:处理文件内容

bash
# 删除超过 1MB 的 blob
git filter-repo --blob-callback '
    if len(blob.data) > 1024*1024:
        blob.skip()
'

# 替换文件中的文本
git filter-repo --blob-callback '
    blob.data = blob.data.replace(b"old_text", b"new_text")
'

--commit-callback:处理提交

bash
# 跳过特定作者的提交
git filter-repo --commit-callback '
    if commit.author_name == b"Bad Author":
        commit.skip()
'

# 修改提交信息
git filter-repo --commit-callback '
    commit.message = commit.message.replace(b"typo", b"fixed")
'

--filename-callback:重命名文件

bash
# 将 src/ 目录移到 lib/ 下
git filter-repo --filename-callback '
    if filename.startswith(b"src/"):
        return b"lib/" + filename
    return filename
'

其他回调

回调说明
--tag-callback处理标签对象
--message-callback处理提交/标签信息
--name-callback处理作者/提交者名字
--email-callback处理作者/提交者邮箱
--refname-callback处理分支/标签引用名

实战示例

示例 1:从历史中彻底删除敏感文件

bash
git filter-repo --invert-paths --path config/secrets.yml
对比 filter-branch

仅需一行命令,而 Git filter-branch 详解#示例 1:从历史中彻底删除敏感文件(filter-branch + 清理备份 + 强制 GC)。filter-repo 会自动处理清理工作。

示例 2:拆分子目录为独立仓库

bash
git filter-repo --subdirectory-filter src/frontend

示例 3:批量重命名目录

bash
git filter-repo --path-rename src/:lib/

示例 4:删除所有二进制大文件

bash
git filter-repo --strip-blobs-bigger-than 50M

示例 5:分析仓库(不做修改)

bash
# 分析仓库中最大的文件、最大的目录等
git filter-repo --analyze
--analyze 模式

.git/filter-repo/analysis/ 下生成报告文件,帮助你了解仓库的文件大小分布、扩展名统计等信息,方便决定清理策略。

从 filter-branch 迁移

以下是 Git filter-branch 详解 常见操作与 filter-repo 等效命令的对照表:

目标filter-branchfilter-repo
删除文件--index-filter 'git rm --cached --ignore-unmatch <file>' HEAD--invert-paths --path <file>
删除文件(glob)--tree-filter 'rm -f *.log' HEAD--path-glob '*.log' --invert-paths
修改作者邮箱--env-filter 'if ...; then export GIT_AUTHOR_EMAIL=...; fi' -- --all--mailmap my-mailmap
修改提交信息--msg-filter 'sed ...' -- --all--message-callback 'return re.sub(...)'
提取子目录--subdirectory-filter <dir> -- --all--subdirectory-filter <dir>
重命名目录--tree-filter 'mv old new' HEAD--path-rename old/:new/
删除大文件无内置支持,需配合脚本--strip-blobs-bigger-than 50M
跳过空提交--prune-empty默认行为(自动跳过)
清理备份引用需手动清理 refs/original/自动完成,无备份引用
强制 GC需手动 git gc --prune=now自动完成
迁移建议
  1. 先用 git filter-repo --analyze 分析仓库,了解文件分布
  2. 在全新 git clone 的副本上执行操作
  3. 大多数 filter-branch 场景只需一行 filter-repo 命令即可完成

--analyze 详解

--analyzefilter-repo 独有的分析模式,不修改任何数据,只生成仓库统计报告。

bash
git filter-repo --analyze

执行后在 .git/filter-repo/analysis/ 目录下生成以下报告文件:

文件内容
blob-shas-and-paths.txt每个 blob 的 SHA、大小和路径
path-all-sizes.txt所有路径的各种大小统计(压缩前后)
path-deleted-sizes.txt已删除文件的大小统计
directories-all-sizes.txt目录级别的大小统计
directories-deleted-sizes.txt已删除目录的大小统计
extensions-all-sizes.txt按扩展名分类的大小统计
extensions-deleted-sizes.txt已删除文件按扩展名的大小统计
renames.txt文件重命名历史

典型使用流程

bash
# 1. 分析仓库
git filter-repo --analyze

# 2. 查看最大的文件
cat .git/filter-repo/analysis/path-all-sizes.txt | head -20

# 3. 查看哪些扩展名占用空间最多
cat .git/filter-repo/analysis/extensions-all-sizes.txt

# 4. 根据分析结果决定清理策略
git filter-repo --strip-blobs-bigger-than 50M
# 或
git filter-repo --path-glob '*.zip' --invert-paths
实际输出示例

path-all-sizes.txt 的输出格式:

=== All paths by reverse accumulated size ===
Format: unpacked size, packed size, date, path name(s)
  132589348  45678901  2024-06-15  data/large-dataset.csv
   52428800  12345678  2024-03-20  assets/video.mp4
   10485760   5242880  2024-01-10  build/output.tar.gz

据此可以快速定位需要清理的大文件。

常见问题

报错 Aborting: Refusing to destructively overwrite repo history

filter-repo 要求在全新 clone 的仓库上操作,以防止意外数据丢失。解决方式:

bash
# 方式一:重新 clone(推荐)
git clone original-repo clean-repo
cd clean-repo
git filter-repo ...

# 方式二:强制执行(确认已备份后)
git filter-repo --force ...
执行后 origin remote 消失了?

这是设计行为filter-repo 会自动删除 origin remote,防止你不小心将重写后的历史推送到原始远程仓库。需要手动重新设置:

bash
git remote add origin git@github.com:user/repo.git
git push --force --all
git push --force --tags
可以增量执行吗?上次执行中断了

filter-repo 不支持增量执行。如果中断了,需要重新 clone 并从头执行。但由于 filter-repo 速度极快(基于 fast-export/import),即使重新执行也通常只需几秒到几分钟。

如何只处理特定分支?
bash
# 只保留 main 和 develop 分支
git filter-repo --refs main develop

# 排除某些分支
git filter-repo --refs main --refs develop --invert-refs refs/heads/feature/*

安全特性

filter-repo 在设计上比 filter-branch 更安全:

  • 强制在全新 clone 上操作 — 默认拒绝在有未推送更改的仓库上运行(可用 --force 覆盖)
  • 自动清理 — 自动删除 origin remote、过期 reflog、运行 GC,无需手动操作
  • 无备份引用污染 — 不像 filter-branchrefs/original/ 留下大量备份引用
  • 确定性结果 — 相同输入总是产生相同输出

与 filter-branch 对比

对比项Git filter-branch 详解filter-repo
速度慢(逐个检出提交)快 10-100 倍(fast-export/import)
安全性多种已知陷阱设计上规避常见问题
易用性复杂的 shell 命令简洁的 CLI + Python 回调
清理工作需手动清理备份引用和 GC自动完成
依赖Git 内置需单独安装(Python 脚本)
官方态度已弃用推荐使用
回调语言ShellPython
分析功能--analyze 生成详细报告

速查表

目标命令
删除文件git filter-repo --invert-paths --path <file>
删除 glob 匹配git filter-repo --path-glob '*.log' --invert-paths
保留文件/目录git filter-repo --path <file> --path <dir>/
重命名目录git filter-repo --path-rename old/:new/
提取子目录git filter-repo --subdirectory-filter <dir>
删除大文件git filter-repo --strip-blobs-bigger-than 50M
替换文本git filter-repo --replace-text expressions.txt
修改作者信息git filter-repo --mailmap my-mailmap
分析仓库git filter-repo --analyze
只处理特定分支git filter-repo --refs main
强制执行--force

Python 库用法

filter-repo 不仅是 CLI 工具,还可以作为 Python 库导入使用,适合编写复杂的自定义过滤脚本。

基本结构

python
import git_filter_repo as fr

# 定义过滤函数
def my_blob_callback(blob, callback_metadata):
    # 处理每个 blob(文件内容)
    if len(blob.data) > 10 * 1024 * 1024:  # 大于 10MB
        blob.skip()

def my_commit_callback(commit, callback_metadata):
    # 处理每个提交
    # 在提交信息前加上前缀
    commit.message = b"[migrated] " + commit.message

# 创建过滤器并运行
args = fr.FilteringOptions.default_options()
args.force = True

repo_filter = fr.RepoFilter(
    args,
    blob_callback=my_blob_callback,
    commit_callback=my_commit_callback,
)
repo_filter.run()

常用对象属性

对象常用属性
Blobdata(文件内容,bytes)、original_id(原始 SHA)
Commitmessageauthor_nameauthor_emailauthor_datecommitter_namecommitter_emailfile_changesparents
FileChangefilenamemodeblob_idtypeb'M' 修改 / b'D' 删除)
Tagnametagger_nametagger_emailmessage

实用示例:条件性文件删除

python
import git_filter_repo as fr

def remove_large_binaries(commit, callback_metadata):
    """删除所有提交中大于 5MB 的二进制文件"""
    new_file_changes = []
    for change in commit.file_changes:
        # 跳过删除操作(保留删除记录)
        if change.type == b'D':
            new_file_changes.append(change)
            continue
        # 检查文件扩展名
        filename = change.filename.decode()
        binary_exts = {'.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.bin'}
        ext = '.' + filename.rsplit('.', 1)[-1] if '.' in filename else ''
        if ext.lower() not in binary_exts:
            new_file_changes.append(change)
    commit.file_changes = new_file_changes

args = fr.FilteringOptions.default_options()
args.force = True
fr.RepoFilter(args, commit_callback=remove_large_binaries).run()
何时使用 Python API
  • CLI 选项无法满足需求时(如条件性删除、跨文件逻辑判断)
  • 需要结合外部数据源(如数据库、配置文件)决定过滤策略时
  • 需要统计或收集过滤过程中的信息时

Windows 注意事项

Windows 用户必读

在 Windows 上使用 filter-repo 有一些平台特有的注意事项。

安装推荐

powershell
# 推荐使用 Scoop 安装(自动处理 PATH)
scoop install git-filter-repo

# 验证安装
git filter-repo --version

路径分隔符

filter-repo 内部统一使用 正斜杠 / 作为路径分隔符,即使在 Windows 上也是如此:

powershell
# 正确 ✅
git filter-repo --path src/utils/helper.py --invert-paths

# 错误 ❌(Windows 反斜杠不被识别)
git filter-repo --path src\utils\helper.py --invert-paths

Shell 引号

PowerShell 和 CMD 的引号规则与 Bash 不同:

powershell
# PowerShell:使用双引号
git filter-repo --path-glob "*.log" --invert-paths

# PowerShell:回调中使用单引号包裹 Python 代码
git filter-repo --blob-callback '
    blob.data = blob.data.replace(b"old", b"new")
'

# 如果遇到引号转义问题,建议使用 Git Bash

编码问题

powershell
# 如果提交信息含中文,确保 UTF-8 编码
git filter-repo --message-callback '
    return message.decode("utf-8").replace("旧文本", "新文本").encode("utf-8")
'
推荐做法

在 Windows 上遇到复杂的回调操作时,建议将 Python 脚本写入 .py 文件,然后用 Python 库方式调用(参见 Python 库用法),避免 shell 引号转义问题。

完整工作流程

从分析到清理的端到端流程:

详细步骤

bash
# ===== 1. 克隆全新副本(必须) =====
git clone --no-local original-repo clean-repo
cd clean-repo

# ===== 2. 分析仓库 =====
git filter-repo --analyze
# 查看报告
cat .git/filter-repo/analysis/path-all-sizes.txt | head -30
cat .git/filter-repo/analysis/extensions-all-sizes.txt

# ===== 3. 确定策略后执行(示例:删除大文件 + 敏感文件) =====
git filter-repo \
    --invert-paths \
    --path config/secrets.yml \
    --path-glob '*.sql.gz' \
    --strip-blobs-bigger-than 50M

# ===== 4. 验证结果 =====
# 检查文件是否已删除
git log --all --full-history -- config/secrets.yml
# 应该无输出

# 检查仓库大小
git count-objects -vH

# 检查提交历史是否完整
git log --oneline | wc -l

# ===== 5. 重设 remote 并推送 =====
git remote add origin git@github.com:user/repo.git
git push --force --all
git push --force --tags

# ===== 6. 通知所有协作者 =====
# 协作者需要重新 clone,不能 pull!
协作者必须重新 clone

历史重写后,所有协作者必须:

  1. 备份本地未推送的更改
  2. rm -rf repo && git clone ... 重新克隆
  3. 将备份的更改 cherry-pick 或重新应用

绝对不要 git pull,否则会将新旧历史合并,产生混乱的重复提交。

更多实战场景

场景 1:合规审计——清除历史中所有敏感信息

bash
# 企业安全审计发现仓库历史中存在 API Key、密码等敏感信息

# 1. 分析仓库
git filter-repo --analyze
cat .git/filter-repo/analysis/path-all-sizes.txt

# 2. 创建替换规则文件
cat > expressions.txt << 'EOF'
# API Keys(正则匹配常见格式)
regex:(?i)(api[_-]?key|apikey)\s*[=:]\s*["']?[A-Za-z0-9_\-]{20,}["']?==>\1="***REMOVED***"

# AWS 凭证
regex:AKIA[0-9A-Z]{16}==>***AWS_KEY_REMOVED***
regex:(?i)aws[_-]?secret[_-]?access[_-]?key\s*[=:]\s*["']?[A-Za-z0-9/+=]{40}["']?==>aws_secret_access_key="***REMOVED***"

# 私钥内容
regex:-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (RSA |EC |DSA )?PRIVATE KEY-----==>***PRIVATE_KEY_REMOVED***

# 数据库连接字符串
regex:(?i)(mysql|postgres|mongodb)://[^\s"']+:[^\s"']+@[^\s"']+==>\1://***REMOVED***
EOF

# 3. 执行替换
git filter-repo --replace-text expressions.txt

# 4. 同时删除敏感文件
git filter-repo --invert-paths \
    --path-glob '*.pem' \
    --path-glob '*.key' \
    --path-glob '*.p12' \
    --path .env \
    --path-glob '.env.*' \
    --force

# 5. 验证
git log -p --all -S "AKIA" | head -20
# 应无输出

场景 2:Monorepo 拆分为多个独立仓库

bash
# monorepo 结构:
# packages/
#   frontend/
#   backend/
#   shared/
# tools/
# docs/

# === 拆分 frontend ===
git clone --no-local monorepo frontend-repo
cd frontend-repo

# 保留 frontend 和 shared(共享代码)
git filter-repo \
    --path packages/frontend/ \
    --path packages/shared/

# 重命名目录结构(去掉 packages/ 前缀)
git filter-repo \
    --path-rename packages/frontend/:src/ \
    --path-rename packages/shared/:shared/ \
    --force

# === 拆分 backend ===
cd ..
git clone --no-local monorepo backend-repo
cd backend-repo

git filter-repo \
    --path packages/backend/ \
    --path packages/shared/

git filter-repo \
    --path-rename packages/backend/:src/ \
    --path-rename packages/shared/:shared/ \
    --force

场景 3:开源项目准备——清理私有信息

bash
# 将内部项目开源前,清理私有信息

# 1. 克隆副本
git clone --no-local internal-repo opensource-repo
cd opensource-repo

# 2. 删除内部文档和配置
git filter-repo --invert-paths \
    --path docs/internal/ \
    --path configs/production/ \
    --path-glob '**/internal-*' \
    --path INTERNAL_README.md

# 3. 替换内部 URL 和邮箱
cat > replacements.txt << 'EOF'
literal:internal.company.com==>example.com
literal:@company-internal.com==>@example.com
regex:https://jira\.company\.com/browse/([A-Z]+-\d+)==>Issue #\1
EOF
git filter-repo --replace-text replacements.txt --force

# 4. 修正作者信息(使用公开邮箱)
cat > mailmap.txt << 'EOF'
Alice <alice@opensource.org> <alice@company-internal.com>
Bob <bob@opensource.org> <bob@company-internal.com>
EOF
git filter-repo --mailmap mailmap.txt --force

# 5. 验证无内部信息残留
git log --all -p | grep -i "company-internal\|internal\.company" | head -20

场景 4:迁移仓库并重组目录结构

bash
# 将旧仓库的代码迁移到新的目录结构

git clone --no-local old-repo new-repo
cd new-repo

# 批量重命名
git filter-repo \
    --path-rename app/:src/app/ \
    --path-rename lib/:src/lib/ \
    --path-rename test/:tests/ \
    --path-rename config/:configs/ \
    --path-rename Gruntfile.js:legacy/Gruntfile.js

# 使用 filename-callback 处理更复杂的规则
git filter-repo --filename-callback '
    # 将所有 .jsx 文件重命名为 .tsx
    if filename.endswith(b".jsx"):
        return filename[:-4] + b".tsx"
    # 将 styles/ 下的 .css 移到 src/styles/
    if filename.startswith(b"styles/"):
        return b"src/" + filename
    return filename
' --force

场景 5:自动化清理脚本

python
#!/usr/bin/env python3
"""repo-sanitizer.py — 仓库清理自动化脚本"""

import git_filter_repo as fr
import re
import sys
from pathlib import Path

# 敏感模式列表
SENSITIVE_PATTERNS = [
    rb'AKIA[0-9A-Z]{16}',                    # AWS Access Key
    rb'(?i)password\s*[=:]\s*["\'][^"\']+',   # 密码赋值
    rb'-----BEGIN.*PRIVATE KEY-----',         # 私钥
    rb'ghp_[A-Za-z0-9]{36}',                 # GitHub Token
    rb'sk-[A-Za-z0-9]{48}',                  # OpenAI API Key
]

# 统计
stats = {'blobs_scanned': 0, 'blobs_modified': 0, 'secrets_found': 0}

def sanitize_blob(blob, callback_metadata):
    """扫描并替换 blob 中的敏感信息"""
    stats['blobs_scanned'] += 1
    original = blob.data

    for pattern in SENSITIVE_PATTERNS:
        matches = re.findall(pattern, blob.data)
        if matches:
            stats['secrets_found'] += len(matches)
            blob.data = re.sub(pattern, b'***REDACTED***', blob.data)

    if blob.data != original:
        stats['blobs_modified'] += 1

def main():
    args = fr.FilteringOptions.default_options()
    args.force = True

    repo_filter = fr.RepoFilter(
        args,
        blob_callback=sanitize_blob,
    )
    repo_filter.run()

    print(f"\n{'='*50}")
    print(f"扫描 blob 数: {stats['blobs_scanned']}")
    print(f"修改 blob 数: {stats['blobs_modified']}")
    print(f"发现敏感信息: {stats['secrets_found']}")
    print(f"{'='*50}")

if __name__ == '__main__':
    main()
bash
# 使用方式
git clone --no-local original-repo clean-repo
cd clean-repo
python ../repo-sanitizer.py

CI/CD 集成

GitHub Actions 自动检测大文件

yaml
# .github/workflows/repo-check.yml
name: Repository Health Check

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 0 * * 1' # 每周一检查

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # 需要完整历史

      - name: Install git-filter-repo
        run: pip install git-filter-repo

      - name: Analyze repository
        run: |
          git filter-repo --analyze
          echo "## 仓库分析报告" >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          head -20 .git/filter-repo/analysis/path-all-sizes.txt >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY

      - name: Check for large files
        run: |
          # 检查是否有超过 50MB 的文件
          LARGE_FILES=$(git rev-list --objects --all | \
            git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
            awk '/^blob/ && $3 > 52428800 {print $4, $3/1048576 "MB"}')
          if [ -n "$LARGE_FILES" ]; then
            echo "::warning::发现大文件,建议使用 Git LFS:"
            echo "$LARGE_FILES"
          fi

预提交检查脚本

bash
#!/bin/bash
# pre-commit hook: 阻止提交大文件
# 安装: cp pre-commit-size-check.sh .git/hooks/pre-commit

MAX_SIZE=10485760  # 10MB

LARGE_FILES=$(git diff --cached --name-only --diff-filter=ACM | while read file; do
    size=$(wc -c < "$file" 2>/dev/null || echo 0)
    if [ "$size" -gt "$MAX_SIZE" ]; then
        echo "  $file ($(echo "scale=1; $size/1048576" | bc)MB)"
    fi
done)

if [ -n "$LARGE_FILES" ]; then
    echo "❌ 以下文件超过 $(echo "scale=0; $MAX_SIZE/1048576" | bc)MB 限制:"
    echo "$LARGE_FILES"
    echo ""
    echo "建议使用 Git LFS: git lfs track \"*.ext\""
    echo "跳过检查: git commit --no-verify"
    exit 1
fi

详见 Git hooks


常见陷阱

陷阱 1:在非全新 clone 上执行
bash
# ❌ 直接在工作仓库上执行
cd my-project
git filter-repo --strip-blobs-bigger-than 50M
# Aborting: Refusing to destructively overwrite repo history

# ✅ 先克隆全新副本
git clone --no-local my-project my-project-clean
cd my-project-clean
git filter-repo --strip-blobs-bigger-than 50M

# 或使用 --force(确认已备份后)
git filter-repo --strip-blobs-bigger-than 50M --force
陷阱 2:多次 filter-repo 导致结果不一致
bash
# ❌ 连续执行多次 filter-repo(第二次在第一次结果上操作)
git filter-repo --invert-paths --path secret.txt
git filter-repo --path-rename old/:new/ --force
# 第二次的 --force 可能导致意外结果

# ✅ 在单次命令中完成所有操作
git filter-repo \
    --invert-paths --path secret.txt \
    --path-rename old/:new/

# 如果必须分步执行,每次都从全新 clone 开始
陷阱 3:--path-rename 的顺序问题
bash
# ❌ 重命名链可能产生意外结果
git filter-repo \
    --path-rename src/:lib/ \
    --path-rename lib/:modules/
# src/foo.js → lib/foo.js → modules/foo.js ?
# 实际上 rename 只应用一次,不会链式处理

# ✅ 直接指定最终目标
git filter-repo --path-rename src/:modules/
陷阱 4:回调中的编码问题
bash
# ❌ Python 回调中忘记使用 bytes
git filter-repo --commit-callback '
    commit.message = commit.message.replace("旧文本", "新文本")
'
# TypeError: a bytes-like object is required

# ✅ 所有字符串必须是 bytes
git filter-repo --commit-callback '
    commit.message = commit.message.replace(b"\xe6\x97\xa7", b"\xe6\x96\xb0")
'

# ✅ 更好:encode/decode 处理
git filter-repo --commit-callback '
    msg = commit.message.decode("utf-8")
    msg = msg.replace("旧文本", "新文本")
    commit.message = msg.encode("utf-8")
'
陷阱 5:origin remote 被自动删除
bash
# filter-repo 执行后 origin 会被删除
git remote -v
# (empty)

# 这是设计行为——防止误推到原始仓库

# ✅ 重新添加 remote
git remote add origin git@github.com:team/repo.git
git push --force --all
git push --force --tags
陷阱 6:filter-repo 后 LFS 指针文件问题
bash
# 如果仓库使用 Git LFS,filter-repo 可能将 LFS 指针文件
# 当作普通文件处理,导致 LFS 关联断裂

# ✅ filter-repo 后重新配置 LFS
git lfs install
git lfs pull

# 或者在 filter-repo 时排除 LFS 跟踪的文件
git filter-repo --invert-paths --path secret.txt
# LFS 文件不受影响(因为它们本身就是指针文件)

性能优化

bash
# filter-repo 已经很快了,但大型仓库(100K+ 提交)可以进一步优化

# 1. 限制处理范围
git filter-repo --refs main --refs develop
# 只处理指定分支,跳过不需要的分支

# 2. 使用 --partial 避免完全重写
git filter-repo --partial --invert-paths --path large-file.bin
# --partial 不会删除 origin remote,适合增量操作

# 3. 并行处理(Python API)
# 将大仓库按时间范围分批处理
# 然后用 git replace 或 grafts 拼接

# 4. 避免不必要的回调
# blob-callback 会对每个 blob 调用,如果只需修改文件名,用 --filename-callback
# commit-callback 比 blob-callback 调用次数少得多

filter-repo vs filter-branch vs BFG 性能对比

工具10K 提交 (删文件)100K 提交 (删文件)优势
filter-branch~45 秒~8 分钟Git 内置
BFG Repo Cleaner~3 秒~20 秒删大文件极快
filter-repo~5 秒~30 秒功能最全、最灵活
选型建议
  • 只需删除大文件/密码 → BFG(最快)
  • 需要路径重命名/内容替换/回调 → filter-repo(最灵活)
  • 不便安装第三方工具 → filter-branch(内置但已弃用)

相关命令

参考资料