1
0

refactor: 重写 Git hooks 体系,委托已有检查、新增模板与 LFS 校验

pre-commit 代码检查改为委托 _backend-lint / _versionctl-lint / _frontend-check,新增 LFS 指针校验;commit-msg 新增多行空行格式校验和模板注释忽略,移除 CJK/Python 字符集检测;新增 prepare-commit-msg 提交信息模板;hooks-install 增加 source 文件存在性校验;前端 check 补入 tsc -b 类型检查并修复暴露的类型错误
This commit is contained in:
2026-05-06 13:44:28 +08:00
parent 5513f0c13d
commit c04a13bf8a
13 changed files with 443 additions and 111 deletions

View File

@@ -70,15 +70,21 @@ clean: _backend-clean _frontend-clean _desktop-clean
hooks-install:
@hooks_dir=$$(git rev-parse --git-path hooks); \
mkdir -p "$$hooks_dir"; \
cp scripts/git-hooks/pre-commit "$$hooks_dir/pre-commit"; \
cp scripts/git-hooks/commit-msg "$$hooks_dir/commit-msg"; \
chmod +x "$$hooks_dir/pre-commit" "$$hooks_dir/commit-msg"; \
for hook in pre-commit commit-msg prepare-commit-msg; do \
src="scripts/git-hooks/$$hook"; \
if [ ! -f "$$src" ]; then \
printf 'ERROR: source hook not found: %s\n' "$$src" >&2; \
exit 1; \
fi; \
cp "$$src" "$$hooks_dir/$$hook"; \
chmod +x "$$hooks_dir/$$hook"; \
done; \
printf 'Installed Git hooks to %s\n' "$$hooks_dir"
hooks-check:
@hooks_dir=$$(git rev-parse --git-path hooks); \
status=0; \
for hook in pre-commit commit-msg; do \
for hook in pre-commit commit-msg prepare-commit-msg; do \
if [ -x "$$hooks_dir/$$hook" ]; then \
printf 'OK: %s\n' "$$hook"; \
else \
@@ -92,17 +98,18 @@ hooks-test:
@scripts/git-hooks/test-hooks.sh
_hooks-pre-commit:
@set -e; \
@set -ef; \
staged_files=$$(git diff --cached --name-only --diff-filter=ACM); \
if [ -z "$$staged_files" ]; then \
printf 'No staged files to check\n'; \
exit 0; \
fi; \
backend_pkgs=''; \
versionctl_pkgs=''; \
run_backend_lint=; \
run_versionctl_lint=; \
run_frontend_check=; \
lfs_patterns=$$(grep 'filter=lfs' .gitattributes 2>/dev/null | awk '{print $$1}' || true); \
for file in $$staged_files; do \
[ -n "$$file" ] || continue; \
case "$$file" in scripts/git-hooks/*) continue ;; esac; \
if git show ":$$file" 2>/dev/null | grep -Eq '^(<<<<<<<|=======|>>>>>>>)'; then \
printf 'Found conflict markers in staged file: %s\n' "$$file" >&2; \
printf 'Resolve conflict markers before committing.\n' >&2; \
@@ -114,37 +121,41 @@ _hooks-pre-commit:
printf 'Warning: large staged text file (%s bytes): %s\n' "$$size" "$$file" >&2; \
fi; \
fi; \
if [ -n "$$lfs_patterns" ]; then \
for lfs_pat in $$lfs_patterns; do \
case "$$file" in $$lfs_pat) \
content=$$(git show ":$$file" 2>/dev/null | head -1); \
case "$$content" in \
"version https://git-lfs.github.com/spec/v1"*) ;; \
*) \
printf 'LFS-tracked file not using LFS pointer: %s\n' "$$file" >&2; \
printf 'Run "git lfs install" and re-add this file.\n' >&2; \
exit 1; \
;; \
esac; \
break; \
;; \
esac; \
done; \
fi; \
case "$$file" in \
backend/*.go) \
dir=$$(dirname "$${file#backend/}"); \
case " $$backend_pkgs " in *" $$dir "*) ;; *) backend_pkgs="$$backend_pkgs $$dir" ;; esac; \
;; \
versionctl/*.go) \
dir=$$(dirname "$${file#versionctl/}"); \
case " $$versionctl_pkgs " in *" $$dir "*) ;; *) versionctl_pkgs="$$versionctl_pkgs $$dir" ;; esac; \
;; \
frontend/*.ts|frontend/*.tsx) \
rel=$${file#frontend/}; \
printf 'Frontend lint: frontend/%s\n' "$$rel"; \
(cd frontend && bunx eslint "$$rel"); \
printf 'Frontend format: frontend/%s\n' "$$rel"; \
(cd frontend && bunx prettier --check "$$rel"); \
;; \
frontend/*.scss) \
rel=$${file#frontend/}; \
printf 'Frontend format: frontend/%s\n' "$$rel"; \
(cd frontend && bunx prettier --check "$$rel"); \
;; \
backend/*.go) run_backend_lint=1 ;; \
versionctl/*.go) run_versionctl_lint=1 ;; \
frontend/*.ts|frontend/*.tsx|frontend/*.scss) run_frontend_check=1 ;; \
esac; \
done; \
for dir in $$backend_pkgs; do \
printf 'Go lint: backend/%s\n' "$$dir"; \
(cd backend && go tool golangci-lint run "$$dir/"); \
done; \
for dir in $$versionctl_pkgs; do \
printf 'Go lint: versionctl/%s\n' "$$dir"; \
(cd versionctl && go tool golangci-lint run "$$dir/"); \
done; \
if [ -n "$$run_backend_lint" ]; then \
printf 'Running backend lint...\n'; \
$(MAKE) _backend-lint; \
fi; \
if [ -n "$$run_versionctl_lint" ]; then \
printf 'Running versionctl lint...\n'; \
$(MAKE) _versionctl-lint; \
fi; \
if [ -n "$$run_frontend_check" ]; then \
printf 'Running frontend check...\n'; \
$(MAKE) _frontend-check; \
fi; \
printf 'Pre-commit checks passed\n'
# ============================================

View File

@@ -369,8 +369,9 @@ make desktop-clean # 清理 desktop 产物
Git hooks 使用仓库内 `scripts/git-hooks/` 的原生脚本,不依赖额外 hook 框架。当前 hooks 包含:
- pre-commit检查 staged files 的冲突标记、Go lint、前端 lint/格式和大文件告警
- commit-msg校验提交信息格式为 `类型: 简短描述`,描述需使用中文
- pre-commit检查 staged files 的冲突标记、大文件告警和 LFS 指针并按文件类型委托后端、versionctl、前端检查
- prepare-commit-msg在编辑器打开时提供提交信息模板,辅助填写 `类型: 简短描述` 和多行说明
- commit-msg校验提交信息格式为 `类型: 简短描述`多行说明需在首行后保留空行提交描述按项目规范使用中文hook 不做字符集检测
## 版本与发布

View File

@@ -149,7 +149,8 @@ bun run build
```bash
bun run lint # ESLint 检查
bun run format:check # Prettier 格式检查
bun run check # 同时检查 lint 和格式
bun run typecheck # TypeScript 类型检查
bun run check # 同时检查类型、lint 和格式
```
### 代码格式化

View File

@@ -6,12 +6,13 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && bun run check && vite build",
"build": "bun run check && vite build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "bun run lint && bun run format:check",
"typecheck": "tsc -b",
"check": "bun run typecheck && bun run lint && bun run format:check",
"fix": "bun run lint:fix && bun run format",
"preview": "vite preview",
"test": "vitest run",

View File

@@ -38,7 +38,9 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
text: id,
onCopy: () => MessagePlugin.success('已复制统一模型 ID'),
}}
/>
>
{''}
</Typography.Text>
</span>
) : null
},

View File

@@ -51,7 +51,9 @@ export function ProviderTable({
text: row.baseUrl,
onCopy: () => MessagePlugin.success('已复制 Base URL'),
}}
/>
>
{''}
</Typography.Text>
</span>
) : null,
},
@@ -87,7 +89,9 @@ export function ProviderTable({
text: row.apiKey,
onCopy: () => MessagePlugin.success('已复制 API Key'),
}}
/>
>
{''}
</Typography.Text>
</span>
) : null,
},

View File

@@ -49,13 +49,14 @@ TBD - 定义前端 ESLint 规则配置、构建集成 lint 检查、以及自定
### Requirement: 构建集成 lint 检查
前端 SHALL 在 `build` 命令中集成 ESLint 检查和 Prettier 格式检查。
前端 SHALL 在 `build` 命令中集成 TypeScript 类型检查、ESLint 检查和 Prettier 格式检查。
#### Scenario: 构建时执行 lint 和格式检查
#### Scenario: 构建时执行类型检查、lint 和格式检查
- **WHEN** 执行 `bun run build`
- **THEN** 构建 SHALL 依次执行 `tsc -b``bun run check``vite build`
- **THEN** `bun run check` SHALL 执行 `bun run lint && bun run format:check`
- **THEN** 构建 SHALL 依次执行 `bun run check``vite build`
- **THEN** `bun run check` SHALL 依次执行 `bun run typecheck``bun run lint``bun run format:check`
- **THEN** 若 `tsc -b` 报告类型错误,构建 SHALL 中断
- **THEN** 若 `eslint .` 报告任何错误,构建 SHALL 中断
- **THEN** 若 `prettier --check .` 报告任何格式问题,构建 SHALL 中断
@@ -77,8 +78,13 @@ TBD - 定义前端 ESLint 规则配置、构建集成 lint 检查、以及自定
#### Scenario: 统一检查命令
- **WHEN** 执行 `bun run check`
- **THEN** SHALL 运行 `bun run lint && bun run format:check`
- **THEN** lint 错误和格式问题 SHALL 都被检查
- **THEN** SHALL 依次运行 `bun run typecheck``bun run lint``bun run format:check`
- **THEN** 类型错误、lint 错误和格式问题 SHALL 都被检查
#### Scenario: 单独执行类型检查
- **WHEN** 执行 `bun run typecheck`
- **THEN** SHALL 运行 `tsc -b`
#### Scenario: 统一修复命令

View File

@@ -8,12 +8,33 @@
### Requirement: pre-commit hook 快速检查
pre-commit hook SHALL 在 `git commit` 执行前对 staged files 进行快速检查,仅检查本次提交涉及的文件
pre-commit hook SHALL 在 `git commit` 执行前对 staged files 进行快速检查。非代码检查冲突标记、大文件告警、LFS 指针SHALL 在 `_hooks-pre-commit` 中直接实现代码检查Go 后端、Go versionctl、前端SHALL 根据 staged 文件类型有条件地委托给已有 Makefile target`_backend-lint``_versionctl-lint``_frontend-check`),不再内联独立的 lint 命令
#### Scenario: 无 Go 和前端文件变更时跳过
#### Scenario: 无 Go 和前端文件变更时跳过代码检查
- **WHEN** staged files 中既无 `.go` 文件也无 `.ts`/`.tsx`/`.scss` 文件
- **THEN** pre-commit hook SHALL 直接通过,不运行任何 linter
- **THEN** pre-commit hook SHALL 跳过代码检查委托,仅执行非代码检查
#### Scenario: Go 文件变更时委托后端 lint
- **WHEN** staged files 中包含 `backend/*.go` 文件
- **THEN** pre-commit hook SHALL 委托 `_backend-lint` target 进行 Go 代码检查
- **THEN** `_backend-lint` SHALL 复用 `backend/.golangci.yml` 配置
- **THEN** 若 lint 报告任何错误commit SHALL 被阻止
#### Scenario: versionctl Go 文件变更时委托 versionctl lint
- **WHEN** staged files 中包含 `versionctl/*.go` 文件
- **THEN** pre-commit hook SHALL 委托 `_versionctl-lint` target 进行 Go 代码检查
- **THEN** `_versionctl-lint` SHALL 复用 `versionctl/.golangci.yml` 配置
- **THEN** 若 lint 报告任何错误commit SHALL 被阻止
#### Scenario: 前端文件变更时委托前端检查
- **WHEN** staged files 中包含 `.ts``.tsx``.scss` 文件
- **THEN** pre-commit hook SHALL 委托 `_frontend-check` target 进行前端代码检查
- **THEN** `_frontend-check` SHALL 运行 `bun run check`(包含 `tsc -b` TypeScript 类型检查、ESLint 和 Prettier 格式检查)
- **THEN** 若检查报告任何错误commit SHALL 被阻止
#### Scenario: 冲突标记检测
@@ -21,37 +42,26 @@ pre-commit hook SHALL 在 `git commit` 执行前对 staged files 进行快速检
- **THEN** pre-commit hook SHALL 报告错误并列出包含冲突的文件名
- **THEN** commit SHALL 被阻止
#### Scenario: Go 文件 lint 检查
- **WHEN** staged files 中包含 `.go` 文件
- **THEN** pre-commit hook SHALL 对 staged `.go` 文件运行 `golangci-lint run`(复用 `backend/.golangci.yml` 配置)
- **THEN** 若 lint 报告任何错误commit SHALL 被阻止
#### Scenario: 前端文件 lint 检查
- **WHEN** staged files 中包含 `.ts``.tsx` 文件
- **THEN** pre-commit hook SHALL 对 staged 前端文件运行 ESLint复用 `frontend/eslint.config.js` 配置)
- **THEN** 若 ESLint 报告任何错误commit SHALL 被阻止
#### Scenario: 前端文件格式检查
- **WHEN** staged files 中包含 `.ts``.tsx``.scss` 文件
- **THEN** pre-commit hook SHALL 对 staged 前端文件运行 Prettier 格式检查(复用 `frontend/.prettierrc` 配置)
- **THEN** 若存在格式不符合规范的文件commit SHALL 被阻止
#### Scenario: 大文件告警
- **WHEN** staged files 中存在超过 500KB 的文本文件
- **THEN** pre-commit hook SHALL 输出警告信息(不阻止提交),提示检查是否误提交
#### Scenario: LFS 指针校验
- **WHEN** staged files 匹配 `.gitattributes``filter=lfs` 的路径模式
- **THEN** pre-commit hook SHALL 检查 staged 内容是否为 LFS 指针格式(`version https://git-lfs.github.com/spec/v1`
- **THEN** 若内容不是 LFS 指针格式commit SHALL 被阻止,并提示安装 git-lfs
- **THEN** 若 staged files 不匹配任何 `filter=lfs` 路径模式SHALL 跳过此检查
#### Scenario: commit 被阻止时显示修复提示
- **WHEN** pre-commit hook 检查失败
- **THEN** hook SHALL 输出明确的修复提示(如 `bun run fix`、手动解决冲突标记等)
- **THEN** hook SHALL 输出明确的修复提示(如 `make lint` 修复代码问题、手动解决冲突标记等)
### Requirement: commit-msg hook 校验提交信息格式
commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确保符合项目规范。提交描述 SHALL 使用中文;版本号、英文专有名词可与中文描述混用
commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确保首行符合项目规范。提交描述按项目规范应使用中文,但 hook SHALL NOT 通过 Python/CJK 字符集检测强制判断描述语言,以避免引入新的运行时依赖
#### Scenario: 合法格式通过
@@ -63,12 +73,6 @@ commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确
- **WHEN** 提交信息首行使用的类型不在允许列表中(如 `update: xxx`
- **THEN** commit-msg hook SHALL 报告错误显示允许的类型列表commit SHALL 被阻止
#### Scenario: 英文描述被拒绝
- **WHEN** 提交信息首行为 `feat: add auth`
- **THEN** commit-msg hook SHALL 报告错误,提示提交描述需使用中文
- **THEN** commit SHALL 被阻止
#### Scenario: 缺少冒号空格被拒绝
- **WHEN** 提交信息首行为 `feat:xxx`(冒号后无空格)或 `feat xxx`
@@ -89,16 +93,35 @@ commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确
- **WHEN** commit-msg hook 检查失败
- **THEN** hook SHALL 输出包含正确格式示例的错误信息(如 `feat: 添加供应商批量管理功能`
#### Scenario: 不执行字符集检测
- **WHEN** 提交信息首行格式合法且类型合法,但描述部分不包含 CJK 字符(如 `feat: add hook tests`
- **THEN** commit-msg hook SHALL 通过
- **THEN** hook SHALL NOT 调用 `python3` 或其他额外运行时做 Unicode/CJK 检测
#### Scenario: 多行格式校验
- **WHEN** 提交信息忽略 `#` 注释行后,第三行及之后存在任一非空详细说明行
- **THEN** commit-msg hook SHALL 检查第二行是否为空行
- **THEN** 若第二行非空行commit SHALL 被阻止,提示首行后应空行再写详细描述
#### Scenario: 模板注释不参与校验
- **WHEN** 提交信息文件中包含 prepare-commit-msg 写入的 `#` 注释模板
- **THEN** commit-msg hook SHALL 忽略这些注释行
- **THEN** 注释行 SHALL NOT 导致首行格式、多行空行分隔校验失败
### Requirement: hooks-install 安装命令
`make hooks-install` SHALL 将 `scripts/git-hooks/` 下的 hook 脚本安装到 `.git/hooks/`,不覆盖 Git LFS 管理的 hook。
#### Scenario: 安装 pre-commit 和 commit-msg
#### Scenario: 安装所有 hook 脚本
- **WHEN** 执行 `make hooks-install`
- **THEN** `scripts/git-hooks/pre-commit` SHALL 被复制到 `.git/hooks/pre-commit`
- **THEN** `scripts/git-hooks/commit-msg` SHALL 被复制到 `.git/hooks/commit-msg`
- **THEN** 两个文件 SHALL 被设置为可执行(`chmod +x`
- **THEN** `scripts/git-hooks/prepare-commit-msg` SHALL 被复制到 `.git/hooks/prepare-commit-msg`
- **THEN** 所有复制文件 SHALL 被设置为可执行(`chmod +x`
#### Scenario: 不覆盖 LFS 管理的 hook
@@ -113,9 +136,15 @@ commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确
#### Scenario: hooks-check 验证安装状态
- **WHEN** 执行 `make hooks-check`
- **THEN** 命令 SHALL 检查 `.git/hooks/pre-commit``.git/hooks/commit-msg` 是否存在且可执行
- **THEN** 命令 SHALL 检查 `.git/hooks/pre-commit``.git/hooks/commit-msg``.git/hooks/prepare-commit-msg` 是否存在且可执行
- **THEN** SHALL 输出每个 hook 的安装状态
#### Scenario: 安装前验证 source 文件存在
- **WHEN** 执行 `make hooks-install`
- **THEN** 命令 SHALL 在复制前验证每个 source 文件(`scripts/git-hooks/<hook-name>`)是否存在
- **THEN** 若 source 文件不存在,命令 SHALL 报告错误并返回非零退出码
### Requirement: hooks-test 回归测试命令
`make hooks-test` SHALL 运行仓库内 hook 回归测试,覆盖 commit-msg 格式校验和 pre-commit staged-file 检查,不污染真实 git index。
@@ -146,19 +175,19 @@ pre-commit 和 commit-msg hook 脚本 SHALL 可在 macOS 和 WindowsGit Bash
### Requirement: pre-commit 核心逻辑在 Makefile 中复用
pre-commit hook 的检查逻辑 SHALL 通过 Makefile target 调用项目已有工具链,不重复实现 hook 框架逻辑。commit-msg hook SHALL 在脚本内直接完成格式校验
pre-commit hook 的检查逻辑 SHALL 通过 Makefile target 调用项目已有工具链,不重复实现。非代码检查冲突标记、大文件、LFS 指针SHALL 在 `_hooks-pre-commit` 中直接实现;代码检查 SHALL 委托 `_backend-lint``_versionctl-lint``_frontend-check` target
#### Scenario: Go lint 复用后端配置
#### Scenario: Go lint 委托后端 lint target
- **WHEN** pre-commit 需要检查 Go 文件
- **THEN** SHALL 调用 Makefile 逻辑,在 `backend/` 目录对 staged `.go` 文件运行 `go tool golangci-lint run`
- **THEN** SHALL 复用 `backend/.golangci.yml` 中的 lint 配置
- **THEN** SHALL 委托 `_backend-lint``_versionctl-lint` target根据文件路径 `backend/` vs `versionctl/`
- **THEN** SHALL NOT 在 `_hooks-pre-commit` 中内联 `golangci-lint` 命令
#### Scenario: 前端 lint 使用 staged 文件参数
#### Scenario: 前端检查委托前端 check target
- **WHEN** pre-commit 需要检查前端文件
- **THEN** SHALL 调用 Makefile 逻辑,在 `frontend/` 目录对 staged 前端文件运行 ESLint 和 Prettier 的文件参数模式
- **THEN** SHALL NOT 在 pre-commit 阶段运行全量 `bun run check`
- **THEN** SHALL 委托 `_frontend-check` target
- **THEN** SHALL NOT 在 `_hooks-pre-commit` 中内联 `eslint``prettier` 命令
#### Scenario: 终端直接调试

View File

@@ -0,0 +1,57 @@
# prepare-commit-msg-hook
## Purpose
定义 prepare-commit-msg Git hook`git commit` 编辑器打开时为开发者提供提交信息模板。
## Requirements
### Requirement: prepare-commit-msg hook 提供提交信息模板
prepare-commit-msg hook SHALL 在 `git commit` 打开编辑器时,将规范格式的提交信息模板预填充到提交信息文件中,辅助开发者编写符合项目规范的多行提交信息。
#### Scenario: 模板预填充到提交信息文件
- **WHEN** `git commit` 被执行且编辑器打开提交信息文件
- **THEN** prepare-commit-msg hook SHALL 在提交信息文件中写入模板内容
- **THEN** 模板 SHALL 包含注释行(以 `#` 开头)引导开发者填写规范格式
#### Scenario: 模板包含格式引导
- **WHEN** 模板被写入提交信息文件
- **THEN** 模板 SHALL 包含首行格式提示:`# <类型>: <简短中文描述>`
- **THEN** 模板 SHALL 包含空行占位符
- **THEN** 模板 SHALL 包含详细描述区:`# <详细说明>`
- **THEN** 模板 SHALL 列出可用类型:`feat / fix / refactor / docs / style / test / chore`
- **THEN** 模板 SHALL 包含示例:`feat: 添加供应商批量管理功能`
#### Scenario: 注释行不被提交
- **WHEN** 用户在编辑器中基于模板填写提交信息并保存
- **THEN** 以 `#` 开头的模板注释行 SHALL 被 Git 作为注释过滤,不会成为提交信息的一部分
#### Scenario: 已有提交信息时跳过
- **WHEN** 提交信息文件已包含非注释内容(如 `-m` 参数指定、`git commit --amend`、merge commit、cherry-pick
- **THEN** prepare-commit-msg hook SHALL NOT 覆盖已有内容,直接退出
#### Scenario: Git 默认注释不阻止模板写入
- **WHEN** 提交信息文件只包含空行或 Git 默认生成的 `#` 注释行
- **THEN** prepare-commit-msg hook SHALL 将其视为没有已有提交信息
- **THEN** hook SHALL 在文件顶部写入模板,并保留 Git 原有注释内容
### Requirement: 通过 hooks-install 安装
prepare-commit-msg hook SHALL 随 `make hooks-install` 一起安装到 `.git/hooks/`
#### Scenario: 安装 prepare-commit-msg
- **WHEN** 执行 `make hooks-install`
- **THEN** `scripts/git-hooks/prepare-commit-msg` SHALL 被复制到 `.git/hooks/prepare-commit-msg`
- **THEN** 该文件 SHALL 被设置为可执行(`chmod +x`
#### Scenario: hooks-check 验证安装状态
- **WHEN** 执行 `make hooks-check`
- **THEN** 命令 SHALL 检查 `.git/hooks/prepare-commit-msg` 是否存在且可执行

View File

@@ -189,7 +189,8 @@
- `format = "prettier --write ."` — 格式化所有文件
- `format:check = "prettier --check ."` — 检查文件格式
- `check = "bun run lint && bun run format:check"` — 检查 lint 和格式
- `typecheck = "tsc -b"` — TypeScript 类型检查
- `check = "bun run typecheck && bun run lint && bun run format:check"` — 检查类型、lint 和格式
- `fix = "bun run lint:fix && bun run format"` — 修复 lint 问题并格式化
#### Scenario: 运行格式化命令
@@ -207,8 +208,14 @@
#### Scenario: 运行统一检查命令
- **WHEN** 执行 `bun run check`
- **THEN** SHALL 运行 `bun run lint && bun run format:check`
- **THEN** lint 错误和格式问题 SHALL 都被检查
- **THEN** SHALL 依次运行 `bun run typecheck``bun run lint``bun run format:check`
- **THEN** TypeScript 类型错误、lint 错误和格式问题 SHALL 都被检查
#### Scenario: 运行类型检查命令
- **WHEN** 执行 `bun run typecheck`
- **THEN** SHALL 运行 `tsc -b`
- **THEN** TypeScript 类型错误 SHALL 报告错误
#### Scenario: 运行统一修复命令

View File

@@ -8,7 +8,33 @@ if [ ! -f "$MSG_FILE" ]; then
exit 1
fi
IFS= read -r FIRST_LINE < "$MSG_FILE" || FIRST_LINE=
FIRST_LINE=
SECOND_LINE=
HAS_BODY=
LINE_NO=0
while IFS= read -r LINE || [ -n "$LINE" ]; do
case "$LINE" in
\#*) continue ;;
esac
if [ -z "$FIRST_LINE" ]; then
[ -n "$LINE" ] || continue
FIRST_LINE=$LINE
LINE_NO=1
continue
fi
LINE_NO=$((LINE_NO + 1))
case "$LINE_NO" in
2) SECOND_LINE=$LINE ;;
*)
if [ -n "$LINE" ]; then
HAS_BODY=1
fi
;;
esac
done < "$MSG_FILE"
case "$FIRST_LINE" in
Merge*)
@@ -31,12 +57,11 @@ EOF
exit 1
fi
DESCRIPTION=${FIRST_LINE#*: }
if printf '%s\n' "$DESCRIPTION" | LC_ALL=C grep -Eq '^[ -~]+$'; then
printf '%s\n' '提交描述需使用中文。' >&2
exit 1
fi
if [ ${#FIRST_LINE} -gt 72 ]; then
printf '%s\n' '警告: 提交信息首行超过 72 个字符,建议保持简短。' >&2
fi
if [ -n "$HAS_BODY" ] && [ -n "$SECOND_LINE" ]; then
printf '%s\n' '提交信息首行后应为空行,再写详细描述。' >&2
exit 1
fi

View File

@@ -0,0 +1,49 @@
#!/bin/sh
set -e
MSG_FILE=$1
MSG_SOURCE=$2
case "$MSG_SOURCE" in
"") ;;
*) exit 0 ;;
esac
if [ ! -f "$MSG_FILE" ]; then
exit 0
fi
has_content=0
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) ;;
*)
has_content=1
break
;;
esac
done < "$MSG_FILE"
if [ "$has_content" -eq 1 ]; then
exit 0
fi
tmp_file=${MSG_FILE}.nex-template.$$
{
cat <<'EOF'
# <类型>: <简短中文描述>
#
# <详细说明>
#
# 类型: feat / fix / refactor / docs / style / test / chore
# 示例: feat: 添加供应商批量管理功能
EOF
if [ -s "$MSG_FILE" ]; then
printf '\n'
while IFS= read -r line || [ -n "$line" ]; do
printf '%s\n' "$line"
done < "$MSG_FILE"
fi
} > "$tmp_file"
mv "$tmp_file" "$MSG_FILE"

View File

@@ -14,7 +14,9 @@ cleanup() {
frontend/src/hook_format_fixture.ts \
docs/hook-doc-fixture.md \
docs/hook-conflict-fixture.md \
docs/hook-large-fixture.txt
docs/hook-large-fixture.txt \
"$TMP_DIR/lfs-pointer-fixture" \
"$TMP_DIR/lfs-bad-fixture"
rm -rf "$TMP_DIR"
}
@@ -35,6 +37,14 @@ write_msg() {
printf '%s\n' "$*" > "$file"
}
write_conflict() {
file=$1
less7=$(printf '<%.0s' $(seq 7))
eq7=$(printf '=%.0s' $(seq 7))
gt7=$(printf '>%.0s' $(seq 7))
printf '%s\n' "${less7} HEAD" '' "${eq7}" '' "${gt7} branch" > "$file"
}
expect_success() {
name=$1
shift
@@ -66,12 +76,34 @@ run_precommit_for() {
GIT_INDEX_FILE=$index make _hooks-pre-commit
}
run_hooks_install_missing_source() {
install_repo=$TMP_DIR/hooks-install-missing
rm -rf "$install_repo"
mkdir -p "$install_repo/scripts/git-hooks"
cp Makefile "$install_repo/Makefile"
cp scripts/git-hooks/pre-commit "$install_repo/scripts/git-hooks/pre-commit"
cp scripts/git-hooks/commit-msg "$install_repo/scripts/git-hooks/commit-msg"
git -C "$install_repo" init >/dev/null 2>&1
(cd "$install_repo" && make hooks-install)
}
MSG_FILE=$TMP_DIR/commit-msg.txt
# ============================================
# commit-msg 测试
# ============================================
write_msg "$MSG_FILE" 'feat: 添加 hook 测试'
expect_success 'commit-msg accepts Chinese description' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'feat: add hook tests'
expect_failure 'commit-msg rejects English-only description' scripts/git-hooks/commit-msg "$MSG_FILE"
expect_success 'commit-msg accepts English-only description without CJK enforcement' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'fix: 修复 auth 模块 bug'
expect_success 'commit-msg accepts Chinese with English technical terms' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'docs: ajouter une fonctionnalité'
expect_success 'commit-msg accepts non-CJK unicode description without CJK enforcement' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'update: 添加 hook 测试'
expect_failure 'commit-msg rejects invalid type' scripts/git-hooks/commit-msg "$MSG_FILE"
@@ -79,6 +111,81 @@ expect_failure 'commit-msg rejects invalid type' scripts/git-hooks/commit-msg "$
write_msg "$MSG_FILE" 'Merge branch feature'
expect_success 'commit-msg accepts merge commits' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'feat: 添加新功能
'
expect_success 'commit-msg accepts single line with trailing newline' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加新功能\n\n详细描述内容\n' > "$MSG_FILE"
expect_success 'commit-msg accepts multi-line with blank line separator' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加新功能\n缺少空行\n详细描述\n' > "$MSG_FILE"
expect_failure 'commit-msg rejects multi-line without blank line separator' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加新功能\n\n' > "$MSG_FILE"
expect_success 'commit-msg accepts two lines with blank line 2' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加新功能\n非空行\n' > "$MSG_FILE"
expect_success 'commit-msg accepts two lines without body (no line 3)' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加模板测试\n# <类型>: <简短中文描述>\n#\n# <详细说明>\n' > "$MSG_FILE"
expect_success 'commit-msg ignores template comments after subject' scripts/git-hooks/commit-msg "$MSG_FILE"
printf '# <类型>: <简短中文描述>\n#\nfeat: 添加模板测试\n' > "$MSG_FILE"
expect_success 'commit-msg ignores leading template comments' scripts/git-hooks/commit-msg "$MSG_FILE"
printf 'feat: 添加新功能\n缺少空行\n# 模板注释\n详细描述\n' > "$MSG_FILE"
expect_failure 'commit-msg rejects non-blank separator with intervening comments' scripts/git-hooks/commit-msg "$MSG_FILE"
# ============================================
# prepare-commit-msg 测试
# ============================================
prepare_msg_file="$TMP_DIR/prepare-msg.txt"
rm -f "$prepare_msg_file"
touch "$prepare_msg_file"
expect_success 'prepare-commit-msg writes template for empty commit' scripts/git-hooks/prepare-commit-msg "$prepare_msg_file" ""
if grep -q '<类型>' "$prepare_msg_file" && grep -q 'feat / fix / refactor' "$prepare_msg_file"; then
pass 'prepare-commit-msg template contains format guidance'
else
fail 'prepare-commit-msg template contains format guidance'
fi
printf '\n# Please enter the commit message for your changes.\n# On branch main\n' > "$prepare_msg_file"
expect_success 'prepare-commit-msg writes template before git comments' scripts/git-hooks/prepare-commit-msg "$prepare_msg_file" ""
if grep -q '<类型>' "$prepare_msg_file" && grep -q 'Please enter the commit message' "$prepare_msg_file"; then
pass 'prepare-commit-msg preserves git comments after template'
else
fail 'prepare-commit-msg preserves git comments after template'
fi
write_msg "$prepare_msg_file" 'existing content'
expect_success 'prepare-commit-msg skips when file has content' scripts/git-hooks/prepare-commit-msg "$prepare_msg_file" ""
if printf '%s\n' "$(cat "$prepare_msg_file")" | grep -q '^existing content$'; then
pass 'prepare-commit-msg does not overwrite existing content'
else
fail 'prepare-commit-msg does not overwrite existing content'
fi
rm -f "$prepare_msg_file"
touch "$prepare_msg_file"
expect_success 'prepare-commit-msg skips for merge' scripts/git-hooks/prepare-commit-msg "$prepare_msg_file" "merge"
if [ ! -s "$prepare_msg_file" ]; then
pass 'prepare-commit-msg skips template for merge'
else
fail 'prepare-commit-msg skips template for merge'
fi
# ============================================
# hooks-install 测试
# ============================================
expect_failure 'hooks-install rejects missing source hook' run_hooks_install_missing_source
# ============================================
# pre-commit 测试
# ============================================
cat > backend/pkg/buildinfo/hook_bad_test_fixture.go <<'EOF'
package buildinfo
@@ -88,20 +195,20 @@ func hookBadTestFixture() {
fmt.Println("bad")
}
EOF
expect_failure 'pre-commit rejects Go lint errors' run_precommit_for backend/pkg/buildinfo/hook_bad_test_fixture.go
expect_failure 'pre-commit rejects Go lint errors (delegated to _backend-lint)' run_precommit_for backend/pkg/buildinfo/hook_bad_test_fixture.go
rm -f backend/pkg/buildinfo/hook_bad_test_fixture.go
cat > frontend/src/hook_bad_fixture.ts <<'EOF'
console.log('bad')
EOF
expect_failure 'pre-commit rejects frontend lint errors' run_precommit_for frontend/src/hook_bad_fixture.ts
expect_failure 'pre-commit rejects frontend lint errors (delegated to _frontend-check)' run_precommit_for frontend/src/hook_bad_fixture.ts
rm -f frontend/src/hook_bad_fixture.ts
cat > frontend/src/hook_format_fixture.ts <<'EOF'
const hookFormatFixture={foo:"bar"}
export { hookFormatFixture }
EOF
expect_failure 'pre-commit rejects frontend format errors' run_precommit_for frontend/src/hook_format_fixture.ts
expect_failure 'pre-commit rejects frontend format errors (delegated to _frontend-check)' run_precommit_for frontend/src/hook_format_fixture.ts
rm -f frontend/src/hook_format_fixture.ts
cat > docs/hook-doc-fixture.md <<'EOF'
@@ -110,16 +217,19 @@ EOF
expect_success 'pre-commit skips non-code staged files' run_precommit_for docs/hook-doc-fixture.md
rm -f docs/hook-doc-fixture.md
cat > docs/hook-conflict-fixture.md <<'EOF'
<<<<<<< HEAD
conflict
=======
other
>>>>>>> branch
EOF
write_conflict docs/hook-conflict-fixture.md
expect_failure 'pre-commit rejects conflict markers' run_precommit_for docs/hook-conflict-fixture.md
rm -f docs/hook-conflict-fixture.md
index=$TMP_DIR/index
rm -f "$index"
GIT_INDEX_FILE=$index git read-tree HEAD
write_conflict "$TMP_DIR/hook-conflict-fixture.sh"
hash=$(git hash-object -w "$TMP_DIR/hook-conflict-fixture.sh")
rm -f "$TMP_DIR/hook-conflict-fixture.sh"
GIT_INDEX_FILE=$index git update-index --add --cacheinfo 100644 "$hash" "scripts/git-hooks/hook-conflict-fixture.sh"
expect_failure 'pre-commit rejects conflict markers in hook scripts' env GIT_INDEX_FILE=$index make _hooks-pre-commit
i=0
while [ "$i" -lt 40000 ]; do
printf 'large hook fixture line\n'
@@ -132,3 +242,32 @@ else
fail 'pre-commit warns for large text files'
fi
rm -f docs/hook-large-fixture.txt
# LFS pointer 校验
lfs_pointer='version https://git-lfs.github.com/spec/v1
oid sha256:abc123
size 100
'
printf '%s\n' "$lfs_pointer" > "$TMP_DIR/lfs-pointer-fixture"
hash=$(git hash-object -w "$TMP_DIR/lfs-pointer-fixture")
index=$TMP_DIR/index
rm -f "$index"
GIT_INDEX_FILE=$index git read-tree HEAD
GIT_INDEX_FILE=$index git update-index --add --cacheinfo 100644 "$hash" "assets/test-lfs-fixture.png"
if GIT_INDEX_FILE=$index make _hooks-pre-commit > "$TMP_DIR/out" 2>&1; then
pass 'pre-commit allows LFS pointer files'
else
cat "$TMP_DIR/out" >&2
fail 'pre-commit allows LFS pointer files'
fi
printf 'fake binary content\n' > "$TMP_DIR/lfs-bad-fixture"
hash=$(git hash-object -w "$TMP_DIR/lfs-bad-fixture")
rm -f "$index"
GIT_INDEX_FILE=$index git read-tree HEAD
GIT_INDEX_FILE=$index git update-index --add --cacheinfo 100644 "$hash" "assets/test-lfs-bad-fixture.png"
if GIT_INDEX_FILE=$index make _hooks-pre-commit > "$TMP_DIR/out" 2>&1; then
cat "$TMP_DIR/out" >&2
fail 'pre-commit rejects non-pointer LFS files'
fi
pass 'pre-commit rejects non-pointer LFS files'