From 6e6bb5a04d294f33191273dc03279bbf48ee4751 Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Sun, 19 Jul 2026 02:05:16 -0700 Subject: [PATCH] chore: normalize commit messages Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- Makefile | 13 +++++++-- README.md | 39 ++++++++++++++++++++++--- scripts/commit-msg.sh | 50 +++++++++++++++++++++++++++++++ scripts/commit-msg_test.sh | 60 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 7 deletions(-) create mode 100755 scripts/commit-msg.sh create mode 100755 scripts/commit-msg_test.sh diff --git a/Makefile b/Makefile index 088be01..d1d48c0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test test-verbose test-race coverage coverage-html lint clean deps check install calibrate-providers install-hooks help +.PHONY: build test test-verbose test-race coverage coverage-html lint clean deps check install calibrate-providers install-hooks test-commit-msg help # Binary name BINARY=nightshift @@ -75,10 +75,17 @@ 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 " test-commit-msg - Test the commit message hook" @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)" + +# Test the commit message hook +test-commit-msg: + @./scripts/commit-msg_test.sh diff --git a/README.md b/README.md index 84f92cd..fbb9115 100644 --- a/README.md +++ b/README.md @@ -258,20 +258,51 @@ 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 optional local hooks to catch code and commit-message issues before pushing: ```bash make install-hooks ``` -This symlinks `scripts/pre-commit.sh` into `.git/hooks/pre-commit`. The hook runs: +This installs symlinks for `scripts/pre-commit.sh` and `scripts/commit-msg.sh` in +`.git/hooks`. 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 -To bypass in a pinch: `git commit --no-verify` +The commit-msg hook requires a Conventional Commit-style first meaningful line: + +```text +type: summary +type(scope): summary +``` + +Use one of these lowercase types: `feat`, `fix`, `docs`, `style`, `refactor`, +`perf`, `test`, `build`, `ci`, or `chore`. Scopes are optional and use lowercase +letters, digits, `.`, `_`, `/`, or `-`. Add `!` before the colon for a breaking +change: + +```text +feat: add task filtering +fix(scheduler): handle empty queues +refactor(config)!: remove legacy keys +``` + +Blank lines and Git comment lines before the subject are ignored. Git-generated +merge and revert subjects, plus `fixup!` and `squash!` subjects, are exempt. The +hook only validates the subject, so commit bodies and required trailers remain +unchanged: + +```text +Nightshift-Task: task-id +Nightshift-Ref: https://github.com/marcus/nightshift +``` + +Run the hook tests with `make test-commit-msg`. To bypass either hook in a pinch, +use `git commit --no-verify`. ## Uninstalling diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 0000000..ee42e2d --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# commit-msg hook for Nightshift +# Install: make install-hooks +set -euo pipefail + +MESSAGE_FILE=${1:-} +ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore" + +fail() { + local subject=${1:-""} + + cat >&2 <" +fi + +subject="" +while IFS= read -r line || [[ -n "$line" ]]; do + # Git may provide a CRLF-formatted message (for example, from an editor). + line=${line%$'\r'} + + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + subject=$line + break +done < "$MESSAGE_FILE" + +[[ -n "$subject" ]] || fail + +merge_re='^Merge[[:space:]].+$' +revert_re='^Revert[[:space:]]".+"$' +autosquash_re='^(fixup|squash)![[:space:]].+$' + +if [[ "$subject" =~ $merge_re || "$subject" =~ $revert_re || "$subject" =~ $autosquash_re ]]; then + exit 0 +fi + +subject_re="^(${ALLOWED_TYPES})(\\([a-z0-9][a-z0-9._/-]*\\))?!?: [^[:space:]](.*[^[:space:]])?$" +[[ "$subject" =~ $subject_re ]] || fail "$subject" diff --git a/scripts/commit-msg_test.sh b/scripts/commit-msg_test.sh new file mode 100755 index 0000000..01c9986 --- /dev/null +++ b/scripts/commit-msg_test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +HOOK="$SCRIPT_DIR/commit-msg.sh" +TEST_DIR=$(mktemp -d) +trap 'rm -rf "$TEST_DIR"' EXIT + +pass=0 +fail=0 + +run_case() { + local expectation=$1 + local name=$2 + local message=$3 + local message_file="$TEST_DIR/message" + + printf '%s' "$message" > "$message_file" + + if "$HOOK" "$message_file" >/dev/null 2>&1; then + actual=valid + else + actual=invalid + fi + + if [[ "$actual" == "$expectation" ]]; then + printf 'ok - %s\n' "$name" + pass=$((pass + 1)) + else + printf 'not ok - %s (expected %s, got %s)\n' "$name" "$expectation" "$actual" + fail=$((fail + 1)) + fi +} + +run_case valid "simple subject" 'feat: add task filtering' +run_case valid "scoped subject" 'fix(scheduler): handle empty queues' +run_case valid "breaking change without scope" 'feat!: replace task configuration' +run_case valid "breaking change with scope" 'refactor(config)!: remove legacy keys' +run_case valid "body and Nightshift trailers" $'docs: explain task selection\n\nMore detail.\n\nNightshift-Task: commit-normalize\nNightshift-Ref: https://github.com/marcus/nightshift\n' +run_case valid "leading comments and blank lines" $'# Please enter a commit message.\n\nchore: refresh generated files\n' +run_case valid "CRLF message" $'test(parser): cover invalid input\r\n\r\nBody.\r\n' +run_case valid "generated merge subject" "Merge branch 'main' into feature" +run_case valid "generated revert subject" 'Revert "feat: add task filtering"' +run_case valid "generated fixup subject" 'fixup! feat: add task filtering' +run_case valid "generated squash subject" 'squash! fix(scheduler): handle empty queues' + +run_case invalid "empty message" '' +run_case invalid "comments-only message" $'# Please enter a commit message.\n# Lines starting with # are ignored.\n' +run_case invalid "unknown type" 'feature: add task filtering' +run_case invalid "uppercase type" 'Feat: add task filtering' +run_case invalid "scope with whitespace" 'fix(task queue): handle empty queues' +run_case invalid "missing summary" 'fix:' +run_case invalid "missing space after colon" 'fix:no separating space' +run_case invalid "extra space after colon" 'fix: too much space' +run_case invalid "leading whitespace" ' fix: leading whitespace' +run_case invalid "trailing whitespace" 'fix: trailing whitespace ' +run_case invalid "malformed breaking marker" 'fix(!): malformed marker' + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[[ "$fail" -eq 0 ]]