Git 附带了一个git filter-branch
命令是帮助完成此类任务的工具。注意git filter-branch
itself 不做这项工作:它只是您可以使用的一个工具,以便you可以完成这项工作。您仍然必须编写自己的命令。您最终可能会使用的是:
git filter-branch --tree-filter '<some command here>' --tag-name-filter cat -- --all
过滤器分支的作用是什么
这里有一个基本问题:任何提交一旦做出,就不能以任何方式更改。Nothing关于提交的信息可以更改:不能更改创建者的姓名、不能更改日期和时间戳、不能更改快照、不能更改其父提交的原始哈希 ID。所以git filter-branch
不这样做。
相反,它所做的是提取每个提交(从某些提交集中 - 在您的情况下,您希望这组提交是all提交),一次一个,然后在提取的犯罪。无论这做什么,过滤器分支都会创建一个new从结果提交。
如果新提交与原始提交完全、完全、100% 逐位相同,则这实际上重复使用了原始提交。否则,它会使用新的不同的哈希 ID 进行新的提交。
一旦您进行了新的不同提交,每个后续提交通常至少会略有不同:它将有不同的父级。过滤器分支工具会为您处理这个重新设置父级的过程。所以它所做的两项艰巨的工作是:
- 提取提交,运行过滤器,然后重新提交
- 适当更新父链接
剩下的艰巨工作当然是编写和运行过滤器。那个过滤器分支留给你了。
The --tree-filter
可能是最容易使用的过滤器,因此也是您想要的过滤器。值得注意的是--index-filter
速度要快得多,但如果您的工作是以某种方式修改每次提交中的快照,那么使用起来就会困难得多。 Filter-branch 有很多过滤选项because --tree-filter
是最慢的过滤器,因为它只适合改变快照. The --msg-filter
例如,可以编辑或替换每次提交中的消息文本。只要你想跑clang-format
不过,在每个快照中的所有文件上,坚持--tree-filter
.
命令行部分如何工作,更详细
让我们从一个只有三个提交的示例开始,简要了解一下它在实践中是如何工作的。这三个提交有又大又难看的哈希 ID,但我们会称它们为A
, B
, and C
为了简单起见。你从以下开始:
A <-B <-C <-- master
分行名称master
保存提交的哈希IDC
,这样我们(和 Git)就可以看到哪个是last犯罪。犯罪C
本身保存了commit的hash IDB
,并提交B
保存提交的哈希IDA
,这样 Git 就可以从最后一次提交向后工作到第一次提交。犯罪A
没有父母because这是第一个,因此这会让向后跟踪所有内容的操作停止。
To run git filter-branch
你可能会使用:
git filter-branch --tree-filter '<command to run>' -- master
最后的事情——master
—是你想要的分支名称filter-branch
当它列出它应该操作的所有提交时使用。也就是说,它将开始于master
然后倒退,直到不能再倒退为止。然后,它将复制每个提交,应用过滤器,然后重新提交。完成后,它将更新的一个分支名称是master
.
Using --all
告诉它从每个分支(以及标签和其他引用)开始,这可能会在stash
参考,有时--branches --tags
可能会更好,但是--all
至少是传统的)。我们会回到--tag-name-filter
稍后也可以选择。现在让我们继续master
.
The --
before master
是将放置分支名称的部分与其余选项分开,其中一些选项可能类似于有效的分支名称。这就是全部:只是标记“过滤器选项结束,分支名称开始”的样板。
最后我们看一下--tree-filter
不看how编写一个树过滤器。这仅仅意味着:运行树过滤器。因此,filter-branch 会将每个提交提取到一个临时目录中,该目录只保存已提交的文件。该临时目录没有.git
子目录,以及不是你的工作树。 (它实际上是-d
您传递的目录,或者默认情况下,filter-branch 创建的临时目录的子目录。)您的树过滤器应该:
- 应用您想要的任何更改
- 到当前工作目录中的每个文件
- 并递归地,到当前目录的每个子目录中的每个文件
例如,如果您想在每个文件中插入标题行,您可以使用:
find . -type f -print | xargs <command to insert header line in every file>
您可以将此命令放入脚本中,以便在使用前进行测试。如果clang-format
有正确的选项(它可能确实如此),您可能根本不需要脚本,只需指定:
--tree-filter 'clang-format <options>'
但无论哪种方式,filter-branch 都会使用 shell 的内置功能exec
运行树过滤器。因此,您必须确保您的命令包含有效的 shell 命令,并且没有return
or exit
其中的 shell 命令(至少在没有首先生成子 shell 的情况下)。如果您要运行的命令is您编写的脚本,请确保可以通过以下方式找到该脚本$PATH
,或提供脚本的完整路径名:
--tree-filter "sh $HOME/scripts/filter-script.sh"
例如。
让我们观看一个简单的过滤器的运行过程
我们假设提交A
里面有一个文件,README.md
。我们假设提交B
添加一个新的foo.cc
将重新格式化并提交的文件C
修改README.md
不改变foo.cc
根本不。您的过滤器只会更改任何.cc
and .h
文件,而不是README.md
。因此,首先,filter-branch 本身枚举所有提交,并将它们按适当的顺序排列:A
, then B
, then C
, 在这种情况下。
现在的树过滤器操作:
- 提取提交
A
;
- 在保存一个文件的临时目录中运行您的过滤器/脚本/命令
README.md
;
- 根据您的命令在临时目录中留下的任何内容进行新的提交。
因为你的命令没有触及README.md
,新提交与原始提交完全 100% 完全相同A
。因此,过滤器分支重新使用原始提交A
.
现在过滤分支移动到提交B
。它提取B
将两个文件放入(现在为空)临时目录并运行您的命令。这次你的命令改变了foo.cc
,虽然它仍然离开README.md
独自的。所以现在过滤器分支使用修改后的内容进行新的提交foo.cc
。重新使用原始提交的作者姓名和电子邮件等会保留原始元数据,但现在快照已更改,因此现在我们得到一个新的不同的哈希 ID,我们将调用B'
:
A--B--C <-- [original master]
\
B' [in progress]
过滤分支现在继续提交C
。它将所有文件提取到(重新清空的)临时目录中,因此您拥有相同的两个文件。您的过滤器现在已修改foo.cc
与操作 commit 内容时的方式相同B
。过滤器分支进行新的提交。新提交的快照已修改foo.cc
和相同的README.md
as in C
-新的foo.cc
匹配在B'
反而-and它有一个新的父母,B'
, 代替B
:最后一部分是过滤器分支为您处理的内容。所以现在我们有:
A--B--C <-- [original master]
\
B'-C' [in progress]
此时,我们已经用完了要复制的提交,因此过滤器分支执行最后几个技巧:
-
如果有标签指向现有提交,and你指定了一个--tag-name-filter
, Git 使new指向这些现有提交的副本的标签。任何指向的标签A
可以单独留下,但如果标签指向B
,filter-branch将其复制到指向的新标签B'
;如果一个标签指向C
,过滤器分支将其复制到指向的新分支C'
。这些新标签的名称来自--tag-name-filter
:旧名称进入过滤器,出来的是新标签名称。
如果你没有标签,这一切都无关紧要。
-
然后,对于您在命令行的分支部分中命名的每个分支,filter-branch 存储该分支的哈希 ID最后复制提交到该分支。所以这里,filter-branch 设置名称master
指向C'
.
如果出现任何问题,过滤器分支会将所有原始分支和标记名称复制到refs/original/
: 老师傅变了refs/original/refs/heads/master
。如果一切顺利,你最终会想扔掉refs/original/
names.
上面的最终绘图将是:
A--B--C <-- refs/original/refs/heads/master
\
B'-C' <-- master
正如 Schwern 的回答一样,如果一切都出现严重错误,您可能希望能够恢复。一种方法是在copy(例如,克隆)存储库,而不是原始存储库。另一种方法是注意,您始终可以将所有更新的引用强制恢复为保存的方式refs/original/
(但这通常需要一些编程)。