如何格式化我的整个 git 历史记录?

How can I clang-format my WHOLE git history?

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

因此,对于历史记录中的每个提交,我必须如何 运行 clang-format?

在你开始重写历史之前,我建议你标记你当前的提交。如果出现严重错误,这将允许您 return 到您的原始版本。或者复制你的整个仓库,以防万一。

我们用 git-filter-branch 批量重写历史。这有点像核瑞士军用电锯。我们将使用 --tree-filter 重写目录 ("tree") 和文件。 --all 表示要执行所有引用的提交(即所有分支和标签),而不仅仅是可从当前结帐中访问的提交。

git filter-branch --tree-filter your_rewrite_command --all

这会检查每个提交,运行s your_rewrite_command,并用结果重写提交。

我建议编写一些 shell 脚本来进行重写并在 运行 宁 git-filter-branch 之前对其进行测试。使用 git ls-files 获取提交中所有文件的列表,并在每个文件上使用 运行 clang-format

Git 带有一个 git filter-branch 命令,它是一个有助于完成此类任务的工具。请注意,git filter-branch 本身 并不能完成这项工作: 它只是一个您可以使用的工具,以便 可以完成这项工作。您仍然必须编写自己的命令。您最终可能会使用的那个是:

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

filter-branch 的作用

这里有一个基本问题:一旦提交,就不能以任何方式更改。 关于提交的任何内容 都不能更改:提交人的名字不能更改,date-and-time 标记不能更改,快照不能更改,其原始哈希 ID 不能更改 parent 提交。所以 git filter-branch 不会那样做。

它所做的是提取每个提交(从一组提交中——在你的情况下,你希望这组是 all 提交),一次一个,然后运行 在 提取的 提交上的一些任意的 user-specified 命令。不管这样做,filter-branch 然后根据结果进行 new 提交。

如果新提交与原始提交完全、完全 100% bit-for-bit 相同,这实际上是 re-uses 原始提交。否则,它会使用新的不同哈希 ID 进行新提交。

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

  • 提取提交,运行 过滤器,并重新提交
  • 适当更新parent链接

剩下的艰巨工作当然是编写和运行安装过滤器。那一个,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)就可以看到哪个是last[=205] =] 提交。 Commit C本身持有commit B的hash ID,而commit B持有commit A的hash ID,这样Git可以从最后提交给第一个。提交 A 没有 parent 因为 它是第一个,所以这让 follow-everything-backwards 动作停止。

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

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

最后的东西——master——是你希望 filter-branch 在列出它应该操作的所有提交时使用的分支名称。也就是说,它将从 master 开始并向后工作,直到无法再向后。然后它将复制这些提交中的每一个,应用过滤器和 re-commit。完成后,它将更新的一个分支名称是 master.

使用 --all 告诉它从每个分支开始(以及标记和其他引用——这在 stash 引用上可能会出现错误,有时 --branches --tags 可能更好,但是 --all 是传统的,至少)。我们稍后也会回到 --tag-name-filter 选项。现在让我们使用 master.

master 之前的 -- 是为了将放置分支名称的部分与其余选项分开,其中一些选项可能类似于有效的分支名称。仅此而已:只是用于标记“过滤器选项结束,分支名称开始”的样板。

最后,让我们看一下--tree-filter,而不是看如何写一个tree-filter。这只是意味着:运行 树过滤器。所以 filter-branch 会将每个提交提取到一个临时目录中,该目录只包含已提交的文件。此临时目录没有 .git 子目录,并且 不是您的 work-tree。 (它实际上是您传递的 -d 目录的子目录,或者默认情况下,filter-branch 创建的临时目录的 sub-directory。)您的树过滤器应该:

  • 应用您想要的任何更改
  • 到其当前工作目录中的每个文件
  • 并递归地,到当前目录sub-directory中的每个文件

例如,如果您想在每个文件中插入 header 行,您可以使用:

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,在这种情况下。

现在tree-filter操作:

  • 提取提交 A
  • 运行你的 filter/script/command 在临时目录中保存一个文件 README.md;
  • 从您的命令留在临时目录中的任何内容进行新提交。

由于您的命令没有触及 README.md,新提交完全 100% 与原始 A 完全相同。 Filter-branch 因此 re-uses 原始提交 A.

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

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

Filter-branch 现在继续提交 C。它将所有文件提取到 (re-emptied) 临时目录中,因此您拥有相同的两个文件。您的过滤器现在修改 foo.cc 的方式与操作提交 B 的内容时的方式相同。 Filter-branch 进行新的提交。新提交的快照具有修改后的 foo.cc 和与 C 中相同的 README.md——新的 foo.ccB' 中的相匹配——并且 它有一个新的 parent、B',而不是 B:这最后一部分是 filter-branch 为您处理的。所以现在我们有:

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

在这一点上,我们已经 运行 没有要复制的提交,所以 filter-branch 使用最后几个技巧:

  • 如果有指向现有提交的标签,你指定了一个--tag-name-filter,Git使得new 标签指向这些现有提交的副本。任何指向 A 的标签都可以保留,但如果指向 B 的标签,filter-branch 会将其复制到指向 B' 的新标签;如果标签指向 C,filter-branch 会将其复制到指向 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

正如 Schwern 的回答,如果一切都非常糟糕,您可能希望能够恢复。一种方法是 运行 filter-branch 在存储库的 copy(例如,克隆)上,而不是在原始存储库上。另一种方法是注意您始终可以强制所有更新的引用返回到它们在 refs/original/ 中保存的方式(但这通常需要一些编程)。