Skip to content

仓库瘦身

随着时间推移,Git 仓库可能因为误提交大文件、大量历史等问题变得庞大。本节介绍如何找到并清理这些"历史负担"。

查找大文件:git rev-list --objects --all

找出仓库历史中哪些文件占用空间最大:

bash
# 列出所有对象,并按大小排序(找出前 20 个)
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  grep '^blob' | \
  sort -k3 -n -r | \
  head -20 | \
  awk '{print $3, $4}'

# 更友好的输出格式
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | \
  sort -k2 -n -r | \
  awk '{printf "%.2f MB\t%s\n", $2/1048576, $3}' | \
  head -20

找到文件在哪次提交中引入:

bash
# 找到大文件的哈希后,查看它属于哪个文件
git log --all --full-history -- "**/<filename>"

# 或者通过哈希查找路径
git log --all --find-object=<blob-hash>

BFG Repo-Cleaner:清理历史中的大文件

BFG Repo-Cleaner 是专门用于清理 Git 历史的工具,比 git filter-branch 更快速易用。

安装

bash
# macOS
brew install bfg

# 或者下载 JAR 文件
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar
alias bfg="java -jar bfg-1.14.0.jar"

使用

bash
# 1. 克隆裸仓库(BFG 要求使用裸仓库)
git clone --mirror https://github.com/user/repo.git

# 2. 删除所有历史中的大文件(>10MB)
bfg --strip-blobs-bigger-than 10M repo.git

# 3. 删除特定文件
bfg --delete-files '*.zip' repo.git
bfg --delete-files 'secret.key' repo.git

# 4. 删除特定文件夹
bfg --delete-folders .git repo.git

# 5. 替换敏感信息(密码、密钥)
echo "my-secret-password" > passwords.txt
bfg --replace-text passwords.txt repo.git

# 6. 清理和推送
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force

git filter-branch:重写历史

git filter-branch 是 Git 内置的历史重写工具(较慢,不推荐用于大型仓库):

bash
# 从所有历史中删除某个文件
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch path/to/large-file.zip' \
  --prune-empty --tag-name-filter cat -- --all

# 清理
git reflog expire --expire=now --all
git gc --prune=now --aggressive

# 强制推送(会改变历史!)
git push origin --force --all
git push origin --force --tags

git filter-repo(推荐替代 filter-branch)

git-filter-repo 是官方推荐的替代品,速度更快,功能更强:

安装

bash
pip install git-filter-repo
# 或
brew install git-filter-repo

使用

bash
# 删除指定文件的所有历史
git filter-repo --path path/to/large-file.zip --invert-paths

# 删除指定目录的历史
git filter-repo --path assets/videos/ --invert-paths

# 使用通配符
git filter-repo --path-glob '*.mp4' --invert-paths
git filter-repo --path-glob '*.zip' --invert-paths

# 替换敏感字符串
git filter-repo --replace-text replacements.txt

# replacements.txt 格式:
# 旧字符串==>新字符串
# my_password==>REDACTED
# api_key_12345==>REDACTED_API_KEY

# 只保留某个目录的历史(提取子目录为独立仓库)
git filter-repo --subdirectory-filter src/

# 重命名路径
git filter-repo --path-rename old/path:new/path

清理后的推送与团队同步

清理历史后,所有提交的 SHA-1 都会改变,需要团队配合:

bash
# 清理完成后,强制推送所有分支和标签
git push origin --force --all
git push origin --force --tags

# 通知所有团队成员!
echo "
⚠️  重要通知:仓库历史已被重写!
所有团队成员需要执行以下操作:

  rm -rf local-repo/      # 删除本地仓库
  git clone <url>         # 重新克隆

或者(如果有未推送的本地工作):
  git fetch origin
  git rebase origin/main  # 在每个分支上执行
"

协作者的恢复操作:

bash
# 方案1:直接重新克隆(最安全)
cd ..
rm -rf my-project
git clone https://github.com/user/repo.git

# 方案2:如果有未提交的本地工作
git stash
git fetch origin
git reset --hard origin/main  # 或对应分支
git stash pop

防止历史污染的最佳实践

bash
# 1. 配置大文件预防性钩子
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
MAX_SIZE=$((10 * 1024 * 1024))  # 10MB
git diff --staged --name-only | while read file; do
  size=$(git cat-file -s "$(git hash-object "$file")" 2>/dev/null || echo 0)
  if [ "$size" -gt "$MAX_SIZE" ]; then
    echo "❌ 文件 '$file' ($(($size/1024/1024))MB) 超过 10MB 限制"
    echo "   请使用 Git LFS 管理大文件:git lfs track '$file'"
    exit 1
  fi
done
EOF
chmod +x .git/hooks/pre-commit

# 2. 使用 Git LFS 管理大文件(见性能优化章节)
git lfs install
git lfs track "*.mp4"
git lfs track "*.zip"
git lfs track "*.pdf"

总结

操作工具/命令适用场景
查找大文件git rev-list --objects诊断阶段
清理大文件历史BFG Repo-Cleaner简单快速(推荐)
复杂历史重写git-filter-repo复杂场景(官方推荐)
内置历史重写git filter-branch内置但较慢(不推荐)

清理历史是破坏性操作,执行前务必:

  1. 通知所有团队成员
  2. 在测试克隆上先验证效果
  3. 备份原始仓库
  4. 推送后所有人重新克隆