Files
STM32F4-Base/docs/代码规范/pre-commit使用说明.md
T

25 KiB
Raw Blame History

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              文档链接存在性(D-3/D-7,.md)
        ├─ task-table             FreeRTOS 任务表一致性(D-2/D-7)
        ├─ mem-numbers            内存/Flash 数字一致性(D-7 体积档)
        ├─ cubemx-survival        CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
        ├─ watchdog-unit-test     看门狗判定状态机主机侧单测(L0.5)
        └─ test-suite-switch      测试套件开关=产品构建(防测试固件静默提交)
  └─ commit-msg(.git/hooks/commit-msg)
       └─ gitlint                  校验 commit message 格式

两阶段钩子:pre-commit 阶段拦截代码问题,commit-msg 阶段拦截格式问题。任一失败 → commit 中止。

CI 侧的同一套门禁(2026-10-01 T-6):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,无依赖)

PATH 要求:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需重开终端);装在非 PATH 位置时可用 DTU_CPPCHECK 环境变量指向 cppcheck.exe 完整路径(显式设置但无效 → 硬报错,不静默回退)。


2. 各钩子规则详解

2.1 trailing-whitespace — 行尾空格清理

项 说明
触发条件 文件任意行末尾含空格或 Tab
处理方式 自动修复(直接改写文件,删除行尾空白)
影响范围 项目自有代码;豁免 CubeMX / Keil 生成文件(见下方"豁免清单")
典型报错 Trim Trailing Whitespace → Fixing path/to/file.c
修复方式 无需手动修复,钩子已自动改写;重新 git add 即可

注意:首次对全库运行时,可能批量修改大量文件的行尾空格。这是正常行为,不会改变代码逻辑。

