diff --git a/Makefile b/Makefile index 088be01..f25b7dd 100644 --- a/Makefile +++ b/Makefile @@ -75,10 +75,12 @@ help: @echo " check - Run tests and lint" @echo " install - Build and install to Go bin directory" @echo " calibrate-providers - Compare local Claude/Codex session usage for calibration" - @echo " install-hooks - Install git pre-commit hook" + @echo " install-hooks - Install git pre-commit and commit-msg hooks" @echo " help - Show this help" -# Install git pre-commit hook +# Install git hooks install-hooks: @ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit + @ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg @echo "✓ pre-commit hook installed (.git/hooks/pre-commit → scripts/pre-commit.sh)" + @echo "✓ commit-msg hook installed (.git/hooks/commit-msg → scripts/commit-msg.sh)" diff --git a/README.md b/README.md index 84f92cd..0dae559 100644 --- a/README.md +++ b/README.md @@ -258,19 +258,37 @@ Each task has a default cooldown interval to prevent the same task from running ## Development -### Pre-commit hooks +### Git hooks -Install the git pre-commit hook to catch formatting and vet issues before pushing: +Install the local git hooks before pushing: ```bash make install-hooks ``` -This symlinks `scripts/pre-commit.sh` into `.git/hooks/pre-commit`. The hook runs: +This symlinks: +- `scripts/pre-commit.sh` into `.git/hooks/pre-commit` +- `scripts/commit-msg.sh` into `.git/hooks/commit-msg` + +The pre-commit hook runs: - **gofmt** — flags any staged `.go` files that need formatting - **go vet** — catches common correctness issues - **go build** — ensures the project compiles +The commit-message hook expects a normalized subject line: +- `type: summary` +- `type(scope): summary` + +Examples: +- `fix: restore commit message hook` +- `feat(tasks): add branch metadata` +- `docs: describe local git hooks` + +Allowed exceptions: +- Git-generated subjects such as `Merge ...` and `Revert ...` +- Autosquash subjects such as `fixup! ...` and `squash! ...` +- Nightshift trailers such as `Nightshift-Task: ...` and `Nightshift-Ref: ...` + To bypass in a pinch: `git commit --no-verify` ## Uninstalling diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 0000000..44d5ef7 --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# commit-msg hook for nightshift +# Install: make install-hooks +set -euo pipefail + +msg_file=${1:-} + +if [[ -z "$msg_file" || ! -f "$msg_file" ]]; then + echo "commit-msg: missing commit message file" >&2 + exit 1 +fi + +normalize_line() { + local line=$1 + line=${line%$'\r'} + printf '%s' "$line" +} + +subject= +while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do + line=$(normalize_line "$raw_line") + [[ -z "$line" ]] && continue + [[ $line == \#* ]] && continue + subject=$line + break +done <"$msg_file" + +if [[ -z "$subject" ]]; then + cat >&2 <<'EOF' +commit-msg: empty commit message +Use: type: summary +EOF + exit 1 +fi + +case "$subject" in + Merge\ *|Revert\ *|fixup!\ *|squash!\ *) + exit 0 + ;; +esac + +pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|style|test)(\([a-z0-9#][a-z0-9._/#-]*\))?(!)?: [^[:space:]](.*[^[:space:]])?$' + +if [[ $subject =~ $pattern ]]; then + exit 0 +fi + +cat >&2 <