如何格式化多分支项目中的代码?

How can I format the code in a multi-branch project?

所以我们有数十万行代码 git 存储库,自从我 2 年前加入这个项目以来,格式让我很烦恼。它不仅让我感到烦恼,而且当开发人员随机 "fix" 格式化时,当代码格式化仅应用于一侧时,合并会导致头痛。现在重新格式化代码是一个两分钟的任务,但也会导致合并冲突。我最近将 master 合并到一个长期存在的功能分支并尝试:

现在我想知道它是否值得合并,因为还有另外 15 个分支都需要完全相同的代码审查,而且手动合并很容易出错我想知道是否有某种方法可以在不进行这些合并的情况下执行此操作冲突。

编辑,2022 年 6 月

我只是在增强来自 的信号:

https://github.com/emilio/clang-format-merge 包含提供 合并驱动程序 的代码,而不是清洁和涂抹过滤器。不过,它看起来可能很有用,尤其是对于从未强制执行标准格式的存储库。

有假设的食谱

(注意:我还没有测试过这些)

我们假设重新格式化程序在 ~/Downloads/android-studio/bin/format.sh 中并且 [注意:显然这是一个错误的假设!] 它读取 stdin 并写入 stdout,并且一次处理一个文件。 (有可能,但非常困难,使它在一次需要多个文件的情况下工作。不过,你不能在这种情况下使用这个方法。Git 的基本过滤机制要求每个过滤器简单地读取 stdin 并写入 stdout。默认情况下 Git 假设过滤器工作,即使它以失败状态退出。)

选择 运行 过滤器的位置;在这里,我将其设置为仅“干净”过滤器。

~/.gitconfig.git/config 中,添加过滤器的定义:

[filter "my-xyz-language-formatter"]
    clean = ~/Downloads/android-studio/bin/format.sh
    smudge = cat

(这假设 运行ning cat 运行s 是一个过滤器,将其未更改的输入写入其标准输出;这在任何类 Unix 系统上都是正确的)。

然后,如果需要,创建一个 .gitattributes 文件。它将应用于您创建它的目录和所有子目录,除非在这些子目录中被覆盖,因此将它放在最高的合理位置,通常是存储库的根目录,但有时在 source/src/ 或任何目录。通过格式化程序将行添加到与某些模式匹配的定向文件。我们在这里假设所有名为 *.xyz 的文件都应格式化为:

*.xyz   filter=my-xyz-language-formatter

此过滤器现在将应用于 *.xyz 个文件的所有提取和插入。 The gitattributes documentation 谈到在退房和入住时应用这些,但这并不完全正确。相反,只要Git从工作树复制到索引(本质上,git add——远在git commit之前,除非你使用git commit -a 或类似的标志)。 smudge 过滤器在 Git 从索引复制到工作树时应用(本质上,git checkout,但也有一些其他情况,例如 git reset --hard ).

请注意,为每个文件启动一个过滤器可能会非常慢。如果您对过滤器有很多控制权,可以使用“long 运行ning filter process”协议,这可以加快速度(尤其是在 Windows 上)。不过,这超出了这个答案的范围。

运行 git merge 通常不使用过滤器(它适用于已经在索引中的副本,这在过滤步骤之外)。但是,将 -X renormalize 添加到标准合并将使 git merge 执行下面描述的“虚拟签入和签出”,以便应用过滤器。这发生在合并中涉及的所有三个 提交(并且在两个方向上——干净和污迹——所以它比只有一个提交慢大约 6 倍)。

说明(见下文)

Git 本身在这里只是部分有用。

从根本上说,问题在于 Git 是愚蠢的并且是面向行的:它 运行s git diff 从合并基础提交到每个提示提交。如果这些 git diff 中的一个或两个看到大量格式更改,它会认为这些更改很重要并且值得应用于基础。它没有输入代码的语义知识。

(既然你可以接管整个合并过程,你可以编写一个更智能的合并,确实使用语义分析。不过,这非常困难。我唯一知道的系统Ira Baxter 的商业软件是 Ira Baxter 的商业软件,我从未真正使用过它;我只是了解它背后的理论。)

一个不依赖于让Git更聪明的解决方案。如果你有一个输出一致格式代码的语义分析器,不管 input 形式如何,你都可以提供所有三个版本——B for base, L 代表左侧或本地或 --oursR 代表右侧或远程或其他或 --theirs—进入此格式化程序:

reformat < B > B.formatted
reformat < L > L.formatted
reformat < R > R.formatted

现在您可以 Git 合并所有三个格式化版本,而不是合并原始可能尚未格式化(但可能已格式化)的版本。

合并的结果当然会重新格式化。但大概这就是您想要的。

使用 Git 的内置工具实现此目的的方法是使用它所谓的 smudgeclean 过滤器。当文件从存储库提取到工作树时,会对文件应用污迹过滤器。每当文件从工作树进入存储库时,都会对文件应用干净的过滤器。

在这种情况下,涂抹过滤器可以“对数据不做任何操作”,准确保留提交的内容。干净的过滤器可以重新格式化。或者,如果您愿意,污迹过滤器可以是重新格式化器,而清洁过滤器可以再次成为重新格式化器,或者是无操作过滤器。一旦你有了它——这是你在 .gitattributes 中设置的东西,通过路径名为特定文件定义过滤器,以及 .git/config 或你的主要(用户或系统范围)中的过滤器驱动程序) .gitconfig.

完成所有设置后,您可以 运行 git merge -X renormalize。 Git 将像往常一样提取 BLR 版本,但随后运行 他们通过“虚拟签出和签入”步骤进行了三个临时提交,1 B.formatted 等等。然后它使用三个临时提交进行合并,而不是从最初的三个提交开始。

困难的部分是找到一个能够满足您的需求的重新格式化程序。一些现代系统有它们,例如 gofmtclang-format。如果有一个可以满足您的需求,那么只需将所有这些整合在一起,并获得团队其他成员的支持,这种重新格式化是个好主意。


1从技术上讲,它只是制作树对象;不需要实际提交。

虽然 torek 可能让我步入正轨,但它并没有帮助我完成 b运行ches 的重新格式化。问题是在 git 添加了这些

之后应用的过滤器
<<<< HEAD
bla foo 123
====
bla 123
>>>> otherBranch

块,所以过滤器会缩进冲突标记...这不好。

虽然这可能有一些解决方案,但我使用了自定义合并工具:

#!/bin/bash

BASE=
LOCAL=
REMOTE=
MERGED=

if echo "$BASE" | grep -q "\.java"; then
    echo "Normalizing java file";
    astyle $BASE
    astyle $LOCAL
    astyle $REMOTE
    astyle $MERGED
fi


meld "$LOCAL" "$BASE" "$REMOTE" --output "$MERGED"

.gitconfig 中配置为:

[merge]
    tool = customMergeTool
[mergetool "customMergeTool"]
    cmd = /path/to/customMergeTool.sh \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"

使用我的方法,git 仍然会检测到在我的 100 个案例中有 40 个没有合并冲突的脚本处理时的冲突,因此 torek 的方法可能会加快速度,但我 运行 在合并其他 40 个文件时遇到严重问题,所以我暂时放弃了。