如何对整个git历史记录进行clang-format格式化?

4

我现在已经完成了我的一个小库。在我开始使用它时,我不知道clang-format。现在我想用它来格式化整个代码库。我知道这会破坏其他人的代码库,因为提交哈希值会发生变化。然而,由于还没有人使用我的库,所以这对我来说没有问题。

那么,我需要怎么做才能在历史记录中的每个提交上运行clang-format呢?


你有多少次提交? - hyde
当你说“整个代码库”时,你是指所有文件?还是每个提交中的所有文件? - Schwern
1
@Schwern 我想要格式化所有提交中的所有文件。 - jan.sende
@hyde 大约有130个,所以手动操作并不是很实际... - jan.sende
2个回答

9

Git带有一个git filter-branch命令,这是一种帮助处理此类任务的工具。请注意,git filter-branch本身不会完成任务,它只是一个你可以使用的工具,以使能够完成任务。你仍然需要编写自己的命令。最终使用的命令可能是:

git filter-branch --tree-filter '<some command here>' --tag-name-filter cat -- --all

filter-branch的功能

有一个基本问题:一旦提交,就不能以任何方式进行修改。提交的任何内容都不能更改:不是制作它的人的姓名,也不是日期和时间戳,不是快照,也不是其父提交的原始哈希 ID。因此git filter-branch并没有修改提交。

它实际上是从一组提交中(在您的情况下,您希望这个集合包括所有的提交)提取每个提交,然后对提取的提交运行一些任意用户指定的命令。无论它做了什么,filter-branch都会用结果生成一个新的提交。

如果新提交与原提交完全,完全,100%按位相同,这实际上重新使用了原提交。否则,它将创建一个具有不同哈希值的新提交。

一旦创建了一个新的、不同的提交之后,每个随后的提交通常都会稍微有所不同:它将具有不同的父提交。filter-branch工具会为您处理这个重置父提交过程。因此,它的两个主要任务是:

  • 提取提交,运行筛选器,并重新提交
  • 根据需要更新父链接

剩下的难事当然是编写和运行筛选器。这个任务由你来完成。

--tree-filter可能是最容易使用的筛选器,因此它也是您想要的筛选器。值得一提的是,--index-filter速度更快,但如果您的任务是以某种方式修改每个提交中的快照,则很难处理。filter-branch有很多过滤选项,因为--tree-filter是最慢的过滤器且仅适用于更改快照。例如,--msg-filter可以编辑或替换每个提交中的消息文本。只要您想在每个快照中的所有文件上运行clang-format,就坚持使用--tree-filter

命令行部分的工作原理

让我们简要地看一下这如何实际操作,在这个例子中只有三个提交。这三个提交具有庞大丑陋的哈希 ID,但出于简单起见,我们将它们称为ABC。您从以下内容开始:

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引用上出问题,有时--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来运行tree-filter。因此,您必须确保您的命令由有效的shell命令组成,并且没有returnexit shell命令(至少不要在没有首先生成子shell的情况下使用)。如果您要运行的命令是您编写的脚本,请确保可以通过$PATH找到此脚本,或者提供脚本的完整路径名称:

--tree-filter "sh $HOME/scripts/filter-script.sh"

例如:

让我们观察一个简单的过滤器操作

假设提交 A 中有一个文件 README.md。假设提交 B 添加了一个新的 foo.cc 文件,需要进行重新格式化,并且提交 C 修改了 README.md 但完全未更改 foo.cc。您的过滤器仅更改任何 . cc.h文件,而不是README.md。因此,首先,filter-branch枚举所有提交,并将它们放在适当的顺序中:在本例中为A,然后B,然后C

树形过滤器现在执行以下操作:

  • 提取提交A
  • 在临时目录中运行您的过滤器/脚本/命令,其中只包含一个文件README.md
  • 从您的命令在临时目录中留下的任何内容创建新的提交。

由于您的命令没有涉及README.md,因此新提交与原始的A完全相同,100%,一模一样。因此 filter-branch 重用原始提交A

