diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..0679578 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# .githooks/commit-msg — enforces conventional commit format +# Format: type: description OR type(scope): description +set -euo pipefail + +RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m' + +VALID_TYPES="feat|fix|docs|chore|test|refactor|perf|ci|build" +MSG_FILE="$1" +MSG=$(head -1 "$MSG_FILE") + +if [[ -z "${MSG// }" ]]; then + echo -e "${RED}Commit rejected:${NC} Commit message is empty." + exit 1 +fi + +if ! echo "$MSG" | grep -qE "^($VALID_TYPES)(\(.+\))?: .+"; then + echo -e "${RED}Commit rejected:${NC} Message does not follow conventional commit format." + echo "" + echo -e " Required: ${YELLOW}type: description${NC} or ${YELLOW}type(scope): description${NC}" + echo -e " Valid types: ${YELLOW}$VALID_TYPES${NC}" + echo "" + echo -e " Your message: ${YELLOW}$MSG${NC}" + echo "" + echo -e " Examples:" + echo -e " ${YELLOW}feat: add cover letter refinement${NC}" + echo -e " ${YELLOW}fix(wizard): handle missing user.yaml gracefully${NC}" + echo -e " ${YELLOW}docs: update tier system reference${NC}" + exit 1 +fi + +exit 0 diff --git a/tests/test_hooks.sh b/tests/test_hooks.sh index b48358c..7683fce 100755 --- a/tests/test_hooks.sh +++ b/tests/test_hooks.sh @@ -48,3 +48,30 @@ run_hook_with "app/app.py" "import streamlit" && pass "allowed safe file" || \ fail "should have allowed safe file" echo "All pre-commit hook tests passed." + +# ── commit-msg hook tests ──────────────────────────────────────────────────── +COMMIT_HOOK=".githooks/commit-msg" +tmpfile=$(mktemp) + +echo "Test 5: accepts valid feat message" +echo "feat: add thing" > "$tmpfile" +bash "$COMMIT_HOOK" "$tmpfile" && pass "accepted feat" || fail "rejected valid feat" + +echo "Test 6: accepts valid fix with scope" +echo "fix(auth): handle token expiry" > "$tmpfile" +bash "$COMMIT_HOOK" "$tmpfile" && pass "accepted fix(scope)" || fail "rejected valid fix(scope)" + +echo "Test 7: rejects empty message" +echo "" > "$tmpfile" +bash "$COMMIT_HOOK" "$tmpfile" && fail "should reject empty" || pass "rejected empty" + +echo "Test 8: rejects non-conventional message" +echo "updated the thing" > "$tmpfile" +bash "$COMMIT_HOOK" "$tmpfile" && fail "should reject non-conventional" || pass "rejected non-conventional" + +echo "Test 9: rejects invalid type" +echo "yolo: ship it" > "$tmpfile" +bash "$COMMIT_HOOK" "$tmpfile" && fail "should reject invalid type" || pass "rejected invalid type" + +rm -f "$tmpfile" +echo "All commit-msg hook tests passed."