Git
English ▾ 主题 ▾ 最新版本 ▾ git-filter-branch 最后更新于 2.44.0

名称

git-filter-branch - 重写分支

概要

git filter-branch [--setup <command>] [--subdirectory-filter <directory>]
	[--env-filter <command>] [--tree-filter <command>]
	[--index-filter <command>] [--parent-filter <command>]
	[--msg-filter <command>] [--commit-filter <command>]
	[--tag-name-filter <command>] [--prune-empty]
	[--original <namespace>] [-d <directory>] [-f | --force]
	[--state-branch <branch>] [--] [<rev-list-options>…​]

警告

git filter-branch 存在许多陷阱,这些陷阱可能会导致对预期历史记录重写的非明显修改(并且可能让你没有足够的时间来调查此类问题,因为它性能极差)。这些安全性和性能问题无法向后兼容地修复,因此不建议使用它。请使用其他历史记录过滤工具,例如 git filter-repo。如果你仍然需要使用 git filter-branch,请仔细阅读 安全(以及 性能)以了解 filter-branch 的地雷,然后尽可能警惕地避免那里列出的许多危险。

描述

允许您通过重写 <rev-list-options> 中提到的分支来重写 Git 修订历史记录,并在每个修订记录上应用自定义过滤器。这些过滤器可以修改每个树(例如,删除文件或对所有文件运行 perl 重写)或有关每个提交的信息。否则,所有信息(包括原始提交时间或合并信息)都将保留。

该命令只会重写命令行中提到的引用(例如,如果您传递a..b,则只会重写b)。如果您未指定任何过滤器,则提交将在没有任何更改的情况下重新提交,这通常不会产生任何影响。但是,这将来可能有助于弥补某些 Git 错误或类似情况,因此允许这种用法。

注意:此命令会遵守 .git/info/grafts 文件和 refs/replace/ 命名空间中的引用。如果您已定义任何移植或替换引用,则运行此命令将使它们永久化。

警告!重写的历史记录将对所有对象具有不同的对象名称,并且不会与原始分支合并。您将无法轻松地将重写分支推送到原始分支之上并分发它。如果您不了解全部含义,请勿使用此命令,并且无论如何,如果一个简单的单个提交足以解决您的问题,请避免使用它。(有关重写已发布历史记录的更多信息,请参阅 git-rebase[1] 中的“从上游变基中恢复”部分。)

始终验证重写版本是否正确:如果原始引用与重写引用不同,则将存储在 refs/original/ 命名空间中。

请注意,由于此操作非常占用 I/O,因此最好使用 -d 选项将临时目录重定向到磁盘外,例如在 tmpfs 上。据报道,加速效果非常明显。

过滤器

过滤器按如下所示的顺序应用。<command> 参数始终在 shell 上下文中使用 eval 命令进行评估(出于技术原因,提交过滤器除外)。在此之前,$GIT_COMMIT 环境变量将被设置为包含正在重写的提交的 ID。此外,GIT_AUTHOR_NAME、GIT_AUTHOR_EMAIL、GIT_AUTHOR_DATE、GIT_COMMITTER_NAME、GIT_COMMITTER_EMAIL 和 GIT_COMMITTER_DATE 取自当前提交并导出到环境中,以便影响由 git-commit-tree[1] 在过滤器运行后创建的替换提交的作者和提交者身份。

如果任何 <command> 的评估返回非零退出状态,则整个操作将中止。

有一个map 函数可用,它接受“原始 sha1 id”参数,如果提交已重写,则输出“重写 sha1 id”,否则输出“原始 sha1 id”;如果你的提交过滤器发出了多个提交,则map 函数可以在单独的行上返回多个 ID。

选项

--setup <command>

这不是为每个提交执行的真实过滤器,而是在循环之前的一次性设置。因此,尚未定义任何特定于提交的变量。此处定义的函数或变量可以在以下过滤器步骤中使用或修改,但出于技术原因,提交过滤器除外。

--subdirectory-filter <directory>

仅查看触及给定子目录的历史记录。结果将包含该目录(且仅包含该目录)作为其项目根目录。暗示 重新映射到祖先

--env-filter <command>