现在 filter-branch 转到提交B。它将B的两个文件提取到(现在为空的)临时目录中,并运行您的命令。这次,您的命令更改了foo.cc,但仍然保持README.md不变。因此,filter-branch 现在创建了一个包含修改后foo.cc的新提交。重新使用原始提交的作者姓名、电子邮件等信息保留了原始元数据,但是现在快照已更改,因此我们获得了一个新的和不同的哈希 ID,我们将其称为B'

A--B--C   <-- [original master]
 \
  B'   [in progress]

Filter-branch现在转到提交C。它将所有文件提取到(重新清空的)临时目录中,所以您拥有相同的两个文件。您的筛选器现在以与操作提交B的内容相同的方式修改foo.cc。 Filter-branch创建一个新的提交。新提交的快照具有修改后的foo.cc和与C中相同的README.md - 新的foo.ccB'中的匹配 - 并且它具有新的父级B'而不是B:这是filter-branch为您处理的最后一部分。因此,我们现在有:

A--B--C   <-- [original master]
 \
  B'-C'   [in progress]

在这一点上,我们已经没有可复制的提交了,所以filter-branch会执行它的最后几个技巧:
  • 如果有指向现有提交的标签,并且您指定了--tag-name-filter,Git会创建一个新的标签,该标签指向那些现有提交的副本。指向A的标签可以不动,但是如果标签指向B,则filter-branch会将其复制到一个新标签,该标签指向B';如果标签指向C,则filter-branch将其复制到一个新的标签,该标签指向C'。这些新标签的名称来自于--tag-name-filter:将旧名称输入过滤器,输出的是新的标签名称。
    如果没有标签,这一切都无关紧要。
  • 然后,对于命令行中命名的每个分支,在branch部分中,filter-branch将最后复制的提交的哈希ID存储到该分支中。因此,在这里,filter-branch将名称master设置为指向C'。
在出现任何问题的情况下,filter-branch会将所有原始的分支和标记名称复制到refs/original/:旧的master成为refs/original/refs/heads/master。如果一切顺利,您最终会想要放弃refs/original/名称。
上述的最终图示如下:
A--B--C   <-- refs/original/refs/heads/master
 \
  B'-C'   <-- master

就像Schwern的答案所说,你可能希望能够在一切都出现严重问题时进行恢复。一种方法是在存储库的副本(例如克隆)上运行filter-branch,而不是在原始存储库上运行。另一种方法是注意到您始终可以强制将所有更新过的引用退回到保存在refs/original/中的状态(但这通常需要一定的编程技巧)。


感谢您的详细解释。然而,它缺少一个关键点,即clang-format集成...但也许您可以在那里帮助我。通常使用clang-format -i file调用clang-format。我猜想根据您的示例,这将是find . -type f -print | xargs clang-format -i $1,对吗?此外,clang-format在当前目录中搜索.clang-format文件。我猜测在这个临时目录中找不到它...是否有任何保证临时目录会被创建在哪里? - jan.sende
你可能想要使用 find . -type f,或者是 find . \( -name '*.cc' -o -name '*.h' \)。如果你的文件名中有空格,你可能还需要使用 -print0xargs -0。至于临时目录,你可以使用 -d 将其指定到某个位置,或者利用一些未记录的特性(因此可能会更改):如果你不使用 -d,那么临时目录将位于 .git 目录下,这意味着 clang 可能会在你的工作树中找到 .clang-format 文件。 - torek
请查看 findxargs 的文档,因为没有 $1 参与。如果您使用的是 Windows 操作系统,则可能没有这些程序,请参阅 https://dev59.com/C14b5IYBdhLWcg3wnS0e。 - torek

1
在开始重写历史之前,我建议您标记当前提交。这将使您能够返回到原始版本,以防出现可怕的错误。或者复制整个存储库,以防万一。
我们使用git-filter-branch大规模重写历史。这有点像核子瑞士军刀。我们将使用--tree-filter来重写目录和文件。--all表示要执行所有引用的提交(即所有分支和标签),而不仅仅是从当前检出可达的提交。
git filter-branch --tree-filter your_rewrite_command --all

这个命令会检查每个提交,运行your_rewrite_command,并使用结果重写提交。
我建议编写一个小的shell脚本来进行重写并在运行git-filter-branch之前进行测试。使用git ls-files获取提交中所有文件的列表,并对每个文件运行clang-format

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接