简体中文 ▾ 主题 ▾ 最新版本 ▾ 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/ 命名空间中的引用。如果您定义了任何 graft 或替换引用,运行此命令将使它们永久生效。

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

始终验证重写后的版本是否正确:原始引用(如果与重写后的不同)将被存储在 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;在这种情况下,原始提交的重写子项将以它们全部作为父项。

您可以在此过滤器中使用 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 作为脚本。

使用 git rm--index-filter 产生了显着更快的版本。与使用 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"
'

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

git filter-branch --msg-filter '
	cat &&
	echo "Acked-by: Bugs Bunny <bunny@bugzilla.org>"
' HEAD~10..HEAD

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

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

要将重写限制在历史的一部分,请在新的分支名称之外指定一个修订范围。新分支名称将指向 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> 中运行 git-filter-branch 并使用 --tags 或 --all。为了保留带注解的标签作为带注解的,您必须使用 --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 文件名、奇怪的作者姓名或电子邮件、无效的时区、graft 或替换对象的存在等)而异,这意味着他们可能不得不长时间等待、遇到错误,然后重新开始。git-filter-branch 的性能非常差,以至于这个周期很痛苦,减少了仔细重新检查的时间(更不用说它对执行重写的人的耐心造成的打击,即使他们技术上有更多的时间可用)。这个问题因以下几点而加剧:有问题的过滤器的错误可能不会很快显示出来,并且/或者会在大量输出中丢失。更糟糕的是,有问题的过滤器通常只会导致静默的错误重写。

  • 最糟糕的是,即使用户最终找到了有效的命令,他们自然也想共享它们。但他们可能不知道他们的存储库没有一些特殊情况,而别人的有。因此,当另一个具有不同存储库的人运行相同的命令时,他们会遇到上述问题。或者,用户只是运行了实际上经过特殊情况验证的命令,但他们在不同的操作系统上运行它,而它在那里不起作用,如上所述。

GIT

Git[1] 套件的一部分