From a2751eab311e32c3a9bba671ca4ef4e5d90539a7 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 5 May 2026 21:58:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=9F=E7=94=9F=20Git=20hooks=20?= =?UTF-8?q?=E6=96=B9=E6=A1=88=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8D=87=E8=BF=81=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 101 +++++++++++++++-- README.md | 15 ++- lefthook.yml | 5 - openspec/specs/git-hooks/spec.md | 167 ++++++++++++++++++++++++++++ openspec/specs/version-bump/spec.md | 18 ++- scripts/git-hooks/commit-msg | 42 +++++++ scripts/git-hooks/pre-commit | 12 ++ scripts/git-hooks/test-hooks.sh | 134 ++++++++++++++++++++++ 8 files changed, 476 insertions(+), 18 deletions(-) delete mode 100644 lefthook.yml create mode 100644 openspec/specs/git-hooks/spec.md create mode 100755 scripts/git-hooks/commit-msg create mode 100755 scripts/git-hooks/pre-commit create mode 100755 scripts/git-hooks/test-hooks.sh diff --git a/Makefile b/Makefile index bf8a6d9..3ef5fd7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: \ - lint test clean \ + lint test clean hooks-install hooks-check hooks-test \ version-sync version-check version-bump \ server-run server-build server-lint server-test server-clean \ desktop-build-mac desktop-build-win desktop-build-linux \ @@ -10,6 +10,7 @@ _backend-lint _backend-test _backend-clean _backend-build \ _versionctl-lint _versionctl-test \ _frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \ + _hooks-pre-commit _check-clean-worktree \ _desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \ _server-run-backend _server-run-frontend \ _check-linux-target-arch _check-windows-target-arch _ensure-appimagetool \ @@ -62,6 +63,82 @@ test: _backend-test _frontend-test _desktop-test _versionctl-test clean: _backend-clean _frontend-clean _desktop-clean @printf 'Clean complete\n' +# ============================================ +# Git hooks +# ============================================ + +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"; \ + 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 \ + if [ -x "$$hooks_dir/$$hook" ]; then \ + printf 'OK: %s\n' "$$hook"; \ + else \ + printf 'MISSING: %s (%s/%s)\n' "$$hook" "$$hooks_dir" "$$hook"; \ + status=1; \ + fi; \ + done; \ + exit $$status + +hooks-test: + @scripts/git-hooks/test-hooks.sh + +_hooks-pre-commit: + @set -e; \ + 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; \ + printf '%s\n' "$$staged_files" | while IFS= read -r file; 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; \ + exit 1; \ + fi; \ + size=$$(git cat-file -s ":$$file" 2>/dev/null || printf '0'); \ + if [ "$$size" -gt 512000 ] 2>/dev/null; then \ + if git show ":$$file" 2>/dev/null | LC_ALL=C grep -Iq .; then \ + printf 'Warning: large staged text file (%s bytes): %s\n' "$$size" "$$file" >&2; \ + fi; \ + fi; \ + case "$$file" in \ + backend/*.go) \ + rel=$${file#backend/}; \ + printf 'Go lint: backend/%s\n' "$$rel"; \ + (cd backend && go tool golangci-lint run "$$rel"); \ + ;; \ + versionctl/*.go) \ + rel=$${file#versionctl/}; \ + printf 'Go lint: versionctl/%s\n' "$$rel"; \ + (cd versionctl && go tool golangci-lint run "$$rel"); \ + ;; \ + 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"); \ + ;; \ + esac; \ + done; \ + printf 'Pre-commit checks passed\n' + # ============================================ # 版本管理 # ============================================ @@ -73,13 +150,21 @@ version-check: go run ./versionctl check version-bump: BUMP ?= patch -version-bump: - $(eval _BUMP_ARG := $(if $(SET_VERSION),$(SET_VERSION),$(BUMP))) - $(eval _NEW_VERSION := $(shell go run ./versionctl bump $(_BUMP_ARG))) - git add VERSION frontend/ - git commit -m "chore: 版本升迁 v$(_NEW_VERSION)" - git tag "v$(_NEW_VERSION)" - @printf '版本升迁完成: v%s\n' "$(_NEW_VERSION)" +version-bump: lint test _check-clean-worktree + @set -e; \ + bump_arg="$(if $(SET_VERSION),$(SET_VERSION),$(BUMP))"; \ + new_version=$$(go run ./versionctl bump "$$bump_arg"); \ + git add VERSION frontend/; \ + git commit -m "chore: 版本升迁 v$$new_version"; \ + git tag "v$$new_version"; \ + printf '版本升迁完成: v%s\n' "$$new_version" + +_check-clean-worktree: + @if [ -n "$$(git status --porcelain)" ]; then \ + printf '工作区不干净,请先提交或清理改动后再执行版本升迁。\n' >&2; \ + git status --short; \ + exit 1; \ + fi # ============================================ # Server 模式 diff --git a/README.md b/README.md index d1e51b5..bdb8b6b 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,13 @@ backend 分类测试、MySQL 专项测试和前端 E2E 测试请分别查看 `ba ```bash # 首次克隆后安装 Git hooks -lefthook install +make hooks-install + +# 检查 Git hooks 安装状态 +make hooks-check + +# 运行 Git hooks 回归测试 +make hooks-test # 全局命令 make lint # 前后端共享检查 @@ -351,6 +357,11 @@ make desktop-test # desktop 专属测试 make desktop-clean # 清理 desktop 产物 ``` +Git hooks 使用仓库内 `scripts/git-hooks/` 的原生脚本,不依赖额外 hook 框架。当前 hooks 包含: + +- pre-commit:检查 staged files 的冲突标记、Go lint、前端 lint/格式和大文件告警 +- commit-msg:校验提交信息格式为 `类型: 简短描述`,描述需使用中文 + ## 版本与发布 ### 统一版本源 @@ -361,7 +372,7 @@ make desktop-clean # 清理 desktop 产物 ### 本地版本演进 ```bash -# 递增版本(自动 sync + check + commit + tag) +# 递增版本(自动 lint + test + 工作区检查 + sync/check + commit + tag) make version-bump BUMP=minor # 或指定具体版本号 diff --git a/lefthook.yml b/lefthook.yml deleted file mode 100644 index 491189e..0000000 --- a/lefthook.yml +++ /dev/null @@ -1,5 +0,0 @@ -pre-commit: - commands: - backend-lint: - glob: "backend/**/*.go" - run: cd backend && go tool golangci-lint run --new-from-rev HEAD ./... diff --git a/openspec/specs/git-hooks/spec.md b/openspec/specs/git-hooks/spec.md new file mode 100644 index 0000000..e10f34d --- /dev/null +++ b/openspec/specs/git-hooks/spec.md @@ -0,0 +1,167 @@ +# git-hooks + +## Purpose + +定义仓库原生 Git hooks 的安装、校验、测试与跨平台执行规则,确保提交前快速检查和提交信息格式校验符合项目规范。 + +## Requirements + +### Requirement: pre-commit hook 快速检查 + +pre-commit hook SHALL 在 `git commit` 执行前对 staged files 进行快速检查,仅检查本次提交涉及的文件。 + +#### Scenario: 无 Go 和前端文件变更时跳过 + +- **WHEN** staged files 中既无 `.go` 文件也无 `.ts`/`.tsx`/`.scss` 文件 +- **THEN** pre-commit hook SHALL 直接通过,不运行任何 linter + +#### Scenario: 冲突标记检测 + +- **WHEN** 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: commit 被阻止时显示修复提示 + +- **WHEN** pre-commit hook 检查失败 +- **THEN** hook SHALL 输出明确的修复提示(如 `bun run fix`、手动解决冲突标记等) + +### Requirement: commit-msg hook 校验提交信息格式 + +commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确保符合项目规范。提交描述 SHALL 使用中文;版本号、英文专有名词可与中文描述混用。 + +#### Scenario: 合法格式通过 + +- **WHEN** 提交信息首行格式为 `<类型>: <描述>`,类型为 `feat`、`fix`、`refactor`、`docs`、`style`、`test`、`chore` 之一 +- **THEN** commit-msg hook SHALL 通过,commit 正常执行 + +#### Scenario: 非法类型被拒绝 + +- **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` +- **THEN** commit-msg hook SHALL 报告格式错误,commit SHALL 被阻止 + +#### Scenario: 首行过长告警 + +- **WHEN** 提交信息首行超过 72 个字符 +- **THEN** commit-msg hook SHALL 输出警告(不阻止提交),提示首行应简短 + +#### Scenario: Merge commit 自动放行 + +- **WHEN** 提交信息首行以 `Merge` 开头 +- **THEN** commit-msg hook SHALL 直接通过,不进行格式校验 + +#### Scenario: 格式错误时显示示例 + +- **WHEN** commit-msg hook 检查失败 +- **THEN** hook SHALL 输出包含正确格式示例的错误信息(如 `feat: 添加供应商批量管理功能`) + +### Requirement: hooks-install 安装命令 + +`make hooks-install` SHALL 将 `scripts/git-hooks/` 下的 hook 脚本安装到 `.git/hooks/`,不覆盖 Git LFS 管理的 hook。 + +#### Scenario: 安装 pre-commit 和 commit-msg + +- **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`) + +#### Scenario: 不覆盖 LFS 管理的 hook + +- **WHEN** `.git/hooks/post-checkout`、`.git/hooks/post-commit`、`.git/hooks/post-merge`、`.git/hooks/pre-push` 已由 Git LFS 管理 +- **THEN** `make hooks-install` SHALL NOT 覆盖或修改这些文件 + +#### Scenario: 重复安装幂等 + +- **WHEN** `make hooks-install` 被执行多次 +- **THEN** hook 文件 SHALL 被正确覆盖更新,不会产生重复或损坏 + +#### Scenario: hooks-check 验证安装状态 + +- **WHEN** 执行 `make hooks-check` +- **THEN** 命令 SHALL 检查 `.git/hooks/pre-commit` 和 `.git/hooks/commit-msg` 是否存在且可执行 +- **THEN** SHALL 输出每个 hook 的安装状态 + +### Requirement: hooks-test 回归测试命令 + +`make hooks-test` SHALL 运行仓库内 hook 回归测试,覆盖 commit-msg 格式校验和 pre-commit staged-file 检查,不污染真实 git index。 + +#### Scenario: 运行 hook 回归测试 + +- **WHEN** 执行 `make hooks-test` +- **THEN** SHALL 运行 `scripts/git-hooks/test-hooks.sh` +- **THEN** 测试 SHALL 使用临时 `GIT_INDEX_FILE` 构造 staged fixture +- **THEN** 若任一 hook 行为不符合预期,命令 SHALL 返回非零退出码 + +### Requirement: 跨平台可用 + +pre-commit 和 commit-msg hook 脚本 SHALL 可在 macOS 和 Windows(Git Bash)上正常执行。 + +#### Scenario: macOS 上正常执行 + +- **WHEN** hook 脚本在 macOS 上被 git 调用 +- **THEN** `#!/bin/sh` shebang SHALL 被系统正确解析 +- **THEN** `exec make` SHALL 正确调用 Makefile target + +#### Scenario: Windows Git Bash 上正常执行 + +- **WHEN** hook 脚本在 Windows 的 Git Bash 环境中被 git 调用 +- **THEN** Git for Windows 自带的 sh.exe SHALL 正确解析 `#!/bin/sh` +- **THEN** `exec make` SHALL 正确调用 Makefile target(依赖 Git Bash/MINGW64 环境中 `make` 可用) +- **THEN** Go 和 Bun 工具链 SHALL 通过 PATH 可被 Makefile 调用 + +### Requirement: pre-commit 核心逻辑在 Makefile 中复用 + +pre-commit hook 的检查逻辑 SHALL 通过 Makefile target 调用项目已有工具链,不重复实现 hook 框架逻辑。commit-msg hook SHALL 在脚本内直接完成格式校验。 + +#### Scenario: Go lint 复用后端配置 + +- **WHEN** pre-commit 需要检查 Go 文件 +- **THEN** SHALL 调用 Makefile 逻辑,在 `backend/` 目录对 staged `.go` 文件运行 `go tool golangci-lint run` +- **THEN** SHALL 复用 `backend/.golangci.yml` 中的 lint 配置 + +#### Scenario: 前端 lint 使用 staged 文件参数 + +- **WHEN** pre-commit 需要检查前端文件 +- **THEN** SHALL 调用 Makefile 逻辑,在 `frontend/` 目录对 staged 前端文件运行 ESLint 和 Prettier 的文件参数模式 +- **THEN** SHALL NOT 在 pre-commit 阶段运行全量 `bun run check` + +#### Scenario: 终端直接调试 + +- **WHEN** 开发者执行 `make _hooks-pre-commit` +- **THEN** SHALL 执行与 pre-commit hook 完全相同的检查逻辑 +- **THEN** 输出 SHALL 与 hook 触发时一致 diff --git a/openspec/specs/version-bump/spec.md b/openspec/specs/version-bump/spec.md index 5aca011..0129a7d 100644 --- a/openspec/specs/version-bump/spec.md +++ b/openspec/specs/version-bump/spec.md @@ -90,22 +90,34 @@ ### Requirement: 版本升迁 Makefile 编排 -`make version-bump` SHALL 编排完整的版本升迁流程:工作区干净检查 → `version bump`(含 sync/check/倒退检查)→ git add → git commit → git tag。不传 `BUMP` 参数时 SHALL 默认执行 `BUMP=patch`。 +`make version-bump` SHALL 编排完整的版本升迁流程:全量 lint 检查 → 全量单元测试 → 工作区干净检查 → `version bump`(含 sync/check/倒退检查)→ git add → git commit → git tag。不传 `BUMP` 参数时 SHALL 默认执行 `BUMP=patch`。lint/test 前置检查 SHALL NOT 替代工作区干净检查。 #### Scenario: 完整升迁流程 - **WHEN** 执行 `make version-bump BUMP=minor`,工作区干净,当前版本 `0.1.0` -- **THEN** Makefile SHALL 依次执行:工作区检查 → `version bump minor` → `git add VERSION frontend/` → `git commit -m "chore: 版本升迁 v0.2.0"` → `git tag v0.2.0` +- **THEN** Makefile SHALL 依次执行:`make lint` → `make test` → 工作区检查 → `version bump minor` → `git add VERSION frontend/` → `git commit -m "chore: 版本升迁 v0.2.0"` → `git tag v0.2.0` #### Scenario: 不传 BUMP 默认 patch - **WHEN** 执行 `make version-bump`,工作区干净,当前版本 `0.1.0` - **THEN** Makefile SHALL 等效于执行 `make version-bump BUMP=patch`,将版本更新为 `0.1.1` +#### Scenario: lint 失败时终止 + +- **WHEN** 执行 `make version-bump`,但 `make lint` 报告错误 +- **THEN** Makefile SHALL 以非零退出码失败,SHALL NOT 执行 `version bump`、git commit、git tag +- **THEN** SHALL 输出错误信息提示修复 lint 问题后重试 + +#### Scenario: test 失败时终止 + +- **WHEN** 执行 `make version-bump`,但 `make test` 报告测试失败 +- **THEN** Makefile SHALL 以非零退出码失败,SHALL NOT 执行 `version bump`、git commit、git tag +- **THEN** SHALL 输出错误信息提示修复测试失败后重试 + #### Scenario: 工作区不干净 - **WHEN** 执行 `make version-bump BUMP=minor`,但工作区有未提交的改动 -- **THEN** Makefile SHALL 以非零退出码失败并提示先提交或暂存改动 +- **THEN** Makefile SHALL 以非零退出码失败并提示先提交或清理改动 #### Scenario: 支持指定版本号 diff --git a/scripts/git-hooks/commit-msg b/scripts/git-hooks/commit-msg new file mode 100755 index 0000000..99682ce --- /dev/null +++ b/scripts/git-hooks/commit-msg @@ -0,0 +1,42 @@ +#!/bin/sh +set -e + +MSG_FILE=$1 + +if [ ! -f "$MSG_FILE" ]; then + printf '%s\n' '提交信息文件不存在。' >&2 + exit 1 +fi + +IFS= read -r FIRST_LINE < "$MSG_FILE" || FIRST_LINE= + +case "$FIRST_LINE" in + Merge*) + exit 0 + ;; +esac + +if ! printf '%s\n' "$FIRST_LINE" | grep -Eq '^(feat|fix|refactor|docs|style|test|chore): .+$'; then + cat >&2 <<'EOF' +提交信息格式错误。 + +格式: <类型>: <简短描述> +类型: feat / fix / refactor / docs / style / test / chore + +示例: + feat: 添加供应商批量管理功能 + fix: 修复流式响应断连问题 + chore: 版本升迁 v0.2.0 +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 diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 0000000..4728cf7 --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +ROOT_DIR=$(git rev-parse --show-toplevel) +cd "$ROOT_DIR" + +command -v make >/dev/null 2>&1 || { + printf '%s\n' '缺少 make 命令,请先安装 Make 或使用 Git Bash/MINGW64 环境。' >&2 + exit 1 +} + +exec make _hooks-pre-commit diff --git a/scripts/git-hooks/test-hooks.sh b/scripts/git-hooks/test-hooks.sh new file mode 100755 index 0000000..0521e9f --- /dev/null +++ b/scripts/git-hooks/test-hooks.sh @@ -0,0 +1,134 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(git rev-parse --show-toplevel) +cd "$ROOT_DIR" + +TMP_DIR=${TMPDIR:-/tmp}/nex-hooks-test.$$ +mkdir -p "$TMP_DIR" + +cleanup() { + rm -f \ + backend/pkg/buildinfo/hook_bad_test_fixture.go \ + frontend/src/hook_bad_fixture.ts \ + frontend/src/hook_format_fixture.ts \ + docs/hook-doc-fixture.md \ + docs/hook-conflict-fixture.md \ + docs/hook-large-fixture.txt + rm -rf "$TMP_DIR" +} + +trap cleanup EXIT HUP INT TERM + +pass() { + printf 'OK: %s\n' "$1" +} + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +write_msg() { + file=$1 + shift + printf '%s\n' "$*" > "$file" +} + +expect_success() { + name=$1 + shift + if "$@" > "$TMP_DIR/out" 2>&1; then + pass "$name" + else + cat "$TMP_DIR/out" >&2 + fail "$name" + fi +} + +expect_failure() { + name=$1 + shift + if "$@" > "$TMP_DIR/out" 2>&1; then + cat "$TMP_DIR/out" >&2 + fail "$name" + fi + pass "$name" +} + +run_precommit_for() { + index=$TMP_DIR/index + rm -f "$index" + GIT_INDEX_FILE=$index git read-tree HEAD + for file in "$@"; do + GIT_INDEX_FILE=$index git add -f "$file" + done + GIT_INDEX_FILE=$index make _hooks-pre-commit +} + +MSG_FILE=$TMP_DIR/commit-msg.txt +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" + +write_msg "$MSG_FILE" 'update: 添加 hook 测试' +expect_failure 'commit-msg rejects invalid type' scripts/git-hooks/commit-msg "$MSG_FILE" + +write_msg "$MSG_FILE" 'Merge branch feature' +expect_success 'commit-msg accepts merge commits' scripts/git-hooks/commit-msg "$MSG_FILE" + +cat > backend/pkg/buildinfo/hook_bad_test_fixture.go <<'EOF' +package buildinfo + +import "fmt" + +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 +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 +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 +rm -f frontend/src/hook_format_fixture.ts + +cat > docs/hook-doc-fixture.md <<'EOF' +hook doc fixture +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 +expect_failure 'pre-commit rejects conflict markers' run_precommit_for docs/hook-conflict-fixture.md +rm -f docs/hook-conflict-fixture.md + +i=0 +while [ "$i" -lt 40000 ]; do + printf 'large hook fixture line\n' + i=$((i + 1)) +done > docs/hook-large-fixture.txt +if run_precommit_for docs/hook-large-fixture.txt > "$TMP_DIR/out" 2>&1 && grep -q 'Warning: large staged text file' "$TMP_DIR/out"; then + pass 'pre-commit warns for large text files' +else + cat "$TMP_DIR/out" >&2 + fail 'pre-commit warns for large text files' +fi +rm -f docs/hook-large-fixture.txt