豁免清单(2026-10-09 定,trailing-whitespace 与 end-of-file-fixer 共用同一份, 配置里以 YAML 锚点 &cubemx-generated 定义):Src/*.{c,h}、Inc/*.{c,h}、 Drivers/CMSIS/、Drivers/STM32F4xx_HAL_Driver/、Middlewares/、 MDK-ARM/*.{uvprojx,uvoptx,sct,map}、.mxproject、*.ioc。 理由:这些文件的内容事实源是 .ioc 与工具链,每次重生成都按工具自己的风格写回 (行尾空白、文件末尾不带换行)——2026-10-09 实测每次重生成后 Src//Inc/ 下 10+ 个 文件各被补 1 行末尾空行,纯属提交抖动。 ⚠ 只豁免"改写型"钩子:cppcheck / 乱码 / cubemx-survival / task-table / mem-numbers 等只读检查仍覆盖 Src/、Inc/(存活审计必须能看到这些文件)。

2.2 end-of-file-fixer — 文件尾换行

项 说明
触发条件 文件最后一行不是换行符(\n)
处理方式 自动修复(在文件尾部追加换行)
影响范围 项目自有代码;豁免 CubeMX / Keil 生成文件(同 2.1 豁免清单)
典型报错 Fix End of Files → Fixing path/to/file.c
修复方式 无需手动修复,钩子已自动改写;重新 git add 即可

2.3 check-added-large-files — 大文件拦截

项 说明
触发条件 单个文件 > 1MB(默认阈值)
处理方式 阻止提交(需手动修复)
典型报错 ilerge file added: path/to/file.bin
修复方式 确认文件是否应提交;若为构建产物则加入 .gitignore,若为必要大文件用 --no-verify 跳过

2.4 check-merge-conflict — 冲突标记拦截

项 说明
触发条件 文件含 <<<<<<<、=======、>>>>>>> 冲突标记
处理方式 阻止提交
修复方式 手动解决冲突后重新提交

2.5 check-yaml — YAML 语法校验

项 说明
触发条件 .yaml / .yml 文件语法错误
处理方式 阻止提交
检查对象 主要校验 .pre-commit-config.yaml 本身
修复方式 修正 YAML 缩进或语法后重新提交

2.6 cppcheck — C 静态分析

cppcheck 是开源 C/C++ 静态分析工具,不运行代码,通过语法分析和数据流推理发现潜在缺陷。本项目开启 warning / performance / portability / style 四个类别,任一类别发现问题即阻止提交(--error-exitcode=1)。

项 说明
触发条件 自有代码 .c 文件(App/、Src/、Inc/、Drivers/BSP/,第三方库除外)含潜在缺陷、冗余代码或可疑写法
处理方式 阻止提交(必须修复)
修复方式 按报告修复代码;确属误报用行内抑制 // cppcheck-suppress 或全局 --suppress
典型报错 [path/to/file.c:123]: (style) variableScope The scope of the variable ... can be reduced
增量速度 单文件约 1~2 秒;全库约 5 秒

完整规则清单(style 类别 95 项详解)、配置参数、YAML 引号陷阱、误报抑制方法见 docs/代码规范/cppcheck使用说明.md。

2.7 no-mojibake — U+FFFD 乱码拦截

项 说明
触发条件 C 文件含 EF BF BD(U+FFFD 替换符,表示不可逆的编码损坏)
处理方式 阻止提交(不可逆,必须修复)
检测原理 scripts/check_mojibake.py 以二进制读取文件,检测 0xEF 0xBF 0xBD 字节序列
典型报错 U+FFFD replacement chars found (encoding corrupted, irreversible!): path/to/file.c
修复方式 从 git 历史中找到最后一个干净版本,手动恢复损坏区域

⚠️ 编码铁律:U+FFFD 是不可逆损坏(无法从替换符反推原始中文字符)。若发现新文件出现 U+FFFD,必须排查写入方式(禁用 Get-Content / Set-Content 重写中文文件)。

2.8 coding-standard — C 代码规范检查

依据 docs/代码规范/嵌入式C语言代码规范_V1.0.md,用 scripts/check_coding_standard.py 对可机械化校验的规则做提交前拦截(纯 Python 正则扫描,无依赖)。

项 说明
触发条件 自有代码 .c/.h 文件(App/、Drivers/BSP/,第三方与 CubeMX 生成文件除外)违反下列规则
处理方式 阻止提交(必须修复)
典型报错 L123 [S3 3.4.1] 宏名须全大写+下划线: net_htonl

检查项(S 编号与脚本输出一致):

编号 规范条款 规则
S1 2.3.1 .h 文件必须有 XXX_H 格式头文件保护宏(禁止 __ 前缀)
S2 5.2 文件开头必须有文件头注释(含 模块名称/作者)
S3 3.4.1 #define 宏名必须全大写+下划线+数字
S4 3.5.1 typedef 类型名必须以 _t 结尾
S5 6.3.2 switch 必须包含 default 分支
S6 7.5 禁用 strcpy/strcat/sprintf/gets/scanf/strtok(改用安全版本)
S7 6.3.3 禁止 goto
S8 4.1.1 禁止 Tab 缩进(4 空格)
S9 5.3 函数定义必须有函数注释(含「函数功能」)
S10 3.3.1 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
S11 2.3.5 禁止相对/绝对路径 include
S12 7.6 禁止可变参数函数(...)
S13 7.7 禁止递归函数
S15 6.1.4 文件作用域指针变量必须初始化为 NULL
S16 3.2.2 文件作用域非 static 全局变量必须 g_ 前缀
S17 2.2 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过)
S18 6.3.1 禁止 Yoda 条件(字面量/常量必须在运算符右侧)
S19 4.4.2 函数定义左大括号必须与右括号同行

不在检查范围(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。

排除文件(与 cppcheck 钩子范围一致,另加):Src/、Inc/(CubeMX 生成,重新生成即覆盖)、Drivers/BSP/GD5F2GQ5UE/nand_ftl.*(dhara/FatFS 接口适配层,函数名由外部 API 固定)。

用 scripts/check_doc_links.py 校验 Markdown 文档里引用的仓库内路径是否真实存在(纯 Python 标准库,无依赖)。

项 说明
触发条件 改动任意 .md 文件,且其中链接指向不存在的仓库内路径
处理方式 阻止提交(改路径,或按规范加 [计划] / 状态词)
检查范围 [文本](相对路径) 链接(相对当前文档解析)+ 反引号内 `docs/xxx.md` 仓库根相对路径
典型报错 [示例] docs/架构设计/图形绘制流程.md:73 [markdown 链接] images/RemoDispBus帧格式.svg

豁免(不算失效):[计划] 前缀;该行含 [示例] 标记(引用报错样例/日志片段时用);引用附近 ±40 字符内的状态词(未创建/尚未创建/已删除/待排期);占位符(*、<模块>、NN_、${...});行内代码 span 里的链接写法(![标题](相对路径.svg) 这类语法示例);外部 URL 与纯锚点;docs/芯片手册/ 等外部资料目录。

排除文件:.opencode/node_modules/、Lib/lvgl/、simulator/、MDK-ARM/RTE/、docs/芯片手册/。

# 全库扫描(开发时自检)
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all

规范依据:docs/文档规范.md §3(链接规范)、§4(门禁)。

2.10 task-table — FreeRTOS 任务表一致性(D-2/D-7)

用 scripts/gen_task_table.py 以 Src/freertos.c 的 osThreadAttr_t 为唯一事实源,校验 README.md / AGENTS.md 中的任务表是否同步(防止改栈后文档失真——2026-09 曾两张表都比源码落后一大截)。

项 说明
触发条件 改动 README.md / AGENTS.md / Src/freertos.c,且受管表格区与源码不一致
处理方式 阻止提交;跑 --write 由脚本自动重写表格
受管区 <!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py --> ~ <!-- TASK_TABLE:END --> 之间
典型报错 [FAIL] 以下文档的任务表与 Src/freertos.c 不一致:README.md
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check   # 检查(钩子用)
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --write   # 自动同步表格
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --diff    # 只看期望表格

⚠️ 改栈流程:先改 Src/freertos.c → 跑 --write 同步文档 → 提交。钩子报不一致时不要手改表格。 脚本按原换行符 / BOM 状态回写,不会造成整文件行尾漂移。

2.11 mem-numbers — 内存/Flash 数字一致性(D-7 体积档)

用 scripts/check_mem_numbers.py 以 .map 为唯一事实源,校验文档里的内存/Flash 数字。 文档中手抄的 RW_IRAM1 82,048 B / 62.6% 这类数字会随代码变化而失真——实测 2026-09-26 复核时 文档仍写 82,048 / 62.6%,实际 .map 已是 87,040 / 66.4%。

项 说明
触发条件 改动 docs/内存管理/*.md / App·Src·Inc 源码 / 本脚本
处理方式 阻止提交;受管区跑 --write 自动同步;散句陈旧数字需人工复核
受管区 <!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py --> ~ <!-- MEM_SNAPSHOT:END -->
典型报错 [FAIL] 以下文档的内存快照与 .map 不一致:docs/内存管理/RAM使用分析与优化方案.md
前置 需存在 .map(@build 产物);缺失时脚本报 [FAIL] 找不到 .map 事实源 并提示先构建
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan    # 钩子用(含陈旧散句扫描)
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --write   # 同步受管快照
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --diff    # 只看期望快照

陈旧数字扫描:从 docs/内存管理/data/ram_report_*.md 历史快照提取「曾为区域用量、现已变化」 的值,正文若仍以现状口吻引用即告警。含「历史/旧/曾/快照/data//实施时」等词的行使行豁免; 讲任务栈/内存池/峰值水位的行也跳过(数字语义不同)。

⚠️ 设计取舍:朴素"扫所有 4~7 位数字"实测 45 处误报(栈配置 4096、堆 49,152、 峰值 1184 都是合法值)。改为"只报历史快照里出现的、当前已变的区域值"后 0 误报。 宁可窄而准——吵的门禁会被绕过。

2.12 cubemx-survival — CubeMX 重生成存活审计

用 scripts/check_cubemx_survival.py 核对"重生成易失清单",防止 CubeMX 重新生成时 静默覆盖手工定制。.ioc 的 ProjectManager.KeepUserCode=true 只保护 /* USER CODE BEGIN/END */ 标记区内的内容——标记之外的一切都会被模板重写, 且多数不报编译错误,只在真出故障时才暴露(2026-09-29 实测一次重生成覆盖 4 处, 含 HardFault_Handler 崩溃采集被打回空 while(1)、两个 FreeRTOS 加固宏被整块删除)。

