Files
STM32F4-Base/docs/代码规范/pre-commit使用说明.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

375 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 语法
├─ 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 看门狗判定状态机主机侧单测(主机侧纯逻辑单测)
├─ 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 格式
└─ manual(pre-commit --hook-stage manual;不随 git commit 跑)
├─ cppcheck 全量 C 静态分析(自有代码)
├─ test-cppcheck test/ 侧静态分析
└─ test-coverage 主机单测覆盖率总闸(44 套 + 回退台账)
```
**阶段**:`pre-commit` 阶段拦截代码问题、`commit-msg` 阶段拦截格式问题,任一失败 → commit 中止;另有 `manual` 阶段(`stages: [manual]` 的慢钩子)**不随 `git commit` 跑**,由远端 CI 与 `scripts/ci_local.py` 的 manual 段承担(见下)。一次 `git commit` 实跑 **48** 根 = `pre-commit` 阶段 47 根 + `gitlint` 1 根。
> **CI 侧的同一套门禁(2026-10-01 起)**:Gitea Actions(`.gitea/workflows/ci.yml`)在 push / PR 时
> 以 `pre-commit run --all-files` 复跑 **`pre-commit` 阶段的 47 根**(`mem-numbers` 在无 Keil `.map` 的 runner 上自适应跳过),
> 再以「手动阶段门禁」步**逐根** `--hook-stage manual` 复跑 `cppcheck` / `test-cppcheck` / `test-coverage` 三根
> (`pre_commit run a b c` 不支持多 id,实测报 `unrecognized arguments`)。
> 本地提交前想跑同一套判定:`& '.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 阶段 **48** 个钩子 + commit-msg 阶段 `gitlint`(= 一次 `git commit` 实跑 **49**)+ `manual` 阶段 3 根(`cppcheck` / `test-cppcheck` / `test-coverage`:`git commit` 不跑,只在远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;三根实测合计约 165 s ≈ 一次带代码提交耗时的三分之二)→ 口径 = YAML 解析后的 hook 条目数 **51**;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2;`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 记了现场)。**它藏不住什么,见下一行** |
| **stash 覆盖面:只覆盖已跟踪文件的未暂存改动**(2026-10-26 读源码定案,**对所有钩子通用**) | 机制在 pre-commit 的 `staged_files_only._unstaged_changes_cleared()`:`git diff-index <tree>` 取补丁 → `git checkout -- .` → 跑完 `git apply` 还原。所以「钩子实跑 = 只看到 HEAD + 本笔 staged」这句**对已跟踪文件成立、对未跟踪文件不成立**:未跟踪文件压根不在 `diff-index` 里,**不会被藏**;`git add -N`(intent-to-add)也救不了——pre-commit 只 `git rm --cached` 把它撤下索引(`_intent_to_add_cleared()`),**文件本体仍在盘上**。后果:别人写了尚未入库的文件(新页面实例 / 新单测目录 / 新文档)躺在树里时,**任何一笔碰到该文件所属钩子 `files:` 的提交都会被这份半成品拖红**——红的内容没错,但账不该记在这笔提交上。实测:`ui_page_clock.c`、`ui_page_reset.c` 处于未跟踪且未绑 `#page` 期间,`generated-fresh` 对**所有人**的 UI 提交都会 FAIL(现场日志 `docs/门禁说明/generated-fresh.md` §6 第 8 条)。三条结论:① 提交排序——会让全树变红的半成品要么先落地(绑引用 + 重跑生成器)要么先挪出工作树;② 凡「判定基准取工作区」的钩子(重跑比对类、全库统计类)在多人同树下必须配**静默窗口**;③ 真要 `--no-verify` 抢跑,正文必须点名是哪几个**未跟踪**文件拖的红,别让它被读成「钩子不可信」 |
| **`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`**(不提供"一条命令把现状洗成合法"的入口)。使用者(2026-10-26 实跑核对):`layer-deps`(**基线内 3** + ALLOW 永久豁免 3,共见 6 处)、`bsp-err`(38 条)、~~已退役的 `id-check`~(工作区台账已还清 = **0 条指纹 / 0 处**;HEAD 那版是 303 条指纹 / 651 处)。原写「layer-deps(6)」「id-check 台账 640 处」都是没跟着还账更新的旧数 |
| **没有 `.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 快速自查入口
- 提交前等价自查(静态门禁 + 提交信息门禁 + 主机侧纯逻辑单测):`scripts/ci_local.py`(用 `.venv` 解释器跑)。
- 单独跑某一条:`-m pre_commit run <hook-id> --files <改动的文件>`;全库:`--all-files`。
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补——**跳之前先跑 §2.4 自查表**(`--no-verify` 一次跳过 47 条,连 `gitlint` 一起跳 = 48 条,见 §2.2「三个阶段」行)。
### 2.4 抢跑自查表(`--no-verify` 之前自己跑一遍:只读、不 stash、别人正在写盘也安全)
`--no-verify` 跳过的不是「一条格式检查」,是**全部 47 条**(外加 commit-msg 阶段的 `gitlint`,共 48 条)。
2026-10-26 本仓连着抢了三笔,放走三条本该被拦的真问题,都在提交正文里可复核:
| 抢跑那笔 | 本该由谁拦下 | 后果什么时候才看得见 |
|---|---|---|
| `e7136cbe` | `coding-standard`(5 条 CODE-RULE-17:模块 `.c`/`.h` 配对)+ `end-of-file-fixer`(2 个文件缺尾换行) | 靠下一笔补;还**漏提**了 `test/host/console_log/run_unit_test.py` ⇒ HEAD 里代码已改成「整行独占」、旁边 runner 的文档串却还写着「无锁直发」——**比少一条断言更坏**,下一个人会被它教出错误心智模型 |
| `1d4b6a17` | `scripts/stack_report.py`(**不在钩子里,只能靠人跑**):控制台「整行独占」给 `dbg_console` 共享尾段加深 80 B 帧 → `iec104Task` 静态最坏链 1,544、margin **1,016 < 1,024** ISR 底线 → 退出码 1(实测差 8 B 过线) | **这是安全边界,不是风格问题**。2026-10-26 21:3x 用本机 20:39 那次构建的 `.htm` 复跑,`iec104Task` 仍是 `1016 / 39% / FAIL`、脚本退出码 1 ⇒ **这笔债还挂着**(谁来还:抬 `iec104Task` 栈要过 `mem-budget` 与 `docs/内存管理/` 台账,或把日志帧深压回去) |
所以纪律不是「以后别抢跑」,而是:**跳闸前把下面 A 组自己跑一遍**——它们全部只读、不 stash、别人正在写盘时
也能安全跑。对照 §2.2「stash 覆盖面」那一行:真提交会把**你自己**未暂存的改动藏起来,所以手跑通常比真提交
**更严**(不是更松);而别人**未跟踪**的半成品两边都藏不住 ⇒ 红了先认是谁的文件,既别替人背账,也别顺手把
别人的账混进你这一笔。
#### A. 抢跑前必跑(每条都是实跑验证过的入口)
| 查什么 | 命令(仓库根执行) | 判据 / 退出码 |
|---|---|---|
| **栈与最坏链**(今天漏的就是这条) | `& '.venv\Scripts\python.exe' scripts/stack_report.py` | 判据 `margin = cfg − max(静态最坏, 运行峰值) ≥ max(1024 B ISR, 30% × cfg)`;**0=全过 / 1=有任务不过 / 2=解析失败** |
| RAM 静态占用 | `& '.venv\Scripts\python.exe' scripts/ram_report.py` | 涨了的段要能指到具体模块与函数 |
| RAM 总量红线(`mem-budget` 的入口) | `& '.venv\Scripts\python.exe' scripts/ram_baseline.py --gate` | >92% 即 1;⚠ **0 包含「没有 `.map` 就跳过」——跳过不等于有余量** |
| 代码规范(含 CODE-RULE-17 / 20) | `& '.venv\Scripts\python.exe' scripts/check_coding_standard.py <本笔的 .c/.h>` | 有违规即 1 |
| BOM 漂移 | `& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --all` | HEAD 有 BOM 而工作区没有 → 1;⚠ **别用「有 BOM 就是错」的外行判据**——`Drivers/BSP/UI/*`、`App/task/ui_task.c` 等本就该带 BOM(§2.2 与 AGENTS 铁律) |
| 乱码 U+FFFD | `& '.venv\Scripts\python.exe' scripts/check_mojibake.py <本笔文件>` | 出现即 1 |
| **字节卫生 = 改写型钩子的只读等价** | 下面的一行式 + `git diff --check` | 一次查完**待提交清单(含未跟踪新文件)**的 CR / 缺尾换行 / U+FFFD / 行尾空格,全程不写盘 |
| 受影响模块的主机单测 | `& '.venv\Scripts\python.exe' test/host/<模块>/run_unit_test.py` | 只跑你碰的那几套;**改过骨架** `test/host/common/unit_test_harness.py` 就要跑全套(等价 22 根钩子同时触发) |
| 全量单测 + 覆盖率 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py` | 0=全过且覆盖率未回退 / 1=有失败或回退 / 2=环境(缺 `cl.exe` / mscoverage) |
| 编号 | `& '.venv\Scripts\python.exe' scripts/archive/id_gate/check_ids.py --strict` | 增量拦截:台账之外新错即 1;顺带核「可判定比例」与「不可判定绝对数」两行(2026-10-26 余量只剩 12 处) |
| 文档链接与章节 | `& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all` | 0 通过 / 1 有问题(钩子还多带 `--sections-strict`) |
| 生成器幂等 | `& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py` | 重跑到临时目录逐字节比对;**必须 `.venv` 解释器**,否则字库链假红 → `docs/门禁说明/generated-fresh.md` §4 |
| 一把梭(上面多数静态项 + 主机单测) | `& '.venv\Scripts\python.exe' scripts/ci_local.py` | 非 0 就别抢跑(但它**不含** `stack_report` / `ram_baseline`,数字与安全边界要另跑) |
> ⚠ **这张表为什么必须存在**:`stack_report` 与 `ram_baseline --gate` 这类**根本不在钩子清单里**(它们要 Keil 构建产物,
> 只能靠人在 `/keil-build` 后跑),所以「人漏跑一次」直接等于「这道检查这一次不存在」。想机械化,先要解决「磁盘上的
> `.htm` / `.map` 属于最后一次构建的人」这个归因问题——与 §2.2「stash 覆盖面」那条同源,不是加一根钩子就完事
> (`mem-budget` 今天红在「台账与 `.map` 不同码」正是同一个形状)。
一行式字节卫生(**整条刻意写成纯 ASCII**——粘进 PowerShell 与存成 `.ps1` 两种方式实测都过;含中文版在 Windows PowerShell 5.1 下按 ANSI 读 `.ps1` 会碎成语法错误,所以别加中文标签)。
实测 2026-10-26:全树 39 个待提交文件(**含 `??` 未跟踪**)→ `problems 0`,退出码 0:
```powershell
& '.venv\Scripts\python.exe' -c "import subprocess as sp,os,sys;st=sp.run(['git','status','--porcelain'],capture_output=True,text=True,encoding='utf-8',errors='replace').stdout.splitlines();ext=('.c','.h','.py','.md','.txt','.yaml','.json');fs=[l[3:].strip().strip(chr(34)) for l in st if l[3:].strip().endswith(ext)];fs=[p for p in fs if os.path.isfile(p)];bad=[];[bad.append((p,''.join(t).strip())) for p in fs for b in [open(p,'rb').read()] for t in [[('CR ' if b'\r' in b else '')+('NOEOL ' if not b.endswith(b'\n') else '')+('FFFD ' if b'\xef\xbf\xbd' in b else '')+('TWS ' if any(x.endswith(b' ') or x.endswith(b'\t') for x in b.split(b'\n')) else '')]] if t[0]];print('byte-hygiene: checked',len(fs),'files (incl untracked), problems',len(bad));[print(' ',p,'=>',t) for p,t in bad]"
```
#### B. 手跑**代替不了**真提交的三类,抢跑时得自己补
| 类别 | 为什么不等价 | 抢跑时怎么做 |
|---|---|---|
| **改写型钩子** `trailing-whitespace` / `end-of-file-fixer` | 它们会**改文件**。真提交里改的是 stash 之后的树;而手跑 `--files` **不 stash**(§2.2 那两行),直接落在工作区——**列了别人在飞的文件就等于替人改文件** | 只列自己的文件:`-m pre_commit run <id> --files <自己的文件>`;或先用 A 组的一行式查出问题自己修,别让钩子替你动盘 |
| **要靠 stash 才能正确归因**的判定:`mem-numbers` / `mem-budget`(读磁盘 `.map`)、`stack_report`(读磁盘 `.htm`)、`ui-golden` / `uinav-selftest`(要模拟器 exe 与台架锁)、`test-coverage`(会编译,可能撞别人的 `.obj`) | 磁盘上的 `.map` / `.htm` / `.exe` 属于**最后一次构建的人**,不是你的 ⇒ 手跑红常常是替别人背账(今天 `mem-budget` 就红在「台账与 `.map` 不同码」,因为 `.map` 里混着三条在飞源码道) | **照样要跑**(宁可多跑),但**红先归因**:`git status --porcelain` 看是谁在飞、`.htm` 的时间戳是哪次构建;确认不属本笔就在提交正文点名,**不要改基线数字去凑绿** |
| **只有真提交才会碰到的面**:`gitlint`(commit-msg 阶段)与未声明 `stages:` 会跑两遍的钩子 | 手跑脚本查不到提交信息规范(标题 ≤100、**正文每行 ≤80**) | 提交信息一律 `git commit -F <UTF-8 无 BOM 文件>` 并自查最长行;或先老实提交一次让 `gitlint` 说话,再改成 `--no-verify` |
| **`manual` 阶段的三根**(`cppcheck` / `test-cppcheck` / `test-coverage`) | `pre-commit run --all-files` 只跑 `pre-commit` 阶段(47 根),**不会碰它们**——漏跑不会报红,只会静默少一道门禁 | 抢跑时补:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`,一次一个 id(见 §2.2「三个阶段」行) |
#### C. 抢跑之后(三条都来自今天的真事,不是设想)
1. **对交付清单**:`git show --stat --oneline HEAD` 与提交正文声明的文件**逐条对上**;再 `git status --porcelain`,
里面**不该再出现你声明过的文件**(含 `??` 未跟踪)——「漏提 runner」正是这一步才会发现的。
2. **正文点名三件事**:哪些门禁没跑、为什么没跑、当时谁在飞。今天三笔都写了,**保留为强制**,不是可选项。
3. **静默窗口补一次全量**:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files`。⚠ `--all-files`
**同样不 stash**,而且会让**改写型钩子当场改全库文件**——只能在确认没人写盘的窗口跑,跑完 `git status`
复核被改动的是不是只有你那几笔(别人在飞时跑它 = 替人改文件,比重跑生成器更危险)。
---
### 2.5 写进提交信息与文档的每条归因,必须带可复核证据(2026-10-27 立,当天三次真实返工换来的)
钩子拦得下**形态**错,拦不下**归因**错。下面四条没有一条是机器能自动判的,但每一条当天都真的
发生过,代价从「别人返工」到「公开怪错一个道」都有:
1. **数字必须钉产物身份**:写一个数,就同时写它出自哪份产物(路径)+ 哪一次构建或运行时刻
(静态链写 `Linker run` 时刻,脚本类写日期 + 命令)。**重跑同一个报告脚本不算新测量**——产物
没变,数字必然不变,那只是复述;写成「我复验过」就是假凭证。
⚠ 本仓真实坑:`scripts/stack_report.py` 的 `REPO_ROOT` 跟着 `__file__` 走(不看 `cwd`),所以
「cd 进别的 worktree 再跑主仓的脚本」量的还是主仓产物。要量哪棵树,就用**那棵树自己的**
`scripts/`、从那棵树的根起跑。
2. **hash 不是稳定身份,tree + 父提交才是**:`--amend` 与重写正文都会换 hash。出「按 hash 复核」
的回执之前先 `git reflog` 看一眼有没有人被重写过;反过来说,**先请别人按 hash 复算、自己再去
重写 hash** 是顺序错误(2026-10-26 真实发生,等于让对方的回执一出生就过期)。要证明「只换了
正文」,用「`git diff 旧 新` 为空 + tree hash 相同」两条,比任何口头都硬。
3. **归因之前先确认这个数字来自哪棵树、哪一次运行**:并发时序不是谁的错。当天有过一次把「文件
mtime 晚于构建」误判成某个道「删了实现才去注册」,实际只是两条道同时在写、构建读到中间态;
更正时把两个道都点到名,并把这条写成硬规矩,而不是悄悄改口。
4. **「我核过」不等于机器核过**:当天两次真实反例——「6 个文件 BOM 逐个核过」随后被 `bom-guard`
抓到丢 BOM;自写的扫描脚本报「0 处嵌套注释」,是脚本自己有 bug 的假绿。结论一律附上**可复跑
的命令与退出码**(或门禁名 + 实测数字),不写「已确认无误」这种无法反驳的话。
**同族第二条:注释与文档必须陈述事实,而不是意图。** `ui_data_maint_ready()` 的头注释写着「页面用
它决定这页能不能操作——未注入时按钮一律拒并按 LOCK 显示」,而**页面从来没用过它**:全仓零消费者,
只有它自己的用例在读。这条值得单列,是因为它解释了失效机制——**评审读的是意图陈述,门禁读的是
可达路径,两者都不反驳「意图未实现」**,所以一个假接口能活过一整轮评审。写法要求:接口注释要么
描述当前真实的消费者与真实行为,要么显式写「尚无消费者,等待某件事落地」;**禁止用将来式或能力式
语气陈述未实现的行为**(那会让下一个人在它上面继续盖楼)。能被机器兜的那一半已经兜住了——死代码
靠 `bsp-err` 的 consume-result 与覆盖率总闸暴露,「改了生成器没重跑生成物」靠 `generated-fresh` 拦;
但**注释里的意图陈述没有任何钩子能证伪**,只能靠写的人自觉,所以才把它写在这里。
## 3. 日常工作流
### 3.1 正常提交(秒级完成)
```bash
git add .
git commit -m "修复网络超时重连逻辑"
```
**全部钩子自动运行**,只检查本次 staged 的文件(增量扫描)。通常 <5 秒完成。
### 3.2 钩子失败后的处理
```bash
# 提交被拦截,查看报错信息
# 修复后重新 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 全库扫描(配置变更后)
```bash
pre-commit run --all-files
```
仅在以下场景使用:
- 首次 `pre-commit install` 后确认基线
- 修改 `.pre-commit-config.yaml` 后验证
- `cppcheck` 新增 suppress 规则后验证
> 日常提交**不需要**跑 `--all-files`,pre-commit 会自动只检查 staged 文件。
### 3.4 手动运行单个钩子
```bash
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 临时跳过钩子(紧急情况)
```bash
git commit -m "紧急修复" --no-verify
# 或跳过特定钩子
SKIP=cppcheck git commit -m "紧急修复"
```
> ⚠️ **不推荐**:`--no-verify` 一次跳过 47 条(连 `gitlint` 一起 = 48 条;manual 阶段三根本来不随提交跑),不是「跳过格式检查」。**跳之前必须先跑 §2.4 抢跑自查表**
> (尤其 `stack_report.py`——它不在钩子清单里,只有人会跑;数字与安全边界类问题不会自己冒出来)。跳之后按 §2.4 C 组
> 补齐三件事:`git status` 对交付清单、正文点名「哪些没跑 / 为什么 / 谁在飞」、静默窗口补一次 `run --all-files`。
---
## 4. 新机器启用
```powershell
# 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 更新在注册表,已运行的终端不会刷新。
**解决**:重开终端即可。若需在当前会话临时修复:
```powershell
$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` | 仓库根 | 钩子配置(**52 个钩子**声明数,一次 `git commit` 实跑 49 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。声明数与一次提交的实跑数分列,勿混写;计数改动的完整台账只记在 AGENTS.md 提交门禁节与本目录 README §2,本行不重述)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「22 套无专属钩子」台账(现值 2026-10-29) `docs/门禁说明/host-unit-tests.md` §6;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 (紧急;跳之前先跑 §2.4 自查表)
查看已安装钩子: ls .git/hooks/
卸载钩子: pre-commit uninstall
更新钩子版本: pre-commit autoupdate
```
**CubeMX 重生成后立刻执行**(检查定制是否被覆盖):
```
python scripts/check_cubemx_survival.py # 列"被覆盖了哪几处"+修复提示
python scripts/check_cubemx_survival.py --write # 确认无误后重建基线
```