快速重写Git仓库历史

Hacker News Top 工具

摘要

git-filter-repo 是一个用于重写 Git 仓库历史的多功能工具,被 Git 项目推荐为比 git filter-branch 更快、功能更强大的替代方案。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/24 05:01

newren/git-filter-repo

来源:https://github.com/newren/git-filter-repo

git filter-repo 是一个多功能的历史重写工具,拥有[我在其他工具中未发现的功能](#filter-repo 背后的设计理念)。它大体上与 git filter-branch (https://git-scm.com/docs/git-filter-branch) 属于同一类工具,但避免了其令人沮丧的糟糕性能 (https://public-inbox.org/git/CABPp-BGOz8nks0+Tdw5GyGqxeYR-3FF6FT5JcgVqZDYVRQ6qog@mail.gmail.com/),拥有更多功能,并且在可用性上的设计使得它能够轻松应对超越简单重写的场景。现在,git 项目推荐使用 git filter-repo (https://git-scm.com/docs/git-filter-branch#_warning) 而非 git filter-branch。

虽然大多数用户可能只会将 filter-repo 用作简单的命令行工具(并且可能只使用它的几个标志),但 filter-repo 的核心包含一个用于创建历史重写工具的库。因此,有特殊需求的用户可以借助它快速创建全新的历史重写工具

目录

前提条件

filter-repo 需要:

  • git >= 2.36.0
  • python3 >= 3.6

如何安装?

虽然 git-filter-repo 仓库包含许多文件,但主要逻辑都包含在一个名为 git-filter-repo 的单一 Python 脚本文件中,这样设计是为了让许多系统上的基本使用变得简单:只需将该文件放入你的 $PATH 即可。

请参阅 INSTALL.md 了解基本用法或特殊情况以外的内容。仅当以下情况之一适用时,才需要更详细的说明:

  • 你并未直观地理解上述关于简单安装的说明
  • 你使用的 python3 可执行文件名称不是 “python3”
  • 你想要安装文档(除了 -h 显示的内置文档)
  • 你想要运行一些 contrib 示例
  • 你想要使用 filter-repo 作为模块/库来创建自己的 Python 过滤脚本

如何使用?

关于全面的文档:

  • 请参阅用户手册 (https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html)
  • 用户手册的替代格式可在各种外部网站上找到 (示例 (https://www.mankier.com/1/git-filter-repo)),适合不喜欢 htmlpreview.github.io 布局的用户,但它可能仅与最新发布版保持同步

如果你更喜欢通过示例学习:

无论哪种情况,你可能也会发现常见问题解答很有用。

为什么选择 filter-repo 而非其他替代方案?

这在 Git Rev News 关于 filter-repo 的文章 (https://git.github.io/rev_news/2019/08/21/edition-54/#an-introduction-to-git-filter-repo–written-by-elijah-newren) 中有更详细的介绍,但对于主要竞争对手,这里有一些重点:

filter-branch

  • filter-branch 对于非平凡仓库来说极慢甚至无法使用 (https://public-inbox.org/git/CABPp-BGOz8nks0+Tdw5GyGqxeYR-3FF6FT5JcgVqZDYVRQ6qog@mail.gmail.com/) (比应有的速度慢好几个数量级 (https://git-scm.com/docs/git-filter-branch#PERFORMANCE))。

  • filter-branch 充满隐患 (https://git-scm.com/docs/git-filter-branch#SAFETY),可能静默破坏你的重写,或至少让你的“清理”工作变得更棘手和混乱,得不偿失。

  • 对于任何稍微有点不平凡的重写,filter-branch 使用起来非常繁琐(参见简单示例)。

  • git 项目已声明上述 filter-branch 的问题无法通过向后兼容的方式修复;他们建议你停止使用 filter-branch (https://git-scm.com/docs/git-filter-branch#_warning)。

  • filter-branch 的死忠粉可能会对 filter-lamely (又名 filter-branch-ish)感兴趣,它是基于 filter-repo 重新实现的 filter-branch,性能更好(尽管不像 filter-repo 那样快或安全)。

  • 一张速查表展示了如何将 filter-branch 手册中的示例命令转换为 filter-repo 命令。

BFG Repo Cleaner

  • 在其时代是非常好的工具,但尽管它简化了一些事情,却仅局限于少数几种重写。

  • 其架构不适合处理更多类型的重写。

  • 即使在其预期用例中,其架构也存在一些缺陷和 bug。

  • BFG 的粉丝可能会对 bfg-ish 感兴趣,它是基于 filter-repo 重新实现的 BFG,相对于 BFG 包含了几项新功能和错误修复。

  • 一张速查表展示了如何将 BFG Repo Cleaner 手册中的示例命令转换为 filter-repo 命令。

简单示例与对比

假设我们要提取仓库的一部分,以便将其合并到另一个更大的仓库中。对于提取,我们希望:

  • 提取单个目录 src/ 的历史。这意味着只有 src/ 下的路径保留在仓库中,任何仅触及该目录外部路径的提交将被移除。
  • 将所有文件重命名,使其具有一个新的顶级目录 my-module/(例如,src/foo.c 变为 my-module/src/foo.c)。
  • 将提取仓库中的任何标签重命名为带有 ‘my-module-’ 前缀(以避免以后将此仓库合并到其他仓库时发生冲突)。

使用 filter-repo 解决

使用 filter-repo 只需以下命令即可完成:

git filter-repo --path src/ --to-subdirectory-filter my-module --tag-rename '':'my-module-'

(单引号并非必需,但能让人类更清楚地看到我们将空字符串作为前缀替换为 my-module-

使用 BFG Repo Cleaner 解决

BFG Repo Cleaner 无法完成这种重写;实际上,所需的三类更改都超出了它的能力范围。

使用 filter-branch 解决

filter-branch 存在一堆注意事项(下面会详述),即便你找出了必要的调用方法:

git filter-branch \
    --tree-filter 'mkdir -p my-module && \
                   git ls-files \
                       | grep -v ^src/ \
                       | xargs git rm -f -q && \
                   ls -d * \
                       | grep -v my-module \
                       | xargs -I files mv files my-module/' \
        --tag-name-filter 'echo "my-module-$(cat)"' \
    --prune-empty -- --all
git clone file://$(pwd) newcopy
cd newcopy
git for-each-ref --format="delete %(refname)" refs/tags/ \
    | grep -v refs/tags/my-module- \
    | git update-ref --stdin
git gc --prune=now

有些人可能注意到上面的 filter-branch 调用由于使用了 –tree-filter 会非常慢;你也可以使用 filter-branch 的 –index-filter 选项,将上述命令改为:

git filter-branch \
    --index-filter 'git ls-files \
                        | grep -v ^src/ \
                        | xargs git rm -q --cached;
                    git ls-files -s \
                        | sed "s%$(printf \\t)%&my-module/%" \
                        | git update-index --index-info;
                    git ls-files \
                        | grep -v ^my-module/ \
                        | xargs git rm -q --cached' \
    --tag-name-filter 'echo "my-module-$(cat)"' \
    --prune-empty -- --all
git clone file://$(pwd) newcopy
cd newcopy
git for-each-ref --format="delete %(refname)" refs/tags/ \
    | grep -v refs/tags/my-module- \
    | git update-ref --stdin
git gc --prune=now

然而,对于任何一个 filter-branch 命令,都存在一堆注意事项。首先,有些人可能想知道为什么我这里列出了五个命令给 filter-branch。尽管使用了 –all 和 –tag-name-filter,并且 filter-branch 的手册声称克隆足以清理旧对象,但为了在推送之前清理旧对象并避免新旧历史混合,仍然需要额外的步骤来删除其他标签并再次执行 gc。其他注意事项:

  • 提交消息没有被重写;因此,如果你的一些提交消息通过(缩写)sha1 引用之前的提交,重写后这些消息现在将引用不再属于历史记录的提交。最好将这些(缩写)sha1 引用重写为引用新的提交 ID。
  • –prune-empty 标志有时会漏掉本应修剪的提交,而且它还会修剪那些开始就是空提交(而非仅因过滤而变空)的提交。对于有意使用空提交进行版本控制和发布相关目的的仓库,这可能是有害的。
  • 上述命令与操作系统相关。GNU 与 BSD 在 sed、xargs 等命令上的差异经常困扰用户;我认为我未能让大多数人使用 –index-filter,因为 filter-branch 手册中唯一一个同时使用它并展示如何将所有内容移入子目录的示例是 Linux 特定的,而且读者不容易看出它有可移植性问题,因为它是静默地表现异常而非大声报错。
  • filter-branch 的 –index-filter 版本可能比 –tree-filter 版本快两到三倍,但两个 filter-branch 命令都比 filter-repo 慢好几个数量级。
  • 两个命令都假设所有文件名完全由 ASCII 字符组成(甚至制表符或双引号等特殊 ASCII 字符也会造成严重破坏,并可能导致文件丢失或命名错误)。

使用 fast-export/fast-import 解决

你可以通过类似以下的方式勉强拼凑起来:

git fast-export --no-data --reencode=yes --mark-tags --fake-missing-tagger \
    --signed-tags=strip --tag-of-filtered-object=rewrite --all \
    | grep -vP '^M [0-9]+ [0-9a-f]+ (?!src/)' \
    | grep -vP '^D (?!src/)' \
    | perl -pe 's%^(M [0-9]+ [0-9a-f]+ )(.*)$%\1my-module/\2%' \
    | perl -pe 's%^(D )(.*)$%\1my-module/\2%' \
    | perl -pe s%refs/tags/%refs/tags/my-module-% \
    | git -c core.ignorecase=false fast-import --date-format=raw-permissive \
          --force --quiet
git for-each-ref --format="delete %(refname)" refs/tags/ \
    | grep -v refs/tags/my-module- \
    | git update-ref --stdin
git reset --hard
git reflog expire --expire=now --all
git gc --prune=now

但这带来了一些严重的注意事项和限制:

  • 各种 grep 和正则替换操作针对整个 fast-export 流,因此可能意外破坏其未预期的部分,例如提交消息。如果你需要编辑文件内容并因此去掉了 –no-data 标志,它也可能最终破坏文件内容。
  • 此命令假设仓库中的所有文件名完全由 ASCII 字符组成,并且也不包含制表符或双引号等特殊字符。如果旧的 src/ 目录中存在这样的特殊文件名,即使本意是要保留它,也会被修剪掉。(在稍微不同的仓库重写中,这种编辑方式也有风险:通过在文件名末尾和某个前导目录名附近添加额外的双引号,可能会破坏包含特殊字符的文件名。)
  • 此命令会留下大量无用的空提交,并且没有实际的方法来修剪它们。(如果你尝试将此技术与其他工具结合来修剪空提交,那么你无法区分哪些提交是因过滤而变空需要移除的,哪些是在过滤之前就是空提交而你可能希望保留的。)
  • 引用其他提交哈希的提交消息现在将引用已不存在的旧提交。尝试编辑提交消息以更新它们,对于这种直接重写来说极其困难。

filter-repo 背后的设计理念

现有的仓库过滤工具没有一个能完全满足我的需求;它们都达不到我的期望。没有任何工具提供了下面我想要的八个特性中的任何一个,也没有任何工具同时提供了最后四个特性中的两个以上:

  1. [起始报告] 为用户提供仓库分析,帮助他们开始决定修剪或重命名什么,而不是期望他们猜测或寻找其他工具来解决。例如,第一次运行带有特殊标志(如 –analyze)时触发。

  2. [保留 vs. 移除] 除了提供让用户轻松移除选定路径的方法外,还应提供标志让用户只保留某些路径。当然,用户可以通过指定移除所有除他们想保留的路径之外的路径来变通,但需要指定在仓库任何版本中都曾经存在过的所有路径,有时可能非常痛苦。对于 filter-branch,使用像 git ls-files | grep -v ... | xargs -r git rm 这样的管道可能是一个合理的变通方法,但可能变得笨重,并且对用户不够直接;此外,这些命令通常依赖于操作系统(你能发现我提供的片段中的 GNU 特性吗?)。

  3. [重命名] 应该能够轻松地重命名文

相似文章

Git history 命令值得更多关注

Hacker News Top

文章重点介绍了新的 `git history` 命令及其 `fixup`、`reword` 和 `split` 子命令,这些命令提供了原子化且感知分支的提交历史编辑功能,带来了类似 jj 的益处,而无需切换版本控制系统。

Git 2.54 亮点速览

Lobsters Hottest

Git 2.54 带来全新的实验性 `git history` 命令,可在不碰工作区的情况下重写或拆分提交,另有 137 位贡献者带来的其他改进。

git rebase -i 并不可怕

Lobsters Hottest

解释 git 中的交互式变基命令,揭示其功能并强调通过中止和 reflog 的安全性。

接受混乱的 git 历史

Lobsters Hottest

一篇博客文章,讨论两种 git 工作流理念——频繁提交与谨慎变基——并解释为何在大型团队中,鉴于团队的韧性,混乱的历史是可以接受的,文中还提到了 GitHub 的新堆叠 PR 功能。