Git 仓库瘦身必备:git filter-repo 完全指南
Git 作为一个分布式版本控制系统,以其强大的分支管理和历史追溯能力赢得了全球开发者的青睐。然而,随着项目的成长,尤其是当不小心提交了大型文件、敏感信息或者整个不必要的目录时,Git 仓库可能会变得异常臃肿。一个庞大的仓库不仅会增加克隆、拉取和推送的时间,消耗大量的存储空间,还可能在CI/CD流程中造成性能瓶颈。更糟糕的是,一旦敏感信息(如密码、私钥)被提交到历史记录中,即使在最新提交中删除了文件,它们仍然存在于仓库的历史深处,随时可能被恢复,带来严重的安全风险。
传统的 Git 历史修改工具如 git filter-branch
能够完成这类任务,但它因其复杂的语法、缓慢的执行速度以及一些难以处理的边缘情况(如空白提交、合并没有保留父提交等)而饱受诟病,并且 Git 官方已经推荐使用更现代、更强大、更易用的替代品:git filter-repo
。
git filter-repo
是一个由 Git 社区积极维护的新工具,旨在解决 git filter-branch
的痛点。它基于 Python 编写,处理速度远超 git filter-branch
,提供了更清晰的选项,并且能够更鲁棒地处理各种复杂的仓库历史结构。它是当前进行 Git 仓库历史重写,实现仓库瘦身和清理的最佳选择。
本指南将带你深入了解 git filter-repo
,包括其安装、基本原理、常见用法(特别是针对仓库瘦身的应用)、高级技巧以及使用时的注意事项。
为什么需要 Git 仓库瘦身?
在深入了解工具之前,先明确一下为什么仓库会变大以及变大带来的问题:
- 误提交大文件: 图片、视频、压缩包、编译产物、数据库备份等大型二进制文件被错误地添加到版本控制中。
- 提交敏感信息: 密码、API 密钥、私钥、配置文件等不应公开或不应进入版本控制的内容。
- 长期积累的无用文件: 项目迭代过程中产生的临时文件、日志文件、废弃资源等被保留在历史中。
- 不当的子模块或外部依赖管理: 将整个外部库的历史或大型子模块错误地包含在主仓库历史中。
这些情况导致仓库体积膨胀,带来以下问题:
- 克隆/拉取耗时: 新用户加入或在不同环境部署时,需要下载完整的历史记录,耗费大量时间和带宽。
- 存储空间占用: 仓库服务器、开发者本地硬盘都需要存储庞大的历史数据。
- 性能下降: 部分 Git 操作(如
git status
,git blame
,git log
某些查询)可能会受到影响。 - 安全风险: 敏感信息泄露的潜在威胁。
git filter-repo
正是为了解决这些问题而生。
git filter-repo
的安装
git filter-repo
需要 Python 3.5 或更高版本。最推荐的安装方式是通过 pip:
bash
pip install git-filter-repo
如果你没有安装 pip,或者想使用系统包管理器,可以参考官方文档。安装完成后,你可以在命令行中运行 git filter-repo --version
来验证是否安装成功。
使用 git filter-repo
的核心原理与重要警告
git filter-repo
的核心工作原理是:
- 读取你的 Git 仓库的 所有 历史记录(所有的提交、树对象、Blob 对象以及引用)。
- 根据你指定的过滤规则,生成一个新的、修改过的提交、树对象和 Blob 对象。
- 创建一个全新的历史记录,新的历史记录中的提交拥有全新的 SHA-1/SHA-256 哈希值。
- 将你的分支和标签等引用指向新的历史记录。
核心警告:历史重写是一个破坏性操作!
- 提交哈希值会改变:
git filter-repo
生成的是一个全新的历史。这意味着 所有 提交的哈希值都会发生变化。 - 影响所有协作者: 一旦你将修改后的历史推送到远程仓库,所有基于旧历史工作的协作者都需要丢弃他们的本地分支,重新克隆仓库,或者执行复杂的变基操作来切换到新的历史。
- 无法撤销: 一旦旧的引用被垃圾回收清理掉,你将很难恢复到修改前的历史状态。
因此,在使用 git filter-repo
进行历史重写之前,请务必执行以下步骤:
- 备份仓库: 最简单的方式是复制你的 Git 仓库目录。或者克隆一个新的副本:
git clone --mirror <你的仓库路径> <备份目录>
。这是最重要的一步! - 在一个干净的环境中操作: 推荐在一个全新的、刚刚克隆下来的仓库副本上进行操作,而不是在你日常工作的仓库目录。最好使用一个全新的克隆:
git clone <你的仓库路径> <新目录>
,然后在<新目录>
中运行git filter-repo
。或者克隆一个裸仓库(bare repo):git clone --bare <你的仓库路径> <新目录>.git
,然后在<新目录>.git
中操作。裸仓库没有工作目录,更适合这类后台历史重写任务。 - 通知你的团队: 如果你修改了共享的远程仓库的历史,务必提前通知所有协作者,并指导他们如何切换到新的历史。
默认行为: git filter-repo
默认会在当前仓库目录中直接进行重写。这会直接修改当前仓库的历史。如果你在一个带有工作目录的仓库中运行它,它会在操作前检查工作目录是否干净。出于安全考虑,强烈建议在一个临时克隆或裸仓库副本上操作。
识别需要移除的内容
在进行瘦身操作之前,你需要知道仓库中哪些文件占用了大量空间。可以使用以下 Git 命令组合来查找大型对象:
“`bash
查找所有 blob (文件内容) 对象及其大小,按大小排序
git rev-list –all –objects | \
git cat-file –batch-check=’%(objectname) %(objecttype) %(size) %(rest)’ | \
grep blob | \
sort -k3n
然后通过 blob hash 找到对应的文件路径
例如,对于上面的输出中的一个 hash (e.g., abcdef123456), 使用:
git rev-list –all –objects –filter=blob:limit=1000M | \ # 可选,限制大小
git log –pretty=format:’%h %p’ –reverse –simplify-by-decoration –date-order –all | \
git ls-tree –long $(grep abcdef123456 .git/objects/info/packs | cut -d’ ‘ -f1) | \
grep abcdef123456
更简单的方式,使用 Git 内置工具或者第三方脚本
Git 2.18+ 内置了更方便的工具:
git size-pack -v # 查看 pack 文件中的对象信息,但直接看文件名不直观
推荐使用专门的 Git 仓库分析工具,例如:
git-sizer (需要安装) 可以直观地报告仓库中哪些方面导致其庞大
BFG Repo-Cleaner (虽然是替代 git-filter-branch 的工具,但它有扫描功能)
或通过上面的 git rev-list | git cat-file | sort 组合,手动查找最大的 Blob hash
“`
一旦你找到了占用空间最大的文件的路径或包含敏感信息的文件的路径,就可以使用 git filter-repo
来移除它们。
git filter-repo
常见瘦身用法示例
以下是使用 git filter-repo
进行仓库瘦身的几种常见场景及其对应的命令。
注意: 在运行这些命令之前,请确保你已经在备份后的仓库副本或新建的临时克隆中执行操作!
场景 1: 移除历史中所有提交中某个特定文件
这是最常见的瘦身场景,例如移除了一个不小心提交的大型压缩包 large_archive.zip
。
“`bash
假设 large_archive.zip 在仓库根目录
git filter-repo –invert-paths –path large_archive.zip
假设 large_archive.zip 在 docs/assets/ 目录下
git filter-repo –invert-paths –path docs/assets/large_archive.zip
“`
--invert-paths
: 反转路径匹配。通常--path
指定要 保留 的路径,加上--invert-paths
就表示要 移除 指定路径。--path <文件路径>
: 指定要匹配的文件或目录路径。路径是相对于仓库根目录的。
重要: git filter-repo
会遍历历史中的每一个提交。如果某个提交修改了 large_archive.zip
,filter-repo
会创建一个新的提交,这个新提交的内容与原提交除了没有 large_archive.zip
之外完全相同。如果没有提交涉及这个文件,这个命令可能不会改变历史中的 Blob 对象,但如果这个文件曾经存在于历史中,它会被移除。
场景 2: 移除历史中所有提交中某个特定目录
例如,不小心将一个包含大量临时文件的 temp/
目录或一个编译输出目录 build/
提交到了历史中。
“`bash
移除仓库根目录下的 temp/ 目录
git filter-repo –invert-paths –path temp/
移除 src/vendor/large_lib/ 目录
git filter-repo –invert-paths –path src/vendor/large_lib/
“`
与文件类似,--path <目录路径>/
指定要移除的目录。
场景 3: 移除历史中所有大小超过阈值的 Blob 对象
如果你不知道具体哪些文件导致仓库臃肿,但知道是因为某些大文件,可以使用这个选项。例如,移除所有大小超过 100MB 的文件 Blob。
bash
git filter-repo --strip-blobs-bigger-than 100M
--strip-blobs-bigger-than <大小>
: 移除所有大小超过指定值的 Blob 对象。大小可以使用 K, M, G 等后缀(如 100K, 50M, 1G)。
警告: 这个选项会移除 所有 大于指定大小的文件,无论它们的路径如何。如果你的项目中确实需要某个大文件(例如,一个大型数据集文件),并且希望保留它在 最新 提交中,但移除历史中的旧版本或一些其他不相关的大文件,你需要更精确地使用 --path
结合 --invert-paths
,或者在运行这个命令 之前 将需要保留的大文件移出仓库,运行清理后再移回并在新的提交中添加。一般来说,对于已知的大文件,使用 --invert-paths --path
更安全可控。只有当你确定所有超大文件都不应该存在于历史中时,才使用此选项。
场景 4: 只保留历史中指定的文件或目录(移除其他所有内容)
这个选项通常用于从一个大仓库中提取出某个子项目或特定文件集的历史。
“`bash
只保留 src/core/ 目录和 README.md 文件
git filter-repo –path src/core/ –path README.md
“`
--path <路径>
: 可以多次使用,指定所有要 保留 的文件或目录。
这将创建一个新的仓库历史,其中只包含指定路径下的文件和目录,以及那些对这些文件有贡献的提交。这对于从一个单体仓库中剥离出一个微服务或库的历史非常有用,也可以间接用于瘦身(如果想剥离的部分比整个仓库小很多的话)。
场景 5: 移除包含特定敏感信息的提交(结合查找工具)
git filter-repo
本身不直接通过 文件内容 来过滤提交。你需要先找到包含敏感信息的文件路径,然后使用 filter-repo
移除这些路径。
-
查找包含敏感信息的文件和提交: 使用
git grep
或其他工具。“`bash
查找所有分支中包含 “my_password” 的文件
git grep -n “my_password” $(git rev-list –all)
或者更强大的工具如 BFG Repo-Cleaner 的扫描模式
“`
-
使用
git filter-repo
移除找到的文件路径: 一旦找到了包含敏感信息的文件路径(例如config/secrets.yml
),使用--invert-paths --path
移除它。bash
git filter-repo --invert-paths --path config/secrets.yml -
如果敏感信息在提交消息中:
git filter-repo
可以修改提交消息。bash
git filter-repo --message-callback '
import re
message = commit.message.decode("utf-8")
message = re.sub("my_password", "[REDACTED]", message) # 替换敏感词
message = re.sub("-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----", "[PRIVATE KEY REMOVED]", message, flags=re.DOTALL) # 替换私钥块
return message.encode("utf-8")
'--message-callback
: 使用 Python 脚本修改提交消息。提供了一个commit
对象,可以通过commit.message
访问和修改消息。
警告: 移除文件路径是最彻底的方式,它会从历史中移除 Blob 对象。修改提交消息只改变可见文本,不改变文件内容 Blob。对于敏感信息,通常需要移除包含它的文件。
场景 6: 重写文件路径
如果你在历史中多次重命名了文件或目录,或者想统一路径命名,可以使用 --path-rewrite
。这通常不是为了瘦身,但属于历史重写范畴,有时整理路径也能间接帮助理解仓库结构。
“`bash
将历史中所有名为 old_folder 的目录重命名为 new_folder
git filter-repo –path-rewrite old_folder/:new_folder/
将特定文件 old_file.txt 重命名为 new_file.md
git filter-repo –path-rewrite old_file.txt:new_file.md
“`
--path-rewrite <旧路径>:<新路径>
: 指定路径重写规则。支持通配符和更复杂的规则,详见官方文档。
高级用法与注意事项
1. 处理空提交 (Empty Commits)
历史重写过程中,可能会出现一些提交在移除内容后变为空提交(即与前一个提交拥有完全相同的树对象)。git filter-repo
默认会移除这些空提交。如果你的工作流程依赖于保留空提交(极少数情况),可以使用 --keep-empty
选项。
2. 使用 --force
如果 git filter-repo
检测到目标仓库不是一个新的克隆,或者工作目录不干净,它会拒绝运行。为了覆盖这个安全检查,可以使用 --force
。请谨慎使用此选项,并确保你完全理解其含义,最好是在临时克隆或裸仓库副本上使用。
bash
git filter-repo --force --invert-paths --path large_file.zip
3. 配置 filter-repo
可以将常用的选项保存在配置文件中,例如 .filterrepo
,然后在命令行中引用。
“`ini
.filterrepo 文件示例
–invert-paths
–path large_archive.zip
–path temp/
–strip-blobs-bigger-than 50M
“`
然后在命令行中运行:git filter-repo --config .filterrepo
4. 工作原理细节:引用和垃圾回收
git filter-repo
完成后,它会创建新的提交对象,并将所有的分支和标签引用(ref)更新指向新的提交。旧的提交对象及其关联的 Blob 对象仍然存在于 .git/objects/
目录中,但已经没有被任何“活跃”的引用指向了。
Git 的垃圾回收机制(git gc
)负责清理这些不再被引用的对象。运行 git gc
可以打包零散对象,移除重复对象,并最终删除那些不再被任何引用(包括 reflog 中的引用,除非 --prune
指定了时间)指向的对象。
在运行 git filter-repo
后,为了真正释放空间,必须运行垃圾回收:
bash
git gc --aggressive --prune=now
--aggressive
: 启用更彻底但可能更耗时的优化。对于一次性的大规模清理非常有用。--prune=now
: 立即删除那些不再被引用的对象。默认情况下,Git 会保留这些“悬空”对象一段时间(通常是 30 天),以防你需要恢复。--prune=now
会立即删除它们。
注意: 即使运行了 git gc --aggressive --prune=now
,有时历史中的 Blob 对象仍可能被某些隐藏的引用(如 reflog 中未过期的条目)或 pack 文件中的中间对象所持有。最彻底的清理方法是:
- 在一个全新的目录中,克隆刚刚修改过的仓库:
git clone <path/to/modified/repo> <new_clean_repo_dir>
。 - 进入新的克隆目录
cd <new_clean_repo_dir>
。 - 运行
git gc --aggressive --prune=now
。 - 删除旧的修改过的仓库目录。
这样可以确保只保留新的历史,并且没有旧对象的残留。
后续步骤:推送修改后的历史并通知协作者
git filter-repo
完成本地仓库的历史修改后,你需要将这些修改推送到远程仓库。这是一个非快进 (non-fast-forward) 的推送,因为它创建了一个全新的历史分支。
你需要使用强制推送:
bash
git push --force
或者更安全地使用 force-with-lease
:
bash
git push --force-with-lease
--force-with-lease
会检查你本地的分支是否是最新的(即远程分支自从你上次拉取后没有被其他人修改过)。如果是,它才会执行强制推送。这有助于防止你在不知情的情况下覆盖其他协作者的提交。
再次强调:强制推送会覆盖远程仓库的历史! 在执行此操作之前,务必:
- 通知所有协作者: 告知他们历史将被重写,并说明原因和影响。
- 提供切换到新历史的指导:
- 最简单的方式是让他们删除本地仓库(如果不是主仓库)或至少删除本地分支,然后重新克隆仓库。
- 或者指导他们如何小心地进行变基或重置操作来同步到新的远程分支。例如:
bash
git fetch origin
git reset --hard origin/master # 或者 origin/<你的分支名>
git clean -fdx # 清理工作目录和未追踪的文件
这个操作会丢弃他们本地所有未提交和未推送的修改,以及基于旧历史的本地分支,直接指向新的远程分支。
对比 git filter-branch
虽然本指南主要关注 git filter-repo
,但快速了解它为何优于 git filter-branch
也是有益的:
- 速度:
git filter-repo
通常比git filter-branch
快 15-20 倍甚至更多,因为它使用了更高效的内部机制来处理 Git 对象。 - 易用性:
git filter-repo
的命令行选项更直观、更少、更易于理解和记忆。git filter-branch
依赖于 Shell 脚本,复杂且容易出错。 - 健壮性:
git filter-repo
能更好地处理各种边缘情况,如合并提交、空提交、子模块等。 - 维护状态:
git filter-repo
是一个积极维护的现代工具,而git filter-branch
已被官方标记为“deprecated”(已弃用),不推荐在新项目中使用。
总结
git filter-repo
是当前进行 Git 仓库历史重写和瘦身的最佳工具。它强大、快速且相对易用。通过本指南,你了解了如何安装它,理解了历史重写的核心原理和风险,以及如何使用它来解决常见的仓库臃肿问题,例如移除大型文件、目录或敏感信息。
关键要点回顾:
- 务必备份!务必备份!务必备份!
- 在一个临时克隆或裸仓库副本上运行
git filter-repo
。 - 使用
--invert-paths --path <路径>
来移除特定的文件或目录。 - 使用
--strip-blobs-bigger-than <大小>
来移除所有超大 Blob 对象(谨慎使用)。 - 完成后,必须运行
git gc --aggressive --prune=now
来释放空间。 - 将修改推送到远程时需要强制推送 (
--force
或--force-with-lease
)。 - 必须提前通知并协调所有协作者。
掌握 git filter-repo
,你就能有效地管理你的 Git 仓库大小,提升团队协作效率,并消除潜在的安全隐患。但请始终记住,历史重写是一个严肃的操作,务必小心行事,并做好充分的准备和沟通。
希望这篇详细指南能帮助你成功地使用 git filter-repo
瘦身你的 Git 仓库!