如果您只需要修改将执行提交的环境,则可以使用此过滤器。具体来说,您可能希望重写作者/提交者姓名/电子邮件/时间环境变量(有关详细信息,请参阅 git-commit-tree[1])。

--tree-filter <command>

这是用于重写树及其内容的过滤器。该参数在 shell 中进行评估,工作目录设置为签出树的根目录。然后按原样使用新树(新文件会自动添加,消失的文件会自动删除 - .gitignore 文件或任何其他忽略规则均无效!)。

--index-filter <command>

这是用于重写索引的过滤器。它类似于树过滤器,但不会签出树,这使其速度更快。通常与 git rm --cached --ignore-unmatch ... 一起使用,请参见下面的示例。对于棘手的情况,请参阅 git-update-index[1]

--parent-filter <command>

这是用于重写提交的父列表的过滤器。它将在 stdin 上接收父字符串,并应在 stdout 上输出新的父字符串。父字符串的格式在 git-commit-tree[1] 中进行了描述:初始提交为空,普通提交为“-p parent”,合并提交为“-p parent1 -p parent2 -p parent3 …​”。

--msg-filter <command>

这是用于重写提交消息的过滤器。该参数在 shell 中进行评估,原始提交消息位于标准输入中;其标准输出用作新的提交消息。

--commit-filter <command>

这是用于执行提交的过滤器。如果指定了此过滤器,它将代替 git commit-tree 命令被调用,参数格式为“<TREE_ID> [(-p <PARENT_COMMIT_ID>)…​]”以及 stdin 上的日志消息。提交 ID 应在 stdout 上。

作为特殊扩展,提交过滤器可以发出多个提交 ID;在这种情况下,原始提交的重写子级将具有所有这些 ID 作为父级。

您可以在此过滤器中使用 map 便利函数以及其他便利函数。例如,调用 skip_commit "$@" 将省略当前提交(但不会省略其更改!如果需要省略更改,请改用 git rebase)。

如果不想保留只有一个父级且对树没有进行任何更改的提交,则还可以使用 git_commit_non_empty_tree "$@" 代替 git commit-tree "$@"

--tag-name-filter <command>

这是用于重写标签名称的过滤器。当传递时,它将被调用以获取指向重写对象(或指向重写对象的标签对象)的每个标签引用。原始标签名称通过标准输入传递,新的标签名称预期在标准输出上。

原始标签不会被删除,但可以被覆盖;使用“--tag-name-filter cat”来简单更新标签。在这种情况下,请务必小心,并确保在转换出现问题时备份旧标签。

支持对标签对象进行几乎正确的重写。如果标签附加了消息,则将创建一个具有相同消息、作者和时间戳的新标签对象。如果标签附加了签名,则签名将被删除。根据定义,不可能保留签名。之所以称其为“几乎”正确,是因为理想情况下,如果标签没有更改(指向同一个对象,具有相同的名称等),则它应该保留任何签名。但事实并非如此,签名将始终被删除,购买者需谨慎。也没有支持更改作者或时间戳(或标签消息)。指向其他标签的标签将被重写为指向底层提交。

--prune-empty

一些过滤器会生成空提交,这些提交不会修改树。此选项指示 git-filter-branch 删除此类提交,前提是它们正好有一个或零个未修剪的父提交;因此,合并提交将保持完整。此选项不能与--commit-filter一起使用,尽管可以通过在提交过滤器中使用提供的git_commit_non_empty_tree函数来实现相同的效果。

--original <namespace>

使用此选项设置存储原始提交的命名空间。默认值为refs/original

-d <directory>

使用此选项设置用于重写的临时目录的路径。应用树过滤器时,命令需要将树临时检出到某个目录,如果项目很大,这可能会消耗大量空间。默认情况下,它在.git-rewrite/目录中执行此操作,但您可以通过此参数覆盖该选择。

-f
--force

git filter-branch 拒绝以现有临时目录或当已经有以refs/original/开头的引用时启动,除非强制。

--state-branch <branch>

此选项将导致从旧对象到新对象的映射在启动时从命名分支加载,并在退出时保存为该分支的新提交,从而实现大型树的增量操作。如果<branch>不存在,则将创建它。

<rev-list options>…​

