#!/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 \ "$TMP_DIR/lfs-pointer-fixture" \ "$TMP_DIR/lfs-bad-fixture" 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" } 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 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 } 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_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" 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 import "fmt" func hookBadTestFixture() { fmt.Println("bad") } EOF 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 (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 (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' 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 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' 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 # 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'