如何在不修改 git 历史记录的情况下 运行 对我的源代码进行格式化?

How do I run a code formatter over my source without modifying git history?

我正在尝试使用代码格式化工具格式化整个存储库。这样做时,我想保留有关谁提交了哪一行的信息,以便像 git blame 这样的命令仍然显示正确的信息。通过这个,我的意思是它应该向作者显示之前编辑过每一行(在格式化之前)。

有git filter-branch 命令,它允许您运行 从时间开始对回购的每个修订执行命令。

git filter-branch --tree-filter '\
  npx prettier --write "src/main/web/app/**/**.{js, jsx}" || \
  echo "Error: no JS files found or invalid syntax"' \
  -- --all

运行 这会花很长时间,我真的不在乎过去。我只想在不更改每一行的所有权的情况下格式化主分支。我怎样才能做到这一点?我尝试在最后使用 rev-list 和其他过滤器类型,但它仍然不起作用。必须有一种方法来格式化代码库,同时保留每一行的作者信息。

你想做的事是不可能的。您不能在某个时间点更改一行代码,但 git 报告该行代码的最新更改发生在该时间点之前。

我想源代码控制工具 可以 支持 "unimportant change" 的想法,您可以在其中将提交标记为装饰性的,然后历史分析将跳过该提交。我不确定该工具如何验证更改是否真的是表面上的,并且如果没有某种形式的工具强制执行,该功能肯定会被滥用,导致错误引入可能隐藏在 "unimportant" 提交中。但我认为这是一个坏主意的真正原因是学术上的——底线是,git 没有这样的功能。 (我也想不出任何源代码控制工具。)

您以后可以更改格式。您可以保留过去更改的可见性。您可以避免编辑历史记录。但是你不能同时做这三件事,所以你将不得不决定牺牲哪一个。

顺便说一句,历史重写实际上有几个缺点。你提到处理时间,我们先看一下:

正如您所指出的,使用 filter-branch 执行此操作的直接方法将非常耗时。你可以做一些事情来加快它的速度(比如给它一个 ramdisk 作为它的工作树),但它是一个 tree-filter 并且它涉及处理每个文件的每个版本。

如果你做了一些预处理,你可能会更有效率。例如,您可以预处理数据库中的每个 BLOB 并创建映射(其中 TREE 包含 BLOB X,将其替换为 BLOB Y),并且然后使用 index-filter 来执行替换。这将避免所有检出和添加操作,并且将避免重复重新格式化相同的代码文件。这样就节省了很多I/O。但设置起来并非易事,而且可能仍然很耗时。

(可以根据相同的原理编写更专业的工具,但 AFAIK 还没有人编写过。有先例表明更专业的工具可以比 filter-branch 更快...)

即使您的解决方案运行 足够快,请记住重写历史记录会干扰您的所有参考。与任何历史重写一样,回购的所有用户都需要更新他们的克隆 - 对于这种彻底的事情,我建议这样做的方式是,在开始重写之前丢弃克隆,然后重新克隆。

这也意味着如果你有任何依赖于提交 ID 的东西,那也将被破坏。 (这可能包括构建基础架构或发布文档等;具体取决于您的项目实践。)

因此,重写历史记录是一个非常激进的解决方案。另一方面,仅仅因为从第一天起就没有完成就认为格式化代码是不可能的似乎也很过分。所以我的建议是:

在新提交中重新格式化。如果您需要使用 git blame,它会将您指向发生重新格式化的提交,然后在重新格式化提交的父项上再次 运行ning git blame 跟进。

是的,这很糟糕。一阵子。但是一段特定的历史往往会随着时间的推移变得不那么重要,所以从那里开始,你只需让问题逐渐消失。

There must be a way to format the codebase while preserving the author information for each line.

您可以做的一件事是从某个较早的提交分支,重新格式化代码,然后将 master 变基到您的分支。这将保留 之后的所有更改的作者身份,无论您从哪里开始提交。

