我现在已经完成了我的一个小库。当我开始使用它时,我不知道 clang 格式。现在我想用它格式化整个存储库。我知道随着提交哈希的变化,这会破坏其他人的存储库。但是,由于还没有人使用我的库,这对我来说很好。
因此,我必须做什么才能为历史记录中的每个提交运行 clang 格式?
Git 附带了一个git filter-branch
命令,这是一个帮助完成此类任务的工具。 请注意,git filter-branch
本身并不能完成这项工作:它只是一个您可以使用的工具,以便您可以完成工作。 您仍必须编写自己的命令。 您最终可能会使用的那个是:
git filter-branch --tree-filter '<some command here>' --tag-name-filter cat -- --all
过滤器分支的作用
这里有一个基本问题:任何提交一旦完成,就不能以任何方式更改。提交的任何内容都无法更改:不是提交人员的姓名,不是日期和时间戳,不是快照,也不是其父提交的原始哈希 ID。 所以git filter-branch
不会那样做。
相反,它所做的是提取每个提交(从一组提交中 - 在您的情况下,您希望这组提交是所有提交),一次一个,然后在提取的提交上运行一些任意的、用户指定的命令。 无论这样做做什么,filter-branch 都会从结果中进行新的提交。
如果新提交与原始提交完全 100% 位对位相同,这实际上会重用原始提交。 否则,它会使用新的和不同的哈希 ID 进行新提交。
一旦你进行了一个新的和不同的提交,每个后续提交通常至少会略有不同:它将有一个不同的父级。 筛选器分支工具将为您处理此重定父级过程。 因此,它所做的两项艰巨工作是:
- 提取提交、运行筛选器并重新提交
- 根据需要更新父链接
剩下的艰巨工作当然是编写和运行过滤器。 那个,过滤器分支留给你。
--tree-filter
可能是最容易使用的过滤器,因此是您想要的过滤器。 值得注意的是,--index-filter
要快得多,但如果你的工作是以某种方式修改每次提交的快照,那么使用起来要困难得多。 Filter-branch 有很多过滤选项--tree-filter
因为它是最慢的过滤器,并且因为它只适用于更改快照。 例如,--msg-filter
可以编辑或替换每个提交中的消息文本。 但是,只要您想对每个快照中的所有文件运行clang-format
,请坚持使用--tree-filter
.
命令行部分如何工作,更详细
让我们简要看一下这在实践中是如何工作的,从一个只有三个提交的示例开始。 这三个提交都有丑陋的哈希 ID,但为了简单起见,我们将它们称为A
、B
和C
。 您从以下方面开始:
A <-B <-C <-- master
分支名称master
保存提交C
的哈希ID,以便我们(和Git)可以看到哪个是最后一次提交。 提交C
本身保存提交B
的哈希ID,而提交B
保存提交A
的哈希ID,以便Git可以从最后一次提交向后工作到第一次提交。 提交A
没有父级,因为它是第一个,因此这会让"跟随一切-向后"操作停止。
要运行git filter-branch
您可以使用:
git filter-branch --tree-filter '<command to run>' -- master
最后的东西 -master
- 是您希望filter-branch
在列出它应该操作的所有提交时使用的分支名称。 也就是说,它将从master
开始并向后工作,直到它不能再倒退。 然后,它将复制这些提交中的每一个,应用过滤器,然后重新提交。完成后,它将更新的一个分支名称是master
.
使用--all
告诉它从每个分支开始(以及标签和其他引用 - 这可能会在stash
ref 上行为不端,有时--branches --tags
可能会更好,但至少--all
是传统的)。 我们稍后也会回到--tag-name-filter
选项。 现在让我们一起去master
.
master
之前--
是将放置分支名称的部分与其余选项分开,其中一些选项可能类似于有效的分支名称。 这就是全部:只需样板来标记"过滤器选项的结束,分支名称的开始"。
最后,让我们看一下--tree-filter
而不看如何编写树过滤器。 这只是意味着:运行树过滤器。 因此,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
或exit
shell 命令(至少在没有首先生成子 shell 的情况下不会)。 如果要运行的命令是已编写的脚本,请确保可以通过$PATH
找到此脚本,或提供脚本的完整路径名:
--tree-filter "sh $HOME/scripts/filter-script.sh"
例如。
让我们看一个简单的过滤器操作
假设提交A
包含一个文件,README.md
. 假设 commitB
添加了一个新的foo.cc
文件,该文件将被重新格式化,并且提交C
修改README.md
而不更改foo.cc
。 您的过滤器仅更改任何.cc
和.h
文件,而不会更改README.md
。 因此,首先,filter-branch 本身枚举所有提交,并将它们按适当的顺序排列:A
,然后是B
,然后是C
,在这种情况下。
树过滤器操作现在:
- 提取提交
A
; - 在保存一个文件
README.md
的临时目录中运行过滤器/脚本/命令; - 从命令留在临时目录中的任何内容进行新提交。
由于您的命令不触及README.md
,因此新提交与原始A
完全相同,100%。 因此,过滤器分支重用原始提交A
。
现在过滤器分支移动到提交B
。 它将B
的两个文件提取到(现在为空的)临时目录中并运行您的命令。 这一次你的命令改变了foo.cc
,尽管它仍然让README.md
一个人呆着。 所以现在过滤器分支使用修改后的foo.cc
进行新提交。 重用原始提交的作者姓名和电子邮件等会保留原始元数据,但现在快照已更改,因此现在我们得到一个新的不同的哈希 ID,我们将调用B'
:
A--B--C <-- [original master]
B' [in progress]
过滤器分支现在继续提交C
。 它将其所有文件提取到(重新清空的)临时目录中,因此您拥有相同的两个文件。 您的过滤器现在修改foo.cc
的方式与对提交B
的内容进行操作时相同。 过滤器分支进行新的提交。 新提交的快照具有修改后的foo.cc
和与C
中相同的README.md
— 新foo.cc
与B'
中的快照匹配 — 并且它有一个新的父级B'
,而不是B
:最后一部分是过滤器分支为您处理的内容。 所以现在我们有:
A--B--C <-- [original master]
B'-C' [in progress]
在这一点上,我们已经用完了要复制的提交,所以 filter-branch 做了最后几个技巧:
如果存在指向现有提交的标签,并且您指定了
--tag-name-filter
,Git 会创建指向这些现有提交副本的新标签。 任何指向A
的标签都可以保留,但如果一个标签指向B
,filter-branch 会将其复制到指向B'
的新标签;如果一个标签指向C
,过滤器分支会将其复制到一个指向C'
的新标签。 这些新标签的名称来自--tag-name-filter
:旧名称进入过滤器,出现的是新标签名称。如果您没有标签,这一切都无关紧要。
然后,对于在命令行的分支部分中命名的每个分支,filter-branch 会将上次复制的提交的哈希 ID 存储到该分支中。 所以在这里,filter-branch 将名称设置为
master
指向C'
。
如果有任何问题,filter-branch 将所有原始分支和标签名称复制到refs/original/
:旧的主节点变为refs/original/refs/heads/master
。 如果一切顺利,你最终想扔掉refs/original/
的名字。
上述内容的最终图纸将是:
A--B--C <-- refs/original/refs/heads/master
B'-C' <-- master
就像施文的回答一样,如果一切都出了可怕的错误,你可能希望能够恢复。 一种方法是在存储库的副本(例如克隆)上运行过滤器分支,而不是在原始存储库上运行。 另一种方法是注意,您始终可以强制所有更新的引用恢复到它们保存在refs/original/
中的方式(但这通常需要一些编程)。
在你开始重写历史之前,我建议标记你当前的提交。这将允许您在出现严重错误时返回到原始版本。或者复制整个存储库,以防万一。
我们用git-filter-branch
批量改写历史.这有点像核瑞士军用电锯。我们将使用--tree-filter
来重写目录("树")和文件。--all
说要执行所有引用的提交(即所有分支和标签),而不仅仅是从当前结帐中可以访问的提交。
git filter-branch --tree-filter your_rewrite_command --all
这将检查每个提交,运行your_rewrite_command
,并使用结果重写提交。
我建议在运行git-filter-branch
之前编写一个小的 shell 脚本来进行重写和测试。使用git ls-files
获取提交中所有文件的列表,并对每个文件运行clang-format
。