首页 > 解决方案 > 如何格式化我的整个 git 历史记录?

问题描述

我现在已经完成了我的一个小图书馆。当我开始使用它时,我不知道 clang-format。现在我想用它格式化整个存储库。我知道随着提交哈希的变化,这会破坏其他人的存储库。但是,由于还没有人使用我的库,所以这对我来说很好。

因此,我必须怎么做才能为我的历史中的每个提交运行 clang-format?

标签: gitclang-format

解决方案


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 进行新的提交。

一旦你做了一个新的和不同的提交,每个后续的提交通常至少会略有不同:它会有一个不同的父级。filter-branch 工具会为您处理这个重新父代的过程。所以它所做的两项艰巨的工作是:

  • 提取提交、运行过滤器并重新提交
  • 酌情更新父链接

剩下的艰巨工作当然是编写和运行过滤器。那一个,过滤器分支留给你。

--tree-filter可能是最容易使用的过滤器,因此是您想要的过滤器。值得注意的是,--index-filter它的速度要快得多——但如果您的工作是以某种方式修改每次提交中的快照,那么使用起来会困难得多。Filter-branch 有很多过滤选项,因为 --tree-filter它是最慢的过滤器,而且它只适用于更改快照。例如,--msg-filter可以编辑或替换每个提交中的消息文本。但是,只要您想clang-format遍历每个快照中的所有文件,请坚持使用--tree-filter.

命令行部分如何工作,更详细

让我们从一个只有三个提交的示例开始简要了解它在实践中是如何工作的。这三个提交都有很大的丑陋哈希 ID,但为了简单起见,我们将它们称为ABC。你开始:

A <-B <-C   <-- master

分支名称master包含 commit 的哈希 ID C,因此我们(和 Git)可以看到哪个是最后一次提交。CommitC本身持​​有 commit 的哈希 ID B,而 commitB持有 commit 的哈希 ID A,因此 Git 可以从最后一次提交向后工作到第一次。CommitA没有父节点,因为它是第一个,所以这让 follow-everything-backwards 动作停止。

要运行git filter-branch,您可以使用:

git filter-branch --tree-filter '<command to run>' -- master

最后的东西——<code>master——是你想要filter-branch使用的分支名称,它列出了它应该操作的所有提交。也就是说,它将开始master并向后工作,直到它不能再向后退。然后它将复制每个提交,应用过滤器并重新提交。完成后,它将更新的一个分支名称是master.

Using--all告诉它从每个分支开始(以及标记和其他引用——这可能会在stashref 上出现异常,有时--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 都会使用内置的 shellexec来运行树过滤器。因此,您必须确保您的命令由有效的 shell 命令组成,并且其中没有returnor exitshell 命令(至少在没有首先生成子shell 的情况下是这样)。如果您要运行的命令您编写的脚本,请确保可以通过 找到此脚本$PATH,或提供脚本的完整路径名:

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

例如。

让我们看一个简单的过滤器在运行中

假设提交A中有一个文件,README.md. 让我们假设 commitB添加了一个将被重新格式化的新foo.cc文件,并且 commitC修改而根本README.md没有改变foo.cc。您的过滤器仅更改任何.cc.h文件,而不更改README.md. 因此,首先,filter-branch 本身会枚举所有提交,并将它们按适当的顺序排列:在这种情况下,是A, then B, then 。C

现在的树过滤器操作:

  • 提取提交A
  • 在保存一个文件的临时目录中运行您的过滤器/脚本/命令README.md
  • 从您的命令留在临时目录中的任何内容进行新的提交。

由于您的命令没有 touch README.md,因此新提交与原始A. 因此,Filter-branch 重用了原始的 commit A

现在 filter-branch 移​​动到 commit B。它将 的两个文件提取B到(现在为空的)临时目录中并运行您的命令。这一次你的命令改变foo.cc了,尽管它仍然保持README.md不变。所以现在 filter-branch 使用修改后的foo.cc. 重新使用原始提交的作者姓名和电子邮件等保留原始元数据,但现在快照已更改,所以现在我们得到一个新的不同的哈希 ID,我们将调用B'

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

Filter-branch 现在继续提交C。它将所有文件提取到(重新清空的)临时目录中,因此您拥有相同的两个文件。您的过滤器现在foo.cc以与对 commit 的内容进行操作时相同的方式进行修改B。Filter-branch 进行新的提交。新提交的快照有一个修改过的,与infoo.cc相同——new匹配 in而不是—— <em>并且它有一个新的父级,, 而不是:最后一部分是 filter-branch 为你处理的。所以现在我们有:README.mdCfoo.ccB'B'B

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

在这一点上,我们已经用完了要复制的提交,所以 filter-branch 做了最后几个技巧:

  • 如果存在指向现有提交的标签,并且您指定了 a --tag-name-filter,Git 会创建标签来指向这些现有提交的副本。任何指向的标签A都可以单独保留,但如果标签指向B, filter-branch 会将其复制到指向B';的新标签。如果标签指向C, filter-branch 将其复制到指向 的新标签C'。这些新标签的名称来自--tag-name-filter:旧名称进入过滤器,出来的是新标签名称。

    如果你没有标签,这一切都无关紧要。

  • 然后,对于您在命令行的分支部分命名的每个分支,filter-branch 将上次复制的提交的哈希 ID 存储到该分支中。所以在这里,filter-branch 将 name 设置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 回到它们保存的方式refs/original/(但这通常需要一些编程)。


推荐阅读