4a4ad6bc5f
现状口径:/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 全过;字节卫生零漂移。
375 lines
34 KiB
Markdown
375 lines
34 KiB
Markdown
# 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 # 确认无误后重建基线
|
||
```
|