我怎样才能格式化我的整个 git 历史记录



我现在已经完成了我的一个小库。当我开始使用它时,我不知道 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,但为了简单起见,我们将它们称为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告诉它从每个分支开始(以及标签和其他引用 - 这可能会在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 将要做的是使用 shell 内置的exec来运行树过滤器。 因此,您必须确保您的命令由有效的 shell 命令组成,并且其中没有returnexitshell 命令(至少在没有首先生成子 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.ccB'中的快照匹配 — 并且它有一个新的父级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

相关内容

  • 没有找到相关文章

最新更新