这就是想法,但有一些重要的原因您不应该这样做:

  1. Rebase 共享分支不是一个好主意。您甚至关心保留更改的作者身份这一事实可能意味着有很多人积极致力于代码。如果你去 rebase master 分支,那么你的 repo 的每个分支或克隆都会有一个具有旧历史的 master 分支,除非你非常小心地管理流程并确定每个人都知道你在做什么,并适当地更新他们的副本。更好的方法可能是不 rebase master,而是将 master 的提交合并到你的分支中。然后,让每个人都开始使用新分支而不是 master.

  2. 合并冲突。 在重新格式化整个代码库时,您可能会更改几乎每个文件中的大量行。当您合并后续提交时,无论是通过 rebase 还是 merge,您都可能需要解决大量冲突。如果你采用我上面建议的方法并将 master 的提交合并到你的新分支而不是变基,那么以有序的方式解决这些冲突会更容易,因为你可以一次合并几个提交直到你被抓住向上.

  3. 不完整的解决方案。您将不得不弄清楚要在历史记录中的什么位置插入您的重新格式化操作。往后退得越远,您就越能保留更改的作者身份,但合并后续更改的工作量也会越大。所以你可能仍然会得到很多代码,其中你的重新格式化提交是最新的更改。

  4. 有限的好处。你实际上从未丢失git中的作者身份信息——就是这样工具通常只显示谁进行了最近的更改。但是您仍然可以回过头来查看之前的提交,并深入挖掘任何代码片段的整个历史,包括是谁创建的。因此,将您的重新格式化操作插入到历史记录中的唯一真正让您受益的是,您可以方便地查看谁更改了一些代码,而无需返回到较早的提交的额外步骤。

  5. 这是不诚实的。当你重写一个分支的历史时,你正在改变一个关于代码如何随时间变化的事实记录,这可以制造真正的问题。假设您的重新格式化并不像您想的那样 相当 无关紧要,并且在进行重新格式化时您实际上制造了一个错误。例如,假设您将一些额外的白色 space 引入到多行字符串常量中。几周后,终于有人注意到了问题并开始寻找原因,看起来更改是在一年半之前进行的(因为那是您将重新格式化插入历史记录的地方)。但这个问题似乎是新问题——它没有出现在两个月前发布的版本中,所以到底发生了什么?

  6. 收益会随着时间的推移而减少。 随着开发的继续,您努力不去掩盖的变化将被一些 other 无论如何都会更改,并且您重新格式化的更改同样会被这些新更改所取代。随着时间和发展的推进,您为掩埋重新格式化所做的工作意义不大。

如果您不希望您的名字显示为项目中每一行的作者,但您也不希望忍受上述问题,那么您可能需要重新考虑您的方法。 一个更好的解决方案 可能是作为一个团队来解决重新格式化问题:让团队中的每个人都同意 运行 他们更改的任何文件的格式化程序,并使正确的格式成为一个今后所有代码审查中的要求。随着时间的推移,您的团队将覆盖大部分代码,并且作者信息将最合适,因为每个重新格式化的文件无论如何都会被更改。您最终可能会得到一小部分永远不会重新格式化的文件,因为它们非常稳定并且不需要更新,您可以选择重新格式化它们(因为有一些格式错误的文件会让您发疯)或不重新格式化(因为无论如何,没有人真正在处理这些文件。

git filter-branch --tree-filter "find < dir > -regex '.*.(cpp\|h\|c\|< etc >)' -exec < formatter-command > {} \;" -- --all

< dir > : 关注的目录,因为上面需要从根目录运行,但你可能只想格式化根目录下的某些sub-dir git 目录

< etc > : 其他文件格式。

< formatter-command > :您可以 运行 用于单个文件的命令,它将格式化该文件。

--all 最后意味着对所有 git 分支执行此操作(总共 4 个破折号)

例如这就是我所拥有的,其中我的 git 包含 src 目录(除了测试、工具等)

git filter-branch --tree-filter "find src -regex '.*.(cpp\|h\|cu\|inl)' -exec clang-format -style=google -i {} \;" -- --all

以上将重写每个 git 提交,但不会更改 git 注释。由于这会修改 git 历史记录,因此一旦推送,每个人都必须重新克隆。

Mercurial 对此有一个(实验性的)选项,“--skip”:

--skip <REV[+]>
    revision to not display (EXPERIMENTAL)

我认为默认情况下还没有等效的 git,但是有一个 hyper-blame command 外部开发的。

自 2.23 起,--ignore-rev <rev>--ignore-revs-file <file> 在 git 中可用:https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt

根据我的经验,两者都不能很好地处理格式更改,尤其是当多行折叠成一行时。

git blame -w -M 应该忽略空格和移动的代码更改,因此您只需要重新格式化代码并记住在寻找责任时使用这些选项!

https://coderwall.com/p/x8xbnq/git-don-t-blame-people-for-changing-whitespaces-or-moving-code

您可以 git blame 忽略某些提交,这些提交只会进行大量重新格式化等操作:

创建文件 .git-blame-ignore-revs 如:

 # Format commit 1 SHA:
 1234af5.....
 # Format commit 2 SHA:
 2e4ac56.....

然后做

git config blame.ignoreRevsFile .git-blame-ignore-revs

,这样您就不必每次都使用 --ignore-revs-file 选项 git blame

投票 https://github.com/github/feedback/discussions/5033 以将该功能添加到 github 的网络责备查看器中。