项 说明
触发条件 改动 STM32F407-Demo.ioc / Src/*.{c,h} / Inc/*.h / App/util/task_watchdog.* / MDK-ARM/*.uvprojx / 本脚本
处理方式 阻止提交;清单项缺失或基线漂移即 FAIL
检查内容 16 项:IWDG 延后启动、崩溃采集×2、栈溢出检测、堆失败钩子、断言改造×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、.ioc 关键项×3
基线文件 .cubemx_survival_baseline.json(各项内容哈希;--write 重建)
防误报 断言前剥掉 C 注释——本项目多处注释刻意引用历史代码(如"原为 HAL_IWDG_Init(&hiwdg) 直呼"),不过滤会恒定误报
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py            # 审计报告(人读,含修复提示)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --check    # 门禁模式(钩子用)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --write    # 重建基线(确认改动无误后)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --report   # 只打印报告,不比对基线

推荐工作流:每次 CubeMX 重生成后立刻跑一次(不带参数)——它会把 "被覆盖了哪几处"直接列出并附修复提示,比事后靠故障现象反推快得多。

标记区外的存量以 WARN 提示(Src/stm32f4xx_it.c 的 3 个 Fault 处理函数、 Src/freertos.c 的栈注释)——这些 CubeMX 不保护,只能人工盯;建议后续 仿照 HardFault_Handler 自造 USER CODE 标记区把它们升级为受保护内容。

方法论(L1 写 .ioc > L2 搬进 USER CODE 区 > L3 审计兜底)详见 ../子系统设计/看门狗设计说明.md §10.2。

2.13 test-suite-switch — 测试套件开关=产品构建

用 scripts/check_test_suite.py 检查 test/board/test_config.h 的 TEST_SUITE_* 开关 是否已全部注释(= 产品构建),防止测试固件被静默提交。

项 说明
触发条件 改动 test/board/test_config.h,且其中有 TEST_SUITE_* 处于启用状态
处理方式 阻止提交(注释掉开关,或显式放行测试构建)
典型报错 [FAIL] 默认要求产品构建:请注释掉 test/board/test_config.h 中的 TEST_SUITE_* 开关。
放行方式 python scripts/check_test_suite.py --allow-test-build 或 ALLOW_TEST_BUILD=1 git commit ...(放行后仍打 [WARN] 横幅提醒)

为什么需要这个钩子(2026-10-01 实测): test_config.h 的 TEST_SUITE_CH395F/STORAGE/GD5F/RTC 决定固件里编入哪个测试套件, 全部注释 = 产品构建——但此前 grep TEST_SUITE scripts/*.py 零命中,没有任何门禁 检查它。实测已提交的 HEAD 里 TEST_SUITE_CH395F 就是开启状态(.map 含 34 处 test 代码), 即仓库里躺着的是测试固件而非产品固件,纯靠开发者记得改回去。

判定实现:先剥掉块注释与行注释再匹配 #define,所以注释掉的开关 (//#define TEST_SUITE_GD5F)不会误判;同文件派生的 TEST_SUITE_SELECTED / TEST_SUITE_NAME 由白名单排除(它们是开关的推导结果,非用户可选项)。

& '.venv\Scripts\python.exe' scripts/check_test_suite.py                    # 门禁判定
& '.venv\Scripts\python.exe' scripts/check_test_suite.py --allow-test-build # 显式放行

套件互斥(一次最多启用一个)由 test_config.h:45 的 #error 在编译期兜底; 本钩子在提交期再拦一道(#error 只在真正构建时暴露,测试固件仍可能被提交)。

2.14 gitlint — Commit Message 格式

项 说明
触发条件 commit message 标题超 100 字符 / 含 body-is-missing 警告
处理方式 阻止提交(commit-msg 阶段)
当前规则 宽松:不强制 Conventional Commits,兼容中文标题
典型报错 T1 Title exceeds max length (159>100)
修复方式 精简 commit message 标题到 100 字符以内,或拆分为多行(标题+空行+body)

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 中的注释即可。


6. 文件清单

文件 位置 说明
.pre-commit-config.yaml 仓库根 钩子配置(15 个钩子 + 1 个预留)
.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/ 文档链接存在性检查(D-3/D-7)
scripts/gen_task_table.py scripts/ FreeRTOS 任务表生成/校验(D-2/D-7)
scripts/check_mem_numbers.py scripts/ 内存/Flash 数字一致性(D-7 体积档)
scripts/check_cubemx_survival.py scripts/ CubeMX 重生成存活审计
scripts/check_test_suite.py scripts/ 测试套件开关门禁(防测试固件静默提交)
docs/代码规范/嵌入式C语言代码规范_V1.0.md docs/代码规范/ 代码规范原文(S1~S19 规则来源)
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    # 确认无误后重建基线