git filter-branch

优质
小牛编辑
126浏览
2023-12-01

命名

git-filter-branch - 重写分支

概要

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

描述

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

该命令将只重写positive命令行中提到的ref(例如,如果通过a..b,只会b被重写)。如果您没有指定过滤器,那么提交将被重新发送而不做任何更改,这通常没有任何影响。尽管如此,这对于补偿一些Git bug或将来可能会有用,因此这种用法是允许的。

注意:该命令.git/info/graftsrefs/replace/命名空间中承认文件和引用。如果您有任何定义的移植或替换参考,运行此命令将使它们永久。

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

始终验证重写的版本是否正确:原始参考文献(如果与重写版本不同)将存储在命名空间中refs/original/

请注意,由于此操作非常昂贵,因此使用该-d选项将临时目录从磁盘重定向到磁盘可能是一个好主意,例如在tmpfs上。据报道,加速非常明显。

过滤器

这些过滤器按以下列出的顺序应用。<command>参数总是使用eval命令在shell上下文中进行评估(出于技术原因,提交过滤器值得注意的例外)。在此之前,$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函数可以使用“original sha1 id”参数,如果提交已被重写,则输出“重写的sha1 id”,否则输出“original sha1 id”。map如果您的提交过滤器发出多个提交,该函数可以在单独的行上返回多个ids。

选项

--setup <command>

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

--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预计在标准输出上。

作为一个特殊的扩展,提交过滤器可能会发出多个提交id; 在那种情况下,原来承诺的改写孩子将把他们全部当作父母。

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

如果您不希望保留对单个父代的提交并且不对树进行更改git_commit_non_empty_tree "$@"git commit-tree "$@"那么也可以使用它。

--tag-name-filter <command>

这是重写标签名称的过滤器。传递时,将调用指向重写对象(或指向重写对象的标记对象)的每个标记ref。原始标签名称通过标准输入传递,新标签名称预计在标准输出上。

原始标签不会被删除,但可以被覆盖; 使用“--tag-name-filter cat”来简单地更新标签。在这种情况下,要非常小心,并确保在转换发生冲突的情况下备份旧标签。

几乎可以正确重写标签对象。如果标签附有消息,则会使用相同的消息,作者和时间戳创建新的标签对象。如果标签附有签名,签名将被剥离。根据定义,不可能保留签名。这是“几乎”适当的原因,因为理想情况下,如果标签没有改变(指向相同的对象,具有相同的名称等),它应该保留任何签名。情况并非如此,签名将永远被删除,买家要小心。也不支持更改作者或时间戳(或针对该问题的标记消息)。指向其他标签的标签将被重写为指向底层提交。

--subdirectory-filter <directory>

只能看看触及给定子目录的历史记录。结果将包含该目录(并且仅包含该目录)作为其项目根目录。意味着重新映射到祖先。

--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拒绝从现有的临时目录开始,或者当已经有ref时refs/original/,除非强制。

<rev-list options>…

参数git rev-list。这些选项包含的所有正面参考都被重写。您也可以指定诸如此类的选项--all,但您必须使用--它们将它们与git filter-branch选项分开。意味着重新映射到祖先。

重新映射到祖先

通过使用git-rev-list [1]参数,例如路径限制器,您可以限制被重写的修订集。然而,在命令行上的正面参考是有区别的:我们不会让这些限制器排除它们。为此,他们改写为指向最近的未被排除的祖先。

例子

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

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

但是,如果该文件在某个提交的树中不存在,则该树的简单操作rm filename将失败并提交。因此,您可以改为使用rm -f filename脚本。

使用它--index-filter可以git rm产生更快的版本。与使用一样,如果文件不在提交树中rm filenamegit 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

甚至更简单:

echo "$commit-id $graft-id" >> .git/info/grafts
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作为它们的父代提交,而不是合并提交。

注意提交引入的更改以及未被后续提交恢复的更改仍将位于重写的分支中。如果你想changes与提交一起扔掉,你应该使用交互模式git rebase

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

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 <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可以帮助您找到重命名。
  • 你真的过滤了所有的refs:--tag-name-filter cat -- --all在调用git-filter-branch时使用。

然后有两种方法可以获得较小的存储库。更安全的方法是克隆,这可以保持原来的原样。

  • 克隆它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
  • 使用所有reflogs git reflog expire --expire=now --all
  • 垃圾收集所有未被引用的对象git gc --prune=now(或者如果你的git-gc不够新以支持参数--prune,则git repack -ad; git prune改为使用)。

警告

git-filter-branch允许你对Git历史进行复杂的shell脚本重写,但如果你只是removing unwanted data像大文件或密码那样,你可能不需要这种灵活性。对于这些操作,您可能需要考虑BFG Repo-Cleaner,一种基于JVM的git-filter-branch替代方案,对于这些用例而言,其典型速度至少快10-50倍,并且具有不同的特征:

  • 任何特定版本的文件都会被精确清理once。与git-filter-branch不同的是,BFG不会让你有机会根据历史记录中何时或何时提交文件来处理文件。这个约束条件给了BFG的核心性能优势,并且非常适合清理不良数据的任务 - 您不关心where坏数据,您只需要它gone
  • 默认情况下,BFG充分利用多核机器,并行清理提交文件树。git-filter-branch清除按顺序提交(即以单线程方式)提交,尽管is可以在针对每个提交执行的脚本中编写包含它们自己的并行性的过滤器。
  • 该命令选项都远远超过git的过滤分支更严格,并致力于只是为了消除不必要的数据-例如任务:--strip-blobs-bigger-than 1M