如何格式化我的整个 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,但为简单起见,我们将它们称为 A
、B
和 C
。您开始于:
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 命令组成,并且其中没有 return
或 exit
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.cc
与 B'
中的相匹配——并且 它有一个新的 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/
中保存的方式(但这通常需要一些编程)。
我现在已经完成了我的一个小图书馆。当我开始使用它时,我不知道 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,但为简单起见,我们将它们称为 A
、B
和 C
。您开始于:
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 命令组成,并且其中没有 return
或 exit
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.cc
与 B'
中的相匹配——并且 它有一个新的 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/
中保存的方式(但这通常需要一些编程)。