【问题标题】:How do I run a code formatter over my source without modifying git history?如何在不修改 git 历史记录的情况下在我的源代码上运行代码格式化程序?
【发布时间】:2019-04-29 08:52:23
【问题描述】:

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

有一个 git filter-branch 命令,它允许您从一开始就针对 repo 的每个修订版运行一个命令。

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 历史的情况下编辑 git 历史?”,对吧?
  • 不,你误会了。 git filter-branch 命令允许我在不更改修订作者的情况下编辑行,因此git blame 仍然有效。我只是想为 HEAD 而不是过去的修订版这样做。
  • 那么不要使用git filter-branch——只需运行格式化程序,添加并提交即可。或者,如果您想修改分支中的最后一次提交 — git addgit commit --amend
  • 问题是这会改变每一行的作者都是我。当我说我希望 git blame 仍然有效时,我的意思是它应该列出该行先前修订的作者。
  • @aherriot - 您将filter-branch 的行为描述为“在不更改作者的情况下编辑行”;这在概念上是不正确的。 “行”没有作者。提交有作者,git blame 找出最近更改每一行的提交并报告有关该提交的信息(包括作者)。

标签: git formatting git-filter-branch prettier


【解决方案1】:

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

我想源代码控制工具可以支持“不重要的更改”的想法,您将提交标记为装饰性的,然后历史分析将跳过该提交。我不确定该工具将如何验证更改确实是装饰性的,并且如果没有某种形式的工具强制执行,该功能肯定会被滥用,导致错误介绍可能隐藏在“不重要的”提交中。但实际上,我认为这是一个坏主意的原因是学术性的——底线是,git 没有这样的功能。 (我也想不出有什么源代码控制工具可以做到。)

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

顺便说一句,历史重写实际上有几个缺点。您提到了处理时间,所以让我们先来看看:

正如您所指出的,使用filter-branch 执行此操作的直接方法将非常耗时。您可以采取一些措施来加快它的速度(例如为它的工作树提供一个 ramdisk),但它是 tree-filter,它涉及每个文件的每个版本的处理。

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

(可以基于同样的原理编写更专业的工具,但 AFAIK 没有人写过。有先例,更专业的工具可以比 filter-branch... 更快)

即使您找到一个运行速度足够快的解决方案,请记住,历史记录重写会干扰您的所有 refs。像任何历史重写一样,repo 的所有用户都必须更新他们的克隆 - 对于这种彻底的事情,我建议这样做的方式是,在开始重写之前扔掉克隆,然后重新克隆。

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

因此,历史重写是一个非常激进的解决方案。另一方面,假设格式化代码是不可能的,因为它不是从第一天开始就完成的,这似乎也很激烈。所以我的建议:

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

是的,很糟糕。一阵子。但随着时间的推移,一段特定的历史往往会变得不那么重要,所以从那里你只是让问题逐渐减少到过去。

【讨论】:

  • “我不确定该工具将如何验证更改是否真的是装饰性的”——对于代码,也许如果该工具能够比较修订的抽象语法树而不是文本,它可以看出它们在逻辑上是等价的,因此是装饰性的。
  • @EricSmith 有一些工具试图提供所谓的“结构差异”。这种方法有很多成本和限制。如果您想要使用特定于语言的 diff 工具,并且在对整个树的文件进行 diff 时使用它们不太耗费资源,则可以使用 .gitattributes 来配置它们。
【解决方案2】:

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

创建一个文件.git-blame-ignore-revs 喜欢:

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

那就做吧

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

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

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

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      git filter-branch --tree-filter "find -regex '.*.(cpp\|h\|c\|)' -exec {} \;" -- --全部

      < 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 {} \;" -- --全部

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

      【讨论】:

        【解决方案5】:

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

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

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

        类似的选项(--ignore-rev &lt;rev&gt;--ignore-revs-file &lt;file&gt; 自 2.23 起在 git 中可用:https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt

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

        【讨论】:

        【解决方案6】:

        必须有一种方法来格式化代码库,同时保留每一行的作者信息。

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

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

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

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

        3. 不完整的解决方案。您必须弄清楚要在历史记录中的哪个位置插入重新格式化操作。越往前走,就越能保留更改的作者身份,但在后续更改中合并的工作量就越大。因此,您可能仍然会得到大量代码,其中重新格式化提交是最新更改。

        4. 收益有限。您永远不会真正丢失git 中的作者信息——只是这些工具通常只显示谁进行了最近的更改。但是您仍然可以返回并查看之前的提交并挖掘任何一段代码的整个历史,包括谁制作了它。因此,将您的重新格式化操作插入到历史记录中真正为您带来的唯一好处是,您可以方便地查看谁更改了某些代码,而无需返回到较早的提交。

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

        6. 收益会随着时间的推移而减少。随着开发的继续,您试图努力不掩盖的变化将被一些其他变化所掩盖无论如何,您的重新格式化更改同样会被这些新更改所取代。随着时间和发展的推进,您为隐藏重新格式化更改所做的工作将没有多大意义。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-09-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多