git rev-list的参数。这些选项包含的所有正引用都将被重写。您还可以指定诸如--all之类的选项,但必须使用--将它们与git filter-branch选项分开。暗示重映射到祖先

重映射到祖先

通过使用git-rev-list[1]参数(例如,路径限制器),您可以限制要重写的修订集。但是,命令行上的正引用是区分的:我们不允许路径限制器将其排除在外。为此,它们将被重写为指向未被排除的最近祖先。

退出状态

成功时,退出状态为0。如果过滤器找不到任何要重写的提交,则退出状态为2。在任何其他错误情况下,退出状态可能是任何其他非零值。

示例

假设您想从所有提交中删除一个文件(包含机密信息或版权侵权)

git filter-branch --tree-filter 'rm filename' HEAD

但是,如果文件不存在于某个提交的树中,则简单的rm filename将对此树和提交失败。因此,您可能希望改用rm -f filename作为脚本。

--index-filtergit rm一起使用会产生一个速度明显更快的版本。与使用rm filename一样,如果文件不存在于提交的树中,则git rm --cached filename将失败。如果您想“完全忘记”一个文件,那么它何时进入历史并不重要,因此我们还添加了--ignore-unmatch

git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD

现在,您将获得保存在 HEAD 中的重写历史记录。

将存储库重写为好像foodir/是其项目根目录,并丢弃所有其他历史记录

git filter-branch --subdirectory-filter foodir -- --all

因此,例如,您可以将库子目录转换为其自己的存储库。请注意将filter-branch选项与修订选项分隔开的--,以及重写所有分支和标签的--all

将一个提交(通常位于另一个历史记录的顶端)设置为当前初始提交的父提交,以便将其他历史记录粘贴到当前历史记录之后

git filter-branch --parent-filter 'sed "s/^\$/-p <graft-id>/"' HEAD

(如果父字符串为空 - 当我们处理初始提交时会发生这种情况 - 将graftcommit添加为父提交)。请注意,这假设历史记录只有一个根(即,没有发生没有共同祖先的合并)。如果不是这种情况,请使用

git filter-branch --parent-filter \
	'test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>" || cat' HEAD

或者更简单地

git replace --graft $commit-id $graft-id
git filter-branch $graft-id..HEAD

从历史记录中删除由“Darl McBribe”创作的提交

git filter-branch --commit-filter '
	if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
	then
		skip_commit "$@";
	else
		git commit-tree "$@";
	fi' HEAD

函数skip_commit定义如下

skip_commit()
{
	shift;
	while [ -n "$1" ];
	do
		shift;
		map "$1";
		shift;
	done;
}

移位魔法首先丢弃树 ID,然后丢弃 -p 参数。请注意,这可以正确处理合并!如果 Darl 在 P1 和 P2 之间提交了合并,它将被正确传播,并且合并的所有子节点将成为合并提交,其父节点为 P1、P2,而不是合并提交。

注意提交引入的更改,并且后续提交未撤消的更改,仍将存在于重写分支中。如果您想连同提交一起丢弃更改,则应使用git rebase的交互模式。

您可以使用--msg-filter重写提交日志消息。例如,可以通过这种方式删除由git svn创建的存储库中的git svn-id字符串

git filter-branch --msg-filter '
	sed -e "/^git-svn-id:/d"
'

如果您需要将Acked-by行添加到例如最后 10 个提交(其中没有一个是合并提交),请使用此命令

git filter-branch --msg-filter '
	cat &&
	echo "Acked-by: Bugs Bunny <[email protected]>"
' HEAD~10..HEAD

--env-filter选项可用于修改提交者和/或作者身份。例如,如果您发现由于用户.email配置错误导致您的提交具有错误的身份,您可以在发布项目之前进行更正,如下所示

git filter-branch --env-filter '
	if test "$GIT_AUTHOR_EMAIL" = "root@localhost"
	then
		[email protected]
	fi
	if test "$GIT_COMMITTER_EMAIL" = "root@localhost"
	then
		[email protected]
	fi
' -- --all

要将重写限制为历史记录的一部分,请除了新分支名称外,还指定一个修订范围。新分支名称将指向git rev-list对此范围的打印将打印的最顶层修订版。

