From d91db420912d901e1dde6d3bfe42ce6d4f5309ba Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sun, 12 Apr 2026 03:31:19 -0700 Subject: [PATCH] chore(hooks): normalize commit subjects Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- Makefile | 29 +++++++++++++++++++++++++---- README.md | 28 +++++++++++++++++++++++++--- scripts/commit-msg.sh | 41 +++++++++++++++++++++++++++++++++++++++++ scripts/pre-commit.sh | 2 +- 4 files changed, 92 insertions(+), 8 deletions(-) create mode 100755 scripts/commit-msg.sh diff --git a/Makefile b/Makefile index 088be01..6b0b35e 100644 --- a/Makefile +++ b/Makefile @@ -75,10 +75,31 @@ 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 hooks (pre-commit + commit-msg)" @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 - @echo "✓ pre-commit hook installed (.git/hooks/pre-commit → scripts/pre-commit.sh)" + @repo_root="$$(git rev-parse --show-toplevel)"; \ + hooks_dir="$$(git rev-parse --git-path hooks)"; \ + case "$$hooks_dir" in \ + /*) ;; \ + *) hooks_dir="$$repo_root/$$hooks_dir" ;; \ + esac; \ + mkdir -p "$$hooks_dir"; \ + for hook in pre-commit commit-msg; do \ + script="$$repo_root/scripts/$$hook.sh"; \ + hook_path="$$hooks_dir/$$hook"; \ + tmp_path="$$hook_path.tmp"; \ + [ -f "$$script" ] || { echo "missing hook script: $$script" >&2; exit 1; }; \ + { \ + printf '%s\n' '#!/usr/bin/env bash'; \ + printf '%s\n' '# generated by make install-hooks'; \ + printf '%s\n' 'set -euo pipefail'; \ + printf '%s\n' 'repo_root=$$(git rev-parse --show-toplevel)'; \ + printf 'exec "$$repo_root/scripts/%s.sh" "$$@"\n' "$$hook"; \ + } > "$$tmp_path"; \ + chmod +x "$$tmp_path"; \ + mv "$$tmp_path" "$$hook_path"; \ + done; \ + echo "✓ hooks installed ($$hooks_dir/pre-commit, $$hooks_dir/commit-msg)" diff --git a/README.md b/README.md index 84f92cd..c5d166b 100644 --- a/README.md +++ b/README.md @@ -258,18 +258,40 @@ Each task has a default cooldown interval to prevent the same task from running ## Development -### Pre-commit hooks +### Commit messages -Install the git pre-commit hook to catch formatting and vet issues before pushing: +Use this format for new local commits: + +```text +type(scope): summary +``` + +If a scope does not add clarity, `type: summary` is also valid. Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `build`, `chore`, `ci`, `perf`. + +Examples: +- `feat(tasks): add commit message validator` +- `fix(#19): tighten config validation` +- `docs: document git hooks` +- `build(release): bump version to v0.3.5` +- `chore(release): prepare v0.3.5` + +Merge, revert, `fixup!`, and `squash!` commits are exempt. This standard applies to future commits only; existing history stays as-is. + +### Git hooks + +Install the git hooks to catch formatting, vet, build, 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 `pre-commit` and `commit-msg` into Git's active hooks directory as small wrapper scripts. That works with the default `.git/hooks`, custom `core.hooksPath`, and linked worktrees because each hook resolves the current checkout at runtime. + +The hooks run: - **gofmt** — flags any staged `.go` files that need formatting - **go vet** — catches common correctness issues - **go build** — ensures the project compiles +- **commit-msg** — validates the first line of the commit message To bypass in a pinch: `git commit --no-verify` diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 0000000..2f5bc83 --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# commit-msg hook for nightshift +# Install: make install-hooks +set -euo pipefail + +msg_file=${1:?commit message file required} +subject="" + +IFS= read -r subject < "$msg_file" || true +subject=${subject%$'\r'} + +case "$subject" in + Merge\ *|Revert\ *|fixup!\ *|squash!\ *) + exit 0 + ;; +esac + +pattern='^(feat|fix|docs|refactor|test|build|chore|ci|perf)(\([A-Za-z0-9._#/-]+\))?(!)?: [^[:space:]].*$' + +if [[ "$subject" =~ $pattern ]]; then + exit 0 +fi + +if [[ -n "$subject" ]]; then + echo "invalid commit subject: $subject" >&2 +else + echo "invalid commit subject: " >&2 +fi + +cat >&2 <<'EOF' +use: type(scope): summary +or: type: summary +types: feat fix docs refactor test build chore ci perf +examples: + feat(tasks): add commit message validator + docs: document git hooks + build(release): bump version to v0.3.5 +allowed: Merge..., Revert..., fixup! ..., squash! ... +EOF + +exit 1 diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index c597d65..975f717 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # pre-commit hook for nightshift -# Install: make install-hooks (or: ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit) +# Install: make install-hooks set -euo pipefail PASS=0