diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 456b816e..f3c90fc9 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: update td to ${{ steps.version.outputs.version }}" git push diff --git a/Makefile b/Makefile index 18da511f..1fecf7bc 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,10 @@ 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" + @echo "Installing git hooks..." + @hooks_dir=$$(git rev-parse --git-common-dir)/hooks; \ + repo_root=$$(git rev-parse --show-toplevel); \ + mkdir -p "$$hooks_dir"; \ + ln -sf "$$repo_root/scripts/pre-commit.sh" "$$hooks_dir/pre-commit"; \ + ln -sf "$$repo_root/scripts/commit-msg.sh" "$$hooks_dir/commit-msg"; \ + echo "Done. Hooks installed at $$hooks_dir/pre-commit and $$hooks_dir/commit-msg" diff --git a/README.md b/README.md index 684416ad..36ea7f1b 100644 --- a/README.md +++ b/README.md @@ -189,10 +189,32 @@ make install-dev # Format code make fmt -# Install git pre-commit hook (gofmt, go vet, go build on staged files) +# Install git hooks make install-hooks ``` +`make install-hooks` installs: + +- `pre-commit`: runs gofmt on staged Go files, `go vet ./...`, and `go build ./...` +- `commit-msg`: normalizes regular commit subjects to Conventional Commit format + +Accepted regular commit subjects use one of these forms: + +```text +type: subject +type(scope): subject +type!: subject +type(scope)!: subject +``` + +Allowed types are `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `ci`, `build`, `perf`, `style`, and `td`. The hook lowercases the type and trims/collapses whitespace in the subject line while preserving the body and trailers. Merge, Revert, fixup!, and squash! commits pass through unchanged. + +Run the hook tests with: + +```bash +bash scripts/test-commit-msg-hook.sh +``` + ## Tests & Quality Checks ```bash diff --git a/docs/guides/releasing-new-version.md b/docs/guides/releasing-new-version.md index ca98e527..a771ab2c 100644 --- a/docs/guides/releasing-new-version.md +++ b/docs/guides/releasing-new-version.md @@ -53,7 +53,7 @@ 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" ``` ### 3. Verify Tests Pass @@ -137,7 +137,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..3569532c --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# commit-msg hook for td. +set -euo pipefail + +msg_file="${1:-}" +if [[ -z "$msg_file" || ! -f "$msg_file" ]]; then + echo "commit-msg: expected path to commit message file" >&2 + exit 1 +fi + +first_line="" +IFS= read -r first_line < "$msg_file" || true + +trimmed=$( + printf "%s\n" "$first_line" | awk '{$1=$1; print}' +) + +case "$trimmed" in + Merge\ *|Revert\ *|fixup!\ *|squash!\ *) + exit 0 + ;; +esac + +if [[ ! "$trimmed" =~ ^([[:alpha:]]+)(\([^()[:space:]]+\))?(!)?:[[:space:]](.+)$ ]]; then + cat >&2 <&2 < "$tmp_file" + mv "$tmp_file" "$msg_file" +fi diff --git a/scripts/test-commit-msg-hook.sh b/scripts/test-commit-msg-hook.sh new file mode 100755 index 00000000..d9ad2a75 --- /dev/null +++ b/scripts/test-commit-msg-hook.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir=$( + cd "$(dirname "${BASH_SOURCE[0]}")" && pwd +) +hook="$script_dir/commit-msg.sh" +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +pass=0 + +assert_hook() { + name="$1" + input="$2" + expected="$3" + msg="$tmp_dir/$name.msg" + want="$tmp_dir/$name.want" + + printf "%s" "$input" > "$msg" + printf "%s" "$expected" > "$want" + + bash "$hook" "$msg" + cmp -s "$msg" "$want" + pass=$((pass + 1)) +} + +assert_rejects_unchanged() { + name="$1" + input="$2" + msg="$tmp_dir/$name.msg" + original="$tmp_dir/$name.original" + err="$tmp_dir/$name.err" + + printf "%s" "$input" > "$msg" + cp "$msg" "$original" + + if bash "$hook" "$msg" 2>"$err"; then + echo "expected rejection for $name" >&2 + exit 1 + fi + + cmp -s "$msg" "$original" + grep -q "invalid commit subject" "$err" + pass=$((pass + 1)) +} + +assert_hook \ + "normalizes-whitespace-and-type" \ + " FIX: collapse whitespace " \ + "fix: collapse whitespace +" + +assert_hook \ + "scoped-breaking" \ + "FEAT(cli)!: Add review guard + +Body paragraph with extra spacing. + +Nightshift-Task: commit-normalize +Nightshift-Ref: https://github.com/marcus/nightshift +" \ + "feat(cli)!: Add review guard + +Body paragraph with extra spacing. + +Nightshift-Task: commit-normalize +Nightshift-Ref: https://github.com/marcus/nightshift +" + +for subject in \ + "Merge branch 'main'" \ + "Revert \"feat: add thing\"" \ + "fixup! feat: add thing" \ + "squash! fix: adjust thing" +do + assert_hook \ + "passthrough-${subject//[^[:alnum:]]/-}" \ + "$subject + +unchanged body +" \ + "$subject + +unchanged body +" +done + +assert_hook \ + "body-and-trailers-preserved" \ + "DOCS: update release notes + +Keep this body exactly. + +Nightshift-Task: commit-normalize +Nightshift-Ref: https://github.com/marcus/nightshift +" \ + "docs: update release notes + +Keep this body exactly. + +Nightshift-Task: commit-normalize +Nightshift-Ref: https://github.com/marcus/nightshift +" + +assert_rejects_unchanged \ + "invalid-subject" \ + "Update the thing + +Body should remain. +" + +echo "commit-msg hook tests passed ($pass)"