考虑以下历史记录

     D--E--F--G--H
    /     /
A--B-----C

要仅重写提交 D、E、F、G、H,但保留 A、B 和 C,请使用

git filter-branch ... C..H

要重写提交 E、F、G、H,请使用以下任一方法

git filter-branch ... C..H --not D
git filter-branch ... D..H --not C

将整个树移动到子目录中,或从那里删除它

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-branch 可用于摆脱一部分文件,通常使用--index-filter--subdirectory-filter的某种组合。人们期望生成的存储库比原始存储库小,但是您需要采取一些额外的步骤才能真正使其变小,因为 Git 竭尽全力不丢失您的对象,直到您告诉它为止。首先确保

  • 您确实删除了所有文件名的变体,如果 Blob 在其生命周期内被移动。git log --name-only --follow --all -- filename可以帮助您查找重命名。

  • 您确实过滤了所有引用:在调用 git-filter-branch 时使用--tag-name-filter cat -- --all

然后有两种方法可以获得更小的存储库。更安全的方法是克隆,它会保持您的原始存储库不变。

  • 使用git clone file:///path/to/repo克隆它。克隆将没有删除的对象。请参阅git-clone[1]。(请注意,使用普通路径克隆只会硬链接所有内容!)

如果您真的不想克隆它,无论出于何种原因,请改为检查以下几点(按此顺序)。这是一种非常具有破坏性的方法,因此请备份或返回到克隆它。您已被警告。

  • 删除由 git-filter-branch 备份的原始引用:例如,git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

  • 使用git reflog expire --expire=now --all使所有 reflog 过期。

  • 使用git gc --prune=now垃圾回收所有未引用的对象(或者如果您的 git-gc 不够新以支持--prune的参数,请改用git repack -ad; git prune)。

性能

git-filter-branch 的性能非常慢;其设计使得向后兼容的实现永远不可能快速。

  • 在编辑文件时,git-filter-branch 根据设计会检出原始存储库中存在的每个提交。如果您的存储库有10^5个文件和10^5个提交,但每个提交只修改五个文件,那么 git-filter-branch 将使您进行10^10次修改,尽管最多只有5*10^5个唯一 Blob。

  • 如果您尝试作弊并尝试使 git-filter-branch 仅对提交中修改的文件起作用,则会发生两件事

    • 每当用户只是尝试重命名文件时,您都会遇到删除问题(因为尝试删除不存在的文件看起来像一个无操作;当重命名通过任意用户提供的 shell 进行时,需要一些欺骗手段才能跨文件重命名重新映射删除)

    • 即使你成功地使用了重命名时删除映射的欺骗手段,从技术上讲你仍然违反了向后兼容性,因为用户被允许以依赖提交拓扑的方式过滤文件,而不是仅仅根据文件内容或名称进行过滤(尽管这在实际使用中尚未观察到)。

  • 即使你不需要编辑文件,而只是想例如重命名或删除一些文件,从而可以避免检出每个文件(即你可以使用 --index-filter),你仍然在传递用于过滤器的 shell 代码片段。这意味着对于每个提交,你都必须准备一个 git 仓库,以便在其中运行这些过滤器。这是一个相当大的设置。

  • 此外,git-filter-branch 还会为每个提交创建或更新多个文件。其中一些文件用于支持 git-filter-branch 提供的便利函数(例如 map()),而另一些文件则用于跟踪内部状态(但也可以被用户过滤器访问;git-filter-branch 的一个回归测试就是这样做的)。这实际上相当于使用文件系统作为 git-filter-branch 和用户提供的过滤器之间的 IPC 机制。磁盘往往是一种缓慢的 IPC 机制,并且写入这些文件也实际上代表了一个强制的同步点,我们在每次提交时都会遇到它。

  • 用户提供的 shell 命令很可能涉及一系列命令管道,从而导致每个提交创建许多进程。在不同的操作系统之间,创建和运行另一个进程所需的时间差异很大,但在任何平台上,相对于调用一个函数来说,它都非常慢。

  • git-filter-branch 本身是用 shell 编写的,这有点慢。这是唯一一个可以向后兼容地修复的性能问题,但与上面提到的 git-filter-branch 设计固有的问题相比,工具本身的语言是一个相对次要的问题。

    • 旁注:不幸的是,人们倾向于专注于用 shell 编写的方面,并定期询问是否可以将 git-filter-branch 重写为另一种语言来解决性能问题。这不仅忽略了设计中更大的内在问题,而且效果也没有你预期的那么好:如果 git-filter-branch 本身不是 shell,那么便利函数(map()、skip_commit() 等)和 --setup 参数将不再能够在程序开始时执行一次,而是需要将其添加到每个用户过滤器前面(并因此在每次提交时重新执行)。

