助手重写文件时尤其容易这样:几百行改动里要判的那几行,被空格和引号埋掉。
合并请求里混进大批只改排版的改动,直接看评审里不再讨论什么。
在一个没跑过排版命令的仓库里敲 make fmt,git status 干不干净?
答「不干净」,说明排版还没交给工具,评审仍要分心在空格和引号上。要补的是配置和一个入口命令,不是让每人自己注意。
把两件事分开:排版命令会改文件,检查命令只报告、不改文件。提交和持续集成只跑检查,退出码必须是 0。混用两者,就会「本地明明格式化过、线上还是红的」。每种语言只留一个排版工具,命令写进仓库;个人编辑器里的格式化设置不算团队约定。
| 语言 | 排版,会改文件 | 检查,不改文件 | 工具不改、要自己写对 |
|---|---|---|---|
| Python | ruff format . | ruff format --check . 与 ruff check . | 删掉没用的 import、注释内容 |
| Go | golangci-lint fmt | golangci-lint run | 名字、注释、错误处理 |
| TypeScript | npm run format | npm run format:check 与 npm run lint,类型另跑 npx tsc -b | 未使用的变量 |
| Java | mvn spotless:apply | mvn spotless:check | 常量全大写、方法名小写开头 |
| C# | dotnet format | dotnet format --verify-no-changes | 类型、方法、常量都用 PascalCase |
| Rust | cargo fmt | cargo fmt --check 与 cargo clippy -- -D warnings | 警告要改掉,不能留着 |
下面六个值写进配置,之后的每次改动不用再讨论一遍。
gofmt 写死。把编辑器设成 4 个空格,保存时也会被改回 Tab,不必争。.editorconfig 的 indent_size。.editorconfig 按扩展名分组给出。make check 判失败。trim_trailing_whitespace 在保存时清掉,不留给评审发现。工具查不到的边界:行宽不在这六个数字里,它不是硬判据——gofmt 根本不折长行,Python 的 E501 在本仓库被忽略,因为格式化器管不到注释。把行宽设成阻断项,只会逼出一批为凑长度而写的换行。
下面用 Python 走一遍,其余语言的命令回第一节的总表找。排版不替人修检查报出来的问题。
手写的是这样:单引号、== 两侧没空格,还有一个用不上的 import。
import json
def visible(notes, scope_id):
return [n for n in notes if n['scope_id']==scope_id]
ruff format . 之后跑完排版:引号变双引号,== 两侧补空格,函数前补到两个空行。json 还在——排版工具不管它有没有被用到。
import json
def visible(notes, scope_id):
return [n for n in notes if n["scope_id"] == scope_id]
ruff check . 拦住输出格式与 Makefile 里的 --output-format=concise 一致:文件、行、列、规则号、修法,照着它改。
$ ruff check --output-format=concise . src/notes.py:1:8: F401 [*] `json` imported but unused Found 1 error. [*] 1 fixable with the `--fix` option.
删掉那一行 import。要保留它,就让代码里真的用到——不许在同一行加 # noqa: F401 了事。scope_id 来自会话这一点从函数体里看不出来,按注释那页的写法补一句文档字符串。
def visible(notes, scope_id):
"""返回属于这个所属范围的笔记。
scope_id 来自会话,不从请求参数读取。
"""
return [n for n in notes if n["scope_id"] == scope_id]
工具查不到的边界:这套四步假定仓库里已经有 pyproject.toml 和入口命令。还没有这两个文件时,先按下一节补上,不要先手工调格式——手工调出来的结果和 ruff format 不一致,下一次保存又会变回去。
排版交给工具之后,评审在这一项上少花时间,但它换来一个新风险:大段重排会盖住真正的功能改动。
git diff -w main...HEAD 忽略空白后还剩多少改动;改动差异里有没有新出现的 # noqa、eslint-disable、//nolint;有没有人改了 .editorconfig、pyproject.toml 的 [tool.ruff] 段或排版工具本身。
make check,红了就重跑 make fmt,不写进评审意见。
| 现象 | 说明什么 | 怎么处理 |
|---|---|---|
git diff --numstat 里某文件改动行数 > 该文件总行数的 60%,且逻辑没变 | 排版和功能混在同一次提交里 | 要求拆成两次提交:先只跑 make fmt 提交一次,再提交功能 |
改动差异里新出现 # noqa、eslint-disable、//nolint | 检查是被绕开而不是被修好 | 按质量门禁的「不许改判据」处理,挡回 |
| 新增文件缩进和同目录其他文件不一致 | .editorconfig 没覆盖这个扩展名,或生成时没读配置 | 补 .editorconfig 的分组,重跑 make fmt |
同一份 pyproject.toml 在两次改动里 line-length 不同 | 有人为了让自己那份文件通过而改了阈值 | 阈值改动走单独一次合并请求,不夹带功能改动 |
工具查不到的边界:「改动超过 60% 就要求拆分」这条对新建文件不适用——新文件的改动比例必然是 100%。这条判据只对已有文件生效。
$ git diff --stat origin/main app/api/notes.py | 247 ++++++++++++++++++++++++++-------------------- 1 file changed, 247 insertions(+), 121 deletions(-) $ git diff -w --stat origin/main app/api/notes.py | 9 +++++---- 1 file changed, 9 insertions(+), 5 deletions(-)
认它的办法:加 -w 再看一遍,剩下的才是要审的。助手重写一个文件时,会把引号、空行、import 顺序一并重排:格式全部正确、检查全部通过,真正要改的三行却被埋在 240 行里。这不是格式问题,是评审注意力被挪走了——花在空格上的每一分钟,都是从那三行上挪走的。
git ls-files --eol,输出里不出现 crlf。违反后果:Windows 组员保存一次会把整个文件标成改动,要找的那几行埋在里面。EF BB BF。违反后果:脚本与配置文件里的中文变乱码,而且只在部分工具里出错。ruff format . 或 prettier --write——有就是错的,只能是 --check。违反后果:服务器改了文件却没提交,本地和线上不一致。make check 会报错,但它报的是「格式」不是「抄错了」,容易朝错的方向改。Makefile判据:全新克隆一份仓库,make fmt 与 make check 都能一次跑通。违反后果:仓库里留下一条永远失败的命令,其他人会开始习惯「这条红的是正常的」。工具查不到的边界:第三条只管自动检查。在命令或提交钩子里放一条 make fmt 给作者手动跑是允许的——它由人触发,改完人能看见。
四个文件加一份编辑器设置:换行与缩进集中在 .editorconfig,语言规则集中在 pyproject.toml,动作集中在 Makefile,触发在持续集成。其余语言的配置文件放哪,见本节最后一张表。
花括号、引号这类交给各语言自己的排版工具,这里只管跨语言共有的那几项。C# 的左花括号另起一行也写在这里,由 dotnet format 执行。
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.{js,jsx,ts,tsx,json,css,yml,yaml,md}]
indent_style = space
indent_size = 2
[*.py]
indent_style = space
indent_size = 4
[*.java]
indent_style = space
indent_size = 2
[*.cs]
indent_style = space
indent_size = 4
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
dotnet_sort_system_directives_first = true
[*.rs]
indent_style = space
indent_size = 4
这份 pyproject.toml 是全仓库唯一的一份,与质量门禁指向的是同一份。不要在两处各维护一半,否则改了一处另一处会静默失效。命名(N)与复杂度语句数(PLR)从各自页面用 extend-select 追加。
[tool.ruff] line-length = 100 indent-width = 4 target-version = "py312" src = ["scripts", "src"] [tool.ruff.lint] select = ["E", "F", "I", "B", "S", "C901"] [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.format] quote-style = "double" indent-style = "space"
扩展 id 以各自扩展页为准。保存时格式化只在装了对应扩展时生效,它不跑 ruff check——F401 只有 make check 拦得住。
{
"editor.formatOnSave": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"[go]": { "editor.defaultFormatter": "golang.go" },
"[python]": { "editor.defaultFormatter": "charliermarsh.ruff" },
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }
}
某种语言的目录还不存在时,删掉对应行,别留下一条永远失败的命令。make check 的聚合入口在质量门禁里定义,全仓库只有那一处;这一页只加 fmt-check 一个子目标,再把它挂进 check 的依赖里。
.PHONY: fmt fmt-check fmt: ruff format . golangci-lint fmt npm --prefix frontend run format # 挂进 check:check: fmt-check lint type secret complexity test fmt-check: ruff format --check . npm --prefix frontend run format:check
工作流只把 make check 搬到服务器上,不在 YAML 里另抄一份规则。
name: check
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements-dev.txt
- run: make check
| 语言 | 配置文件放哪 | 关键项 |
|---|---|---|
| Go | 根目录 .golangci.yml | 在 formatters 里开 gofmt 与 goimports;goimports.local-prefixes 填 go.mod 第一行的 module |
| TypeScript | frontend/.prettierrc.json 与 frontend/eslint.config.mjs | Prettier 管空格与引号,在 eslint 最后一项引入 eslint-config-prettier 关掉重复规则 |
| Java | java/pom.xml 里的 spotless 插件 | 把风格选成 GOOGLE;Maven 用 JRE 17 或更高版本运行 |
| C# | 复用根目录 .editorconfig 的 [*.cs] | 规则来自 .editorconfig,不在 App.csproj 里另写一份 |
| Rust | rust/rustfmt.toml | max_width = 100、newline_style = "Unix",与 .editorconfig 的 LF 一致 |
工具查不到的边界:仓库只用一种语言时,把 .editorconfig 里其他语言的分组和 Makefile 里对应的行都删掉。留着不会报错,但下一个读配置的人会以为仓库里还有那些语言。