登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
17 KiB
pre-commit 自动化代码审查 — 部署与使用说明
生效前提:本机已运行过
pre-commit install(钩子写入.git/hooks/)。 新机器/新开发者进入仓库后需执行一次pre-commit install,详见「新机器启用」。
1. 部署架构
git commit
└─ pre-commit(.git/hooks/pre-commit)
├─ trailing-whitespace 自动修复行尾空格
├─ end-of-file-fixer 自动修复文件尾换行
├─ check-added-large-files 阻止 >1MB 大文件
├─ check-merge-conflict 阻止含冲突标记的文件
├─ check-yaml 校验 YAML 语法
├─ cppcheck C 静态分析(自有代码)
├─ no-mojibake 阻止 U+FFFD 乱码文件
├─ coding-standard C 代码规范检查(规范V1.0 可机械校验项)
├─ doc-links 文档链接存在性(.md)
├─ task-table FreeRTOS 任务表一致性
├─ mem-numbers 内存/Flash 数字一致性(体积档)
├─ lv-conf-sync 两份 lv_conf.h 特性子集一致性(比对生效值)
├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5)
├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,登记簿「rs485_task 白名单缺口」;含 UI 页面描述符表接线,「UI 字符集与像素回归门禁」)
├─ mem-budget RAM 预算 92% 阈值门禁(登记簿条目「缩堆迁 CCM 后半(暂缓)」,2026-10-12)
└─ font-charset UI 字符集差集检查(防缺字白框,「UI 字符集与像素回归门禁」,2026-10-15)
└─ commit-msg(.git/hooks/commit-msg)
└─ gitlint 校验 commit message 格式
两阶段钩子:pre-commit 阶段拦截代码问题,commit-msg 阶段拦截格式问题。任一失败 → commit 中止。
CI 侧的同一套门禁(2026-10-01 起):Gitea Actions(
.gitea/workflows/ci.yml)在 push / PR 时 以pre-commit run --all-files复跑全部上述钩子(mem-numbers在无 Keil.map的 runner 上自适应跳过)。 本地提交前想跑同一套判定:& '.venv\Scripts\python.exe' scripts/ci_local.py。详见../测试说明/测试开发标准流程.md§9。
依赖清单
| 依赖 组件 | 安装方式 | 用途 |
|---|---|---|
| pre-commit 4.6.2 | uv tool install pre-commit |
钩子框架 |
| cppcheck 2.21.0 | winget install cppcheck |
C 静态分析引擎 |
| scripts/check_mojibake.py | 随仓库提交 | U+FFFD 乱码检测(纯 Python,无依赖) |
| scripts/check_coding_standard.py | 随仓库提交 | 代码规范检查(纯 Python,无依赖) |
| scripts/check_doc_links.py | 随仓库提交 | 文档链接存在性检查(纯 Python,无依赖) |
| scripts/gen_task_table.py | 随仓库提交 | FreeRTOS 任务表生成/校验(纯 Python,无依赖) |
| scripts/check_mem_numbers.py | 随仓库提交 | 内存/Flash 数字一致性(纯 Python,无依赖) |
| scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) |
| scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) |
| scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) |
| scripts/ram_baseline.py | 随仓库提交 | RAM 预算 92% 门禁 + 趋势台账(纯 Python,无依赖) |
| scripts/check_font_charset.py | 随仓库提交 | UI 字符集差集检查(纯 Python,无依赖;复用 tools/font_gen/ 两个生成器的提取与 cmap 解析规则) |
PATH 要求:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需重开终端);装在非 PATH 位置时可用
DTU_CPPCHECK环境变量指向cppcheck.exe完整路径(显式设置但无效 → 硬报错,不静默回退)。
2. 通用机制与索引(逐项详解已迁往 docs/门禁说明/)
2026-10-23 迁移:原本节 27 个"逐钩子详解"小节,已全部迁往
docs/门禁说明/——每种门禁 一篇,八段固定结构(查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / 限制与盲区 / 豁免与基线口径 / 改门禁本身)。40 个 hook 的总览与"三种拦截姿态" →docs/门禁说明/README.md。 搬走的原因:只写"拦了什么"的文档会让人以为绿就是对的,而"什么时候会骗人"必须有归处。 同一事实只在一处详述(docs/文档规范.md同此要求),所以本节不再重复逐钩子表格。
2.1 旧小节 → 新文档 对照(历史引用查号用)
| 旧小节编号(原手册 §2.N,已停用) | 现在住在 |
|---|---|
| 2.1 / 2.2 | docs/门禁说明/trailing-whitespace.md、docs/门禁说明/end-of-file-fixer.md |
| 2.3 / 2.4 / 2.5 | docs/门禁说明/check-added-large-files.md、check-merge-conflict.md、check-yaml.md |
| 2.6 | docs/门禁说明/cppcheck.md(含 test-cppcheck) |
| 2.7 / 2.8 | docs/门禁说明/no-mojibake.md、coding-standard.md |
| 2.9 / 2.10 / 2.11 | docs/门禁说明/doc-links.md、task-table.md、mem-numbers.md |
| 2.12 / 2.13 / 2.14 | docs/门禁说明/cubemx-survival.md、test-suite-switch.md、gitlint.md |
| 2.15 | docs/门禁说明/lv-conf-sync.md |
| 2.16 / 2.17 / 2.20 / 2.21 / 2.22 / 2.23 | docs/门禁说明/host-unit-tests.md(16 个 *-unit-test 合一篇,含各套件来历与用例规模) |
| 2.18 / 2.19 | docs/门禁说明/registration-check.md、mem-budget.md |
| 2.24 / 2.25 / 2.26 / 2.27 | docs/门禁说明/font-charset.md、id-check.md、layer-deps.md、bsp-err.md |
这些编号本身不再新增引用(历史变更记录里的旧编号按"冻结历史不回改"保留,按上表查号即可)。
2.2 跨钩子的通用机制(这些事实仍归本节)
| 机制 | 口径 |
|---|---|
| 两个阶段 | pre-commit 阶段 39 个钩子 + commit-msg 阶段 gitlint;default_install_hook_types: [pre-commit, commit-msg] 是显式声明的——换机后 pre-commit install 才会带上 commit-msg 钩子 |
| 一根钩子在输出里出现两次 | 未声明 stages: 的钩子默认属于所有阶段,pre-commit 与 commit-msg |
两遍都会列出它;第二遍的"文件"是提交信息文件,几乎必然 (no files to check)Skipped。**只看 |
|
| 输出结尾会把"实跑通过"读成"没跑"——判某钩子跑没跑要认它第一次**出现的那行(实测 | |
2026-10-24:只改 .pre-commit-config.yaml 的提交里 test-coverage 与 bsp-err 都是 Passed, |
|
| 尾巴那一遍是 Skipped) | |
| stash 陷阱 | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。改代码 + 同时更新基线/受管区的,必须先 git add 再跑钩子,否则会拿到假报警(本仓踩过,docs/门禁说明/bsp-err.md §4 记了现场) |
always: true 已废 |
pre-commit 4.x 对 local 钩子里这个键只告警 Unexpected key(s) 然后忽略 → "想总是跑"只能把 files: 写全(教训 → §5.5) |
| 改写型与只读型的豁免不对称 | YAML 锚点 &cubemx-generated 只豁免 trailing-whitespace 与 end-of-file-fixer;cppcheck / no-mojibake / cubemx-survival / task-table / mem-numbers 仍覆盖 Src/、Inc/ |
| 增量拦截三段式 | 规则恒生效 + 存量钉在基线/台账 + --prune 点名"已还清、请删行";脚本刻意没有 --write(不提供"一条命令把现状洗成合法"的入口)。使用者:layer-deps(6)、bsp-err(38)、id-check(台账 640 处,随 --prune-backlog 变动) |
没有 .map 即跳过 |
mem-numbers / mem-budget 在无 Keil 构建的机器(含远端 CI)上是跳过,不是"通过"——别把跳过读成有余量 |
| 配置自己坏 = 全部静默不跑 | 改完 .pre-commit-config.yaml 的验收不是过 check-yaml,而是实跑 -m pre_commit run --all-files,确认目标钩子出现在输出里且状态不是 Skipped |
| 范围策略 | 第三方与生成物只靠 .gitignore 与顶层 exclude(不在各钩子里重复写目录名);权威口径 → AGENTS.md 提交门禁一节 + §5.5 |
| 新增一个门禁的三步 | ① 配置落地并做一次注入自证(故意违反一次,看它是否当场 FAIL);② 在 docs/门禁说明/ 落一篇,"限制与盲区"一节不许留空;③ 在 docs/门禁说明/README.md 索引加一行 |
2.3 快速自查入口
- 提交前等价自查(L0 + L0b + L0.5):
scripts/ci_local.py(用.venv解释器跑)。 - 单独跑某一条:
-m pre_commit run <hook-id> --files <改动的文件>;全库:--all-files。 - 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补(
--no-verify会跳过全部 39 条,包括gitlint)。
3. 日常工作流
3.1 正常提交(秒级完成)
git add .
git commit -m "修复网络超时重连逻辑"
全部钩子自动运行,只检查本次 staged 的文件(增量扫描)。通常 <5 秒完成。
3.2 钩子失败后的处理
# 提交被拦截,查看报错信息
# 修复后重新 add + commit
git add .
git commit -m "修复网络超时重连逻辑"
常见失败场景与处理:
| 钩子 | 失败表现 | 处理步骤 |
|---|---|---|
| trailing-whitespace | Fixing xxx.c → 需要重新 git add |
钩子已自动修复文件,git add 后重新提交 |
| end-of-file-fixer | 同上 | 同上 |
| cppcheck | 报 warning/performance/portability/style | 根据报告修复代码;误报用行内抑制或 --suppress(详见 docs/代码规范/cppcheck使用说明.md) |
| no-mojibake | U+FFFD replacement chars found |
从 git 历史恢复干净版本(不可逆!) |
| coding-standard | L行号 [Sx x.x.x] 规则说明 |
按违规项修复(命名加前缀/大写化、补函数注释、sprintf 改 snprintf 等) |
| gitlint | Title exceeds max length |
精简 commit message 标题 |
3.3 全库扫描(配置变更后)
pre-commit run --all-files
仅在以下场景使用:
- 首次
pre-commit install后确认基线 - 修改
.pre-commit-config.yaml后验证 cppcheck新增 suppress 规则后验证
日常提交不需要跑
--all-files,pre-commit 会自动只检查 staged 文件。
3.4 手动运行单个钩子
pre-commit run cppcheck --all-files # 只跑 cppcheck
pre-commit run trailing-whitespace --files path/to/file.c # 只检查单个文件
pre-commit run no-mojibake --files path/to/file.c
pre-commit run gitlint --hook-stage commit-msg --commit-msg-filename .git/COMMIT_EDITMSG
3.5 临时跳过钩子(紧急情况)
git commit -m "紧急修复" --no-verify
# 或跳过特定钩子
SKIP=cppcheck git commit -m "紧急修复"
⚠️ 不推荐,仅用于紧急修复。跳过后的代码应在下一次提交中补上检查。
4. 新机器启用
# 1. 安装 pre-commit(需要 uv)
uv tool install pre-commit
# 2. 安装 cppcheck(需要 winget)
winget install cppcheck
# 重开终端使 PATH 生效
# 3. 进入仓库,安装钩子
cd D:\Code\DTU Code\STM32F4-Base
pre-commit install
# 4. 首次全库扫描(确认基线)
pre-commit run --all-files
5. 已知问题与注意事项
5.1 cppcheck 找不到
现象:Executable cppcheck not found
原因:cppcheck 安装后 PATH 更新在注册表,已运行的终端不会刷新。
解决:重开终端即可。若需在当前会话临时修复:
$env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Environment]::GetEnvironmentVariable("Path","User")
5.4 clang-format 未启用
原因:全库 222 个 .c/.h 文件大概率不满足 clang-format 默认格式,启用后会批量重写。
计划:Route B —— 全库统一格式化后形成独立基线 commit,再启用 clang-format 钩子。启用时取消 .pre-commit-config.yaml 中的注释即可。
5.5 顶层 exclude 曾整目录豁免 simulator/(2026-10-11 已修)
现象:改 simulator/ 下任何文件(含 simulator/lv_conf.h)的提交一个钩子都不跑——pre-commit run --files simulator/lv_conf.h 里所有钩子都是 (no files to check) Skipped。
根因:顶层 exclude 写了 simulator/。当初是为了挡 SDL2 预编译库、build/、截图产物,但这些已在 .gitignore 里,本就进不了 git 索引;pre-commit 只看索引中的文件,所以那条豁免唯一实际效果是把本项目自研的 21 个 tracked 文件(main.c、sim/*.c|h、tests/*.py、lv_conf.h、dbg_log.h、3 个 README)整体挡在门禁之外。
为什么必须修:模拟器与固件共用同一套 UI 源码,且它是 UI 回归唯一的像素级证据来源——它落在门禁外,等于验证链的地基不受检。
纳入后的实测收益(当天):cppcheck 立刻揪出 sim_syslog.c 一处真实空指针解引用(if (p_epoch == NULL || …) { *p_epoch = 0u; … }——判空分支里紧接着解引用),另有 2 处 localtime_s 未初始化告警、1 处 px 可加 const;coding-standard 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 __ 前缀保护宏、2 处 sprintf、2 处 CODE-RULE-11 误报),已全部处理。
顺带修掉的规则缺陷:CODE-RULE-11 原实现把任何含 / 的 include 判违规,误伤 <sys/stat.h> 这类系统头;已按 include 形态细化,判定见 docs/门禁说明/coding-standard.md §1。
教训:exclude 只写真正不该进钩子的路径。已被 .gitignore 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。
6. 文件清单
| 文件 | 位置 | 说明 |
|---|---|---|
.pre-commit-config.yaml |
仓库根 | 钩子配置(40 个钩子,2026-10-24 清点:新增 test-coverage 覆盖率总闸)。逐项详解在 docs/门禁说明/(索引 docs/门禁说明/README.md;主机侧单测 16 个合一篇,含各套件来历;UI 侧另有 font-charset 与 ui-golden 两篇——后者此前在旧手册里连小节都没有,只活在配置注释里,本次补齐) |
.gitlint |
仓库根 | gitlint 规则(宽松,兼容中文) |
.cubemx_survival_baseline.json |
仓库根 | CubeMX 易失定制基线(哈希;--write 重建) |
scripts/check_mojibake.py |
scripts/ | U+FFFD 检测脚本 |
scripts/check_coding_standard.py |
scripts/ | 代码规范检查脚本(规范 V1.0 可机械校验项) |
scripts/check_doc_links.py |
scripts/ | 文档链接存在性检查 |
scripts/gen_task_table.py |
scripts/ | FreeRTOS 任务表生成/校验 |
scripts/check_mem_numbers.py |
scripts/ | 内存/Flash 数字一致性(体积档) |
scripts/check_cubemx_survival.py |
scripts/ | CubeMX 重生成存活审计 |
scripts/check_lv_conf_sync.py |
scripts/ | 两份 lv_conf.h 特性子集一致性(比对生效值 + 无效宏名 + 重复定义) |
scripts/check_test_suite.py |
scripts/ | 测试套件开关门禁(防测试固件静默提交) |
scripts/check_font_charset.py |
scripts/ | UI 字符集差集检查(复用 tools/font_gen/ 提取与 cmap 解析规则,防缺字白框) |
scripts/check_layer_deps.py |
scripts/ | 分层方向门禁(BSP 反向 include App 的增量拦截,存量基线只减不增) |
scripts/check_bsp_err.py |
scripts/ | BSP 错误码与超时命名门禁(三条规则的增量拦截,存量 19 条基线只减不增;--emit-baseline 建基线) |
docs/代码规范/嵌入式C语言代码规范_V1.0.md |
docs/代码规范/ | 代码规范原文(CODE-RULE-01~CODE-RULE-20 规则来源;CODE-RULE-20 出处不在规范正文而在 |
AGENTS.md 平台约束,见 docs/门禁说明/coding-standard.md) |
||
docs/代码规范/cppcheck使用说明.md |
docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
docs/文档规范.md |
docs/ | 文档体系规范(链接形态/命名/门禁要求) |
docs/代码规范/pre-commit使用说明.md |
本文档 | 部署与使用说明 |
7. 快速参考卡片
提交代码: git add . && git commit -m "描述"
全库扫描: pre-commit run --all-files
单钩子扫描: pre-commit run <hook-id> --all-files
扫描单个文件: pre-commit run <hook-id> --files <path>
跳过钩子: git commit --no-verify (紧急)
查看已安装钩子: ls .git/hooks/
卸载钩子: pre-commit uninstall
更新钩子版本: pre-commit autoupdate
CubeMX 重生成后立刻执行(检查定制是否被覆盖):
python scripts/check_cubemx_survival.py # 列"被覆盖了哪几处"+修复提示
python scripts/check_cubemx_survival.py --write # 确认无误后重建基线