Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 the changes (td-abc123)

Details here

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
Nightshift-Task: commit-normalize
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"
Expand All @@ -56,6 +55,15 @@ go install -ldflags "-X main.Version=v0.3.0" ./...
td version
```

Use `make install-hooks` in this repo to install both local Git hooks:
- `pre-commit` runs `gofmt`, `go vet`, and `go build`
- `commit-msg` enforces `<type>: <summary> (td-<id>)` for task work

Allowed `commit-msg` exceptions:
- `docs: Update changelog for vX.Y.Z`
- `td: bump to vX.Y.Z`
- Git-generated merge, revert, `fixup!`, and `squash!` subjects

## Architecture

- `cmd/` - Cobra commands
Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help fmt test install tag release check-clean check-version install-hooks
.PHONY: help fmt test install tag release check-clean check-version install-hooks test-commit-msg

SHELL := /bin/sh

Expand All @@ -13,7 +13,8 @@ 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-commit-msg # run commit-msg hook regression checks" \
" 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)" \
Expand Down Expand Up @@ -52,6 +53,10 @@ release: tag
git push origin "$(VERSION)"

install-hooks:
@echo "Installing git pre-commit hook..."
@echo "Installing git hooks..."
@ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
@echo "Done. Hook installed at .git/hooks/pre-commit"
@ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg
@echo "Done. Hooks installed at .git/hooks/pre-commit and .git/hooks/commit-msg"

test-commit-msg:
@./scripts/test_commit_msg_hook.sh
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,13 @@ 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 runs gofmt, go vet, and go build on staged work
# commit-msg enforces <type>: <summary> (td-<id>) for task commits
make install-hooks

# Example task commit
git commit -m "feat: normalize commit messages (td-abc123)"
```

## Tests & Quality Checks
Expand All @@ -207,9 +212,14 @@ make test
# Format code (runs gofmt)
make fmt

# Run commit message hook regression checks
make test-commit-msg

# No linter configured yet — clean gofmt is current quality bar
```

Task-oriented commits should use a conventional prefix plus trailing td reference, such as `fix: preserve stash on branch switch (td-abc123)`. The `commit-msg` hook allows release-specific exceptions that already exist in this repo, including `docs: Update changelog for vX.Y.Z`, `td: bump to vX.Y.Z`, and Git-generated merge, revert, `fixup!`, or `squash!` subjects.

## Release

Releases are automated via GoReleaser. Pushing a version tag triggers GitHub Actions to build binaries and update the Homebrew formula.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/releasing-new-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ git add CHANGELOG.md
git commit -m "docs: Update changelog for vX.Y.Z"
```

That changelog subject is intentionally exempt from the local `commit-msg` hook because release bookkeeping is not tied to a single `td-...` task.

### 3. Verify Tests Pass

```bash
Expand Down
60 changes: 60 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# commit-msg hook for td
# Installed by: make install-hooks
# Manual install: ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: $0 <commit-message-file>" >&2
exit 2
fi

msg_file=$1

if [[ ! -f "$msg_file" ]]; then
echo "commit-msg: file not found: $msg_file" >&2
exit 2
fi

clean_message=$(git stripspace --strip-comments <"$msg_file")
subject=$(printf '%s\n' "$clean_message" | sed -n '1p')

if [[ -z "$subject" ]]; then
cat >&2 <<'EOF'
Commit message is empty.

Use the standard task-work format:
<type>: <summary> (td-<id>)

Example:
feat: normalize commit messages (td-abc123)
EOF
exit 1
fi

task_pattern='^(feat|fix|docs|chore|refactor|test|perf|build|ci|style|revert)(\([a-z0-9._/-]+\))?: .+ \(td-[a-z0-9]+\)$'
exception_pattern='^(Merge( branch| remote-tracking branch| pull request).+|Revert ".+"|(fixup|squash)! .+|docs: Update changelog for v[0-9]+\.[0-9]+\.[0-9]+|td: bump to v[0-9]+\.[0-9]+\.[0-9]+)$'

if [[ "$subject" =~ $task_pattern ]] || [[ "$subject" =~ $exception_pattern ]]; then
exit 0
fi

cat >&2 <<EOF
Commit subject does not match td's standard format:
$subject

Expected:
<type>: <summary> (td-<id>)

Examples:
feat: normalize commit messages (td-abc123)
fix(parser): handle empty commit subject (td-9f2e1a)

Allowed exceptions:
docs: Update changelog for vX.Y.Z
td: bump to vX.Y.Z
Merge / Revert / fixup! / squash! messages generated by Git

If this commit maps to task work, include the task id at the end of the subject.
EOF
exit 1
3 changes: 2 additions & 1 deletion scripts/pre-commit.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# pre-commit hook for td
# Install: make install-hooks (or: ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit)
# Installed by: make install-hooks
# Manual install: ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
set -euo pipefail

PASS=0
Expand Down
60 changes: 60 additions & 0 deletions scripts/test_commit_msg_hook.sh
Original file line number Diff line number Diff line change
@@ -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"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

pass_count=0
fail_count=0

run_case() {
local name=$1
local expected=$2
local content=$3
local msg_file="$tmpdir/msg.txt"
local out_file="$tmpdir/out.txt"

printf '%s\n' "$content" >"$msg_file"

if "$hook" "$msg_file" >"$out_file" 2>&1; then
status=0
else
status=$?
fi

if [[ $status -eq $expected ]]; then
printf "ok %s\n" "$name"
pass_count=$((pass_count + 1))
else
printf "FAIL %s (expected exit %s, got %s)\n" "$name" "$expected" "$status"
sed 's/^/ /' "$out_file"
fail_count=$((fail_count + 1))
fi
}

run_case "accepts standard task commit" 0 "feat: normalize commit messages (td-abc123)"
run_case "accepts scoped standard task commit" 0 "fix(parser): handle empty commit subject (td-9f2e1a)"
run_case "accepts trailers in body" 0 "$(cat <<'EOF'
docs: explain commit hook usage (td-00cafe)

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
EOF
)"
run_case "accepts changelog exception" 0 "docs: Update changelog for v1.2.3"
run_case "accepts release bump exception" 0 "td: bump to v1.2.3"
run_case "accepts merge subject" 0 "Merge branch 'main' into feat/commit-message-normalizer-task"
run_case "accepts fixup subject" 0 "fixup! feat: normalize commit messages (td-abc123)"
run_case "accepts revert type" 0 "revert: back out broken normalization rule (td-abc123)"
run_case "rejects missing task id" 1 "feat: normalize commit messages"
run_case "rejects missing type prefix" 1 "normalize commit messages (td-abc123)"
run_case "rejects uppercase task id" 1 "feat: normalize commit messages (TD-abc123)"

if [[ $fail_count -gt 0 ]]; then
printf "\n%s commit-msg regression check(s) failed.\n" "$fail_count"
exit 1
fi

printf "\nAll %s commit-msg regression checks passed.\n" "$pass_count"