从 filter-branch 转换
本文档面向熟悉 filter-branch 并希望学习如何转换到使用 filter-repo 的人。
目录
- 基本差异
- [filter-branch 示例的转换](#filter-branch 示例的转换)
- 速查表:额外的转换示例
基本差异
使用 git filter-branch 时,你有一个 git 仓库,其中每个提交(在你指定的分支或修订版本内)都会被检出,然后你运行一个或多个 shell 命令来将工作副本转换为你想要的最终状态。
使用 git filter-repo 时,你实际上获得了一个编辑工具,可以操作仓库的 fast-export 序列化。这意味着有一个包含仓库所有内容的输入流,你通常不是以要运行的命令形式指定过滤器,而是使用许多常见的预定义过滤器,这些过滤器提供各种方式来基于仓库的组件(如路径名、文件内容、用户名或电子邮件等)对仓库进行切片、切块或修改。这使得常见操作更容易,即使它不如 shell 回调那么灵活。对于需要更复杂或特殊处理的情况,filter-repo 提供了 Python 回调,可以对从 fast-export 流中填充的数据结构进行操作,几乎可以做任何你想做的事。
filter-branch 默认在仓库的一个子集上工作,并要求你指定一个或多个分支,这意味着你需要指定 -- --all 来修改所有提交。相比之下,filter-repo 默认重写所有内容,如果你想限制到某个特定的分支集或提交范围,你需要指定 --refs <rev-list-args>。(但是,以连字符开头的任何 <rev-list-args> 都不被 filter-repo 接受,因为它们看起来像是不同选项的开始。)
filter-repo 还自动处理额外的问题,比如重写旧commit ID 的提交消息,使其引用重写后的commit ID,删除由于指定的过滤器而变为空的提交,以及在过滤操作结束时自动缩小和 gc 仓库。
filter-branch 示例的转换
删除文件
filter-branch 手册提供了三个删除单个文件的不同示例,基于不同级别的易用性与谨慎性和性能:
git filter-branch --tree-filter 'rm filename' HEAD
- 这个命令会检出每个提交,运行 rm filename 命令,然后重新提交。
- 如果文件不存在,rm 命令会报错,但过滤器会继续执行。
- 这种方法最慢,因为它需要在每个提交上实际检出文件。
git filter-branch --tree-filter 'rm -f filename' HEAD
- 这个命令与第一个类似,但使用了 rm -f,这意味着"强制删除"。
- 即使文件不存在,也不会报错。
- 仍然会检出每个提交,但比第一个命令稍微健壮一些。
git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD
- 这个命令使用 --index-filter,它只操作 Git 索引,不会检出文件。
- git rm --cached 从 Git 索引中删除文件,但不触及工作目录。
- --ignore-unmatch 选项使得即使文件不在某些提交中也不会报错。
这是最快和最高效的方法,特别是对于大型仓库。
所有这些在git filter-repo都变成了
git filter-repo --invert-paths --path filename
提取子目录
通过以下方式提取子目录:
git filter-branch --subdirectory-filter foodir -- --all
这是最容易转换的命令之一;它只是变成了
git filter-repo --subdirectory-filter foodir
将整个树移动到子目录
保留所有文件但将它们放在新的子目录中:
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
变成了
git filter-repo --to-subdirectory-filter newsubdir
删除某个作者的提交
警告:这对于 filter-branch 和 filter-repo 都是一个糟糕的例子。它并不从仓库中删除用户所做的更改,它只是删除有问题的提交,同时将其更改压缩到任何后续提交中,就好像后续作者也对这些更改负责一样。如果你在看这个例子,git rebase 可能更适合你真正想要的。(另见这个解释 rebase 和 filter-repo 之间差异的说明)
这个 filter-branch 例子
git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
then
skip_commit "$@";
else
git commit-tree "$@";
fi' HEAD
变成了
git filter-repo --commit-callback '
if commit.author_name == b"Darl McBribe":
commit.skip()
'
重写提交消息 -- 删除文本
通过以下方式从提交消息中删除 git-svn-id: 行:
git filter-branch --msg-filter '
sed -e "/^git-svn-id:/d"
'
变成了
git filter-repo --message-callback '
return re.sub(b"^git-svn-id:.*\n", b"", message, flags=re.MULTILINE)
'
重写提交消息 -- 添加文本
通过以下方式向最后十个提交添加 Acked-by 行:
git filter-branch --msg-filter '
cat &&
echo "Acked-by: Bugs Bunny <bunny@bugzilla.org>"
' master~10..master
变成了
git filter-repo --message-callback '
return message + b"Acked-by: Bugs Bunny <bunny@bugzilla.org>\n"
' --refs master~10..master
更改作者/提交者(/标签者?)信息
git filter-branch --env-filter '
if test "$GIT_AUTHOR_EMAIL" = "root@localhost"
then
GIT_AUTHOR_EMAIL=john@example.com
fi
if test "$GIT_COMMITTER_EMAIL" = "root@localhost"
then
GIT_COMMITTER_EMAIL=john@example.com
fi
' -- --all
变成了
# 确保 '<john@example.com> <root@localhost>' 是 .mailmap 中的一行,然后:
git filter-repo --use-mailmap
或
git filter-repo --email-callback '
return email if email != b"root@localhost" else b"john@example.com"
'
(作为额外的好处,这两种 filter-repo 替代方案也会修复标签者的电子邮件,而 filter-branch 示例不会)