From 463f0910cc942e6a9ebcf107bce15a9c9e3e1dab Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sun, 5 Apr 2026 03:37:11 -0700 Subject: [PATCH] feat(hooks): normalize commit subjects Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- .github/workflows/release.yml | 2 +- CLAUDE.md | 7 +- Makefile | 19 ++- README.md | 8 +- docs/guides/releasing-new-version.md | 6 +- scripts/commit-msg.sh | 208 +++++++++++++++++++++++++++ scripts/loop-prompt.md | 2 +- scripts/pre-commit.sh | 2 +- 8 files changed, 240 insertions(+), 14 deletions(-) create mode 100755 scripts/commit-msg.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 456b816e..be5c8a42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,5 +74,5 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/td.rb git diff --cached --quiet && echo "No changes" && exit 0 - git commit -m "td: bump to ${{ steps.version.outputs.version }}" + git commit -m "chore(homebrew): bump td to ${{ steps.version.outputs.version }}" git push diff --git a/CLAUDE.md b/CLAUDE.md index cd0ab661..a3b35dca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,13 +34,12 @@ go test ./... # Test all ```bash # Commit changes with proper message git add . -git commit -m "feat: description of changes +git commit -m "feat: describe changes (td-) Details here -🤖 Generated with Claude Code - -Co-Authored-By: Claude Haiku 4.5 " +Nightshift-Task: +Nightshift-Ref: https://github.com/marcus/nightshift" # Create version tag (bump from current version, e.g., v0.2.0 → v0.3.0) git tag -a v0.3.0 -m "Release v0.3.0: description" diff --git a/Makefile b/Makefile index 18da511f..8a39529f 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ help: @printf "%s\n" \ "Targets:" \ " make fmt # gofmt -w ." \ - " make install-hooks # install git pre-commit hook" \ + " make install-hooks # install git pre-commit and commit-msg hooks" \ " make test # go test ./..." \ " make install # build and install with version from git" \ " make tag VERSION=vX.Y.Z # create annotated git tag (requires clean tree)" \ @@ -52,6 +52,17 @@ release: tag git push origin "$(VERSION)" install-hooks: - @echo "Installing git pre-commit hook..." - @ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit - @echo "Done. Hook installed at .git/hooks/pre-commit" + @hooks_dir=$$(git rev-parse --git-path hooks); \ + echo "Installing git hooks in $$hooks_dir..."; \ + mkdir -p "$$hooks_dir"; \ + for hook in pre-commit commit-msg; do \ + hook_path="$$hooks_dir/$$hook"; \ + printf '%s\n' \ + '#!/bin/sh' \ + 'set -eu' \ + 'repo_root=$$(git rev-parse --show-toplevel)' \ + "exec \"\$$repo_root/scripts/$$hook.sh\" \"\$$@\"" \ + > "$$hook_path"; \ + chmod +x "$$hook_path"; \ + done; \ + echo "Done. Hooks installed at $$hooks_dir/pre-commit and $$hooks_dir/commit-msg" diff --git a/README.md b/README.md index 684416ad..83861a4f 100644 --- a/README.md +++ b/README.md @@ -189,10 +189,16 @@ make install-dev # Format code make fmt -# Install git pre-commit hook (gofmt, go vet, go build on staged files) +# Install git hooks: +# pre-commit -> gofmt, go vet, go build +# commit-msg -> normalize commit subjects make install-hooks ``` +`make install-hooks` installs wrappers into the repo's resolved git hooks directory, so the same setup works from linked worktrees too. + +Commit subjects are normalized to `type: summary` or `type(scope): summary`, with an optional trailing ` (td-)`. The `commit-msg` hook rewrites obvious cases like `Docs - Update changelog` to `docs: update changelog`, preserves commit bodies and trailers, leaves Git-generated merge/fixup/squash/revert subjects alone, and stops the commit with guidance when the subject cannot be safely interpreted. + ## Tests & Quality Checks ```bash diff --git a/docs/guides/releasing-new-version.md b/docs/guides/releasing-new-version.md index ca98e527..e0f0c5b9 100644 --- a/docs/guides/releasing-new-version.md +++ b/docs/guides/releasing-new-version.md @@ -53,9 +53,11 @@ Add entry at the top of `CHANGELOG.md`: Commit the changelog: ```bash git add CHANGELOG.md -git commit -m "docs: Update changelog for vX.Y.Z" +git commit -m "docs: update changelog for vX.Y.Z" ``` +If you have run `make install-hooks`, the local `commit-msg` hook will normalize obvious subject formatting issues, preserve the commit body and trailers, and reject subjects it cannot safely interpret. Use `type: summary` or `type(scope): summary`, plus an optional trailing ` (td-)` when the release prep is tied to a task. + ### 3. Verify Tests Pass ```bash @@ -137,7 +139,7 @@ go test ./... # Update changelog # (Edit CHANGELOG.md, add entry at top) git add CHANGELOG.md -git commit -m "docs: Update changelog for vX.Y.Z" +git commit -m "docs: update changelog for vX.Y.Z" # Push commits, then tag (tag push triggers automated release) git push origin main diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 00000000..e69bf2b1 --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# commit-msg hook for td +# Install: make install-hooks (recommended; resolves the active hooks path for linked worktrees too) +set -euo pipefail + +message_file=${1:-} + +trim() { + local value=${1-} + value=${value#"${value%%[![:space:]]*}"} + value=${value%"${value##*[![:space:]]}"} + printf '%s' "$value" +} + +normalize_type() { + local raw normalized + + raw=$(trim "${1-}") + raw=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]') + raw=${raw//_/-} + + case "$raw" in + feat|feature|features) + normalized="feat" + ;; + fix|bug|bugfix|bug-fix|hotfix) + normalized="fix" + ;; + docs|doc|documentation) + normalized="docs" + ;; + chore|chores) + normalized="chore" + ;; + build|ci|perf|refactor|revert|style) + normalized="$raw" + ;; + test|tests|testing) + normalized="test" + ;; + *) + return 1 + ;; + esac + + printf '%s' "$normalized" +} + +normalize_scope() { + local raw normalized + + raw=$(trim "${1-}") + raw=${raw#[} + raw=${raw%]} + raw=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]') + normalized=$(printf '%s' "$raw" | sed -E 's/[[:space:]_]+/-/g; s/^-+//; s/-+$//; s/-+/-/g') + + if [[ -z "$normalized" || ! "$normalized" =~ ^[a-z0-9][a-z0-9._/-]*$ ]]; then + return 1 + fi + + printf '%s' "$normalized" +} + +normalize_summary() { + local summary first_char + + summary=$(trim "${1-}") + summary=$(printf '%s' "$summary" | sed -E 's/[[:space:]]+/ /g') + + if [[ "$summary" == *"." && "$summary" != *"..." ]]; then + summary=${summary%.} + fi + + if [[ "$summary" =~ ^([A-Z])([a-z].*)$ ]]; then + first_char=$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]') + summary="${first_char}${BASH_REMATCH[2]}" + fi + + printf '%s' "$summary" +} + +print_error() { + local subject=${1-} + + cat >&2 <&2 + exit 1 +fi + +lines=() +while IFS= read -r line || [[ -n "$line" ]]; do + lines+=("$line") +done < "$message_file" +subject_index=-1 + +for i in "${!lines[@]}"; do + line=${lines[$i]} + if [[ -z $(trim "$line") || "$line" =~ ^# ]]; then + continue + fi + subject_index=$i + break +done + +if (( subject_index < 0 )); then + exit 0 +fi + +original_subject=${lines[$subject_index]} +normalized_subject=$(normalize_subject "$original_subject") + +if [[ "$normalized_subject" != "$(trim "$original_subject")" ]]; then + lines[$subject_index]=$normalized_subject + printf '%s\n' "${lines[@]}" > "$message_file" + echo "commit-msg: normalized subject to '$normalized_subject'" >&2 +fi diff --git a/scripts/loop-prompt.md b/scripts/loop-prompt.md index 7f84db2e..f9883b09 100644 --- a/scripts/loop-prompt.md +++ b/scripts/loop-prompt.md @@ -174,4 +174,4 @@ Use `td review`, not `td close` — self-closing is blocked. - **Don't break sync.** Deterministic IDs, proper event logging, no hard deletes. - **Session isolation is sacred.** Don't bypass review guards. - **If stuck, log and skip.** `td log "Blocked: "` then `td block `. -- **Commit messages reference td.** Format: `feat|fix|chore: (td-)` +- **Commit messages follow the repo hook.** Format: `type: ` or `type(scope): `, with optional ` (td-)`. diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 2b563f26..ddc8085a 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # pre-commit hook for td -# Install: make install-hooks (or: ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit) +# Install: make install-hooks (recommended; resolves the active hooks path for linked worktrees too) set -euo pipefail PASS=0