git filter-repo 工具是 git-filter-branch 的替代方案,它没有这些性能问题或安全问题(如下所述)。对于那些依赖于 git-filter-branch 的现有工具,git filter-repo 还提供了 filter-lamely,一个可直接替换 git-filter-branch 的工具(有一些注意事项)。虽然 filter-lamely 存在与 git-filter-branch 相同的所有安全问题,但它至少在一定程度上缓解了性能问题。

安全

git-filter-branch 充满了陷阱,导致各种方式很容易损坏仓库或最终得到比你开始时更糟糕的混乱状态

  • 某人可能有一组“工作并经过测试的过滤器”,他们会记录这些过滤器或提供给同事,然后同事在不同的操作系统上运行这些过滤器,而这些命令在该操作系统上不起作用/未经测试(git-filter-branch 手册页中的一些示例也受此影响)。BSD 与 GNU 用户空间的差异可能会造成严重影响。如果幸运的话,会输出错误消息。但同样可能的是,这些命令要么没有执行请求的过滤,要么通过进行一些不需要的更改而静默地破坏了仓库。不需要的更改可能只影响少数几个提交,因此也不一定很明显。(问题不一定会很明显的事实意味着它们很可能在重写的历史记录被使用相当长一段时间后才会被注意到,到那时,再进行另一次大规模的重写来修复就很难让人接受了。)

  • 由于文件名中包含空格会导致 shell 管道出现问题,因此 shell 代码片段通常无法正确处理文件名中包含空格的情况。并不是每个人都熟悉 find -print0、xargs -0、git-ls-files -z 等。即使熟悉这些命令的人也可能认为这些标志不相关,因为在进行过滤的人加入项目之前,其他人已经重命名了仓库中任何此类文件。而且通常,即使是那些熟悉处理包含空格的参数的人,也可能不会这样做,因为他们并没有考虑到所有可能出错的事情。

  • 非 ASCII 文件名可能会被静默删除,即使它们在所需的目录中。保留所需的路径通常是使用诸如 git ls-files | grep -v ^WANTED_DIR/ | xargs git rm 之类的管道来完成的。ls-files 只有在需要时才会引用文件名,因此人们可能没有注意到其中一个文件与正则表达式不匹配(至少在为时已晚之前)。是的,了解 core.quotePath 的人可以避免这种情况(除非他们有其他特殊字符,如 \t、\n 或 "),并且使用 ls-files -z 和 grep 以外的工具的人也可以避免这种情况,但这并不意味着他们会这样做。

  • 同样,当移动文件时,可能会发现包含非 ASCII 或特殊字符的文件名最终位于不同的目录中,该目录包含双引号字符。(从技术上讲,这与上面引用的问题相同,但也许是一种有趣且不同的问题表现形式。)

  • 意外混合旧历史和新历史太容易了。使用任何工具都可能发生这种情况,但 git-filter-branch 几乎是在邀请这种情况的发生。如果幸运的话,唯一的缺点是用户会感到沮丧,因为他们不知道如何缩小仓库并删除旧内容。如果不走运,他们会合并旧历史和新历史,最终得到每个提交的多个“副本”,其中一些副本包含不需要的文件或敏感文件,而另一些副本则没有。这可以通过多种不同的方式发生

    • 默认情况下只进行部分历史重写(--all 不是默认值,而且很少有示例显示它)

    • 没有自动的后运行清理

    • --tag-name-filter(用于重命名标签时)不会删除旧标签,而只是添加具有新名称的新标签

    • 提供的教育信息很少,无法告知用户重写的影响以及如何避免混合旧历史和新历史。例如,此手册页讨论了用户需要了解他们需要在其所有分支上重新设置其更改的基础,以建立在新历史记录之上(或删除并重新克隆),但这只是需要考虑的多个问题之一。有关更多详细信息,请参阅 git filter-repo 手册页的“讨论”部分。

  • 带注释的标签可能会意外地转换为轻量级标签,这是由于以下两个问题之一造成的

    • 某人可以进行历史重写,意识到自己搞砸了,从 refs/original/ 中的备份中恢复,然后重新执行他们的 git-filter-branch 命令。(refs/original/ 中的备份不是真正的备份;它首先取消引用标签。)

    • 在你的 <rev-list-options> 中使用 --tags 或 --all 运行 git-filter-branch。为了将带注释的标签保留为带注释的,你必须使用 --tag-name-filter(并且必须没有在之前失败的重写中从 refs/original/ 中恢复)。

  • 任何指定编码的提交消息都将因重写而损坏;git-filter-branch 会忽略编码,获取原始字节,并将其馈送到 commit-tree 而没有告诉它正确的编码。(无论是否使用 --msg-filter,都会发生这种情况。)

  • 提交消息(即使它们都是 UTF-8)默认情况下会因未更新而损坏——提交消息中对其他提交哈希的任何引用现在都将指向不再存在的提交。

  • 没有帮助用户找到他们应该删除哪些不需要的垃圾的功能,这意味着他们更有可能进行不完整或部分清理,这有时会导致混淆,并导致人们浪费时间试图理解。(例如,人们倾向于只查找要删除的大文件,而不是大目录或扩展名,并且一旦他们这样做,那么之后使用新仓库并查看历史记录的人会注意到一个构建工件目录,其中包含一些文件但不包含其他文件,或者一个依赖项缓存(node_modules 或类似的),由于缺少一些文件,因此永远无法正常工作。)

  • 如果未指定 --prune-empty,则过滤过程可能会创建大量令人困惑的空提交

  • 如果指定了 --prune-empty,则在过滤操作之前有意放置的空提交也会被修剪,而不仅仅是修剪由于过滤规则而变为空的提交。

  • 如果指定了 --prune-empty,有时空提交会被遗漏并保留下来(一个不太常见的错误,但确实会发生……)

  • 一个次要问题,但目标是更新仓库中所有姓名和电子邮件的用户可能会被引导使用 --env-filter,它只会更新作者和提交者,而不会更新标签者。

  • 如果用户提供的 --tag-name-filter 将多个标签映射到相同的名称,则不会提供任何警告或错误;git-filter-branch 只是按照一些未记录的预定义顺序覆盖每个标签,最终只留下一个标签。(一个 git-filter-branch 回归测试需要这种令人惊讶的行为。)

此外,git-filter-branch 的性能不佳通常会导致安全问题

  • 除非你只是进行简单的修改,例如删除几个文件,否则想出正确的 shell 代码片段来执行你想要的过滤有时很困难。不幸的是,人们通常通过尝试来了解代码片段是否正确,但正确性可能会因特殊情况而异(文件名中的空格、非 ASCII 文件名、有趣的作者姓名或电子邮件、无效的时区、是否存在移植或替换对象等),这意味着他们可能需要等待很长时间,遇到错误,然后重新开始。git-filter-branch 的性能非常糟糕,以至于这个循环非常痛苦,减少了仔细重新检查的时间(更不用说它对进行重写的人的耐心有什么影响,即使他们确实有更多可用时间)。这个问题更加复杂,因为来自损坏过滤器的错误可能很长时间都不会显示出来和/或淹没在大量的输出中。更糟糕的是,损坏的过滤器通常会导致静默的错误重写。

  • 最糟糕的是,即使用户最终找到了有效的命令,他们自然也希望共享这些命令。但他们可能不知道他们的仓库中没有其他人仓库中的一些特殊情况。因此,当其他人使用不同的仓库运行相同的命令时,他们会遇到上述问题。或者,用户只是运行了确实针对特殊情况进行了审查的命令,但他们在不同的操作系统上运行它,而它在那里不起作用,如上所述。

Git

git[1] 套件的一部分

scroll-to-top