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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@ 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
# Allowed commit-msg hook exception for release automation.
git commit -m "td: bump to ${{ steps.version.outputs.version }}"
git push
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ go test ./... # Test all
## Version & Release

```bash
# Commit changes with proper message
# Commit changes with the canonical subject format
git add .
git commit -m "feat: description of changes
git commit -m "feat: normalize commit messages (td-a1b2)

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,8 @@ go install -ldflags "-X main.Version=v0.3.0" ./...
td version
```

Human-authored commits should use `type: summary (td-<id>)`. The `commit-msg` hook normalizes spacing and casing automatically. Automated release bump commits may use `td: bump to vX.Y.Z`.

## Architecture

- `cmd/` - Cobra commands
Expand Down
16 changes: 11 additions & 5 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 test-hooks install tag release check-clean check-version install-hooks

SHELL := /bin/sh

Expand All @@ -13,8 +13,9 @@ help:
@printf "%s\n" \
"Targets:" \
" make fmt # gofmt -w ." \
" make install-hooks # install git pre-commit hook" \
" make test # go test ./..." \
" make install-hooks # install git pre-commit + commit-msg hooks" \
" make test # go test ./... + hook tests" \
" make test-hooks # run commit-msg hook tests" \
" make install # build and install with version from git" \
" make tag VERSION=vX.Y.Z # create annotated git tag (requires clean tree)" \
" make release VERSION=vX.Y.Z # tag + push (triggers GoReleaser via GitHub Actions)"
Expand All @@ -24,6 +25,10 @@ fmt:

test:
go test ./...
bash ./scripts/test_commit-msg.sh

test-hooks:
bash ./scripts/test_commit-msg.sh

install:
@V="$(GIT_DESCRIBE)"; V=$${V:-dev}; \
Expand Down Expand Up @@ -52,6 +57,7 @@ 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"
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,14 @@ 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 checks + commit subject normalization)
make install-hooks
```

## Tests & Quality Checks

```bash
# Run all tests (114 tests across cmd/, internal/db/, internal/models/, etc.)
# Run all tests (Go packages + commit-msg hook coverage)
make test

# Expected output: ok for each package, ~2s total runtime
Expand All @@ -207,7 +207,13 @@ make test
# Format code (runs gofmt)
make fmt

# No linter configured yet — clean gofmt is current quality bar
# Commit messages are normalized by the git commit-msg hook to:
# type: summary (td-<id>)
# Example:
# feat: add sync audit trail (td-a1b2)
#
# Allowed exceptions:
# td: bump to vX.Y.Z # automated release/homebrew bump commits
```

## Release
Expand Down
5 changes: 3 additions & 2 deletions docs/guides/releasing-new-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (td-a1b2)"
```

### 3. Verify Tests Pass
Expand All @@ -74,6 +74,7 @@ git push origin vX.Y.Z
```

Pushing the tag triggers `.github/workflows/release.yml`, which runs GoReleaser to build binaries, create the GitHub release, and update the Homebrew tap.
The repo's `commit-msg` hook enforces `type: summary (td-<id>)` for human-authored commits. The Homebrew automation commit `td: bump to vX.Y.Z` is an approved exception.

### 5. Verify

Expand Down Expand Up @@ -137,7 +138,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 (td-a1b2)"

# Push commits, then tag (tag push triggers automated release)
git push origin main
Expand Down
91 changes: 91 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# commit-msg hook for td
# Install: make install-hooks (or: ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg)
set -euo pipefail

MSG_FILE=${1:?commit message file required}

trim() {
local value=${1-}
value=$(printf '%s' "$value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
printf '%s' "$value"
}

squeeze_spaces() {
local value=${1-}
value=$(printf '%s' "$value" | tr '\t' ' ' | sed -E 's/[[:space:]]+/ /g')
trim "$value"
}

lowercase() {
printf '%s' "${1-}" | tr '[:upper:]' '[:lower:]'
}

fail_invalid() {
cat >&2 <<'EOF'
td commit-msg: commit subject must look like:
type: summary (td-<id>)

Examples:
feat: add commit message hook (td-a1b2)
fix: preserve commit trailers (td-c3d4)

Allowed exceptions:
- merge/revert/fixup/squash commits generated by git
- automated release bump commits: td: bump to vX.Y.Z
EOF
exit 1
}

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

subject=$(sed -n '1p' "$MSG_FILE")
rest=$(tail -n +2 "$MSG_FILE" 2>/dev/null || true)
subject=$(squeeze_spaces "$subject")

if [[ -z "$subject" ]]; then
fail_invalid
fi

case "$subject" in
Merge\ *|Revert\ *|fixup!\ *|squash!\ *)
exit 0
;;
esac

if [[ "$subject" =~ ^[Tt][Dd][[:space:]]*:[[:space:]]*bump[[:space:]]+to[[:space:]]+(v[0-9]+(\.[0-9]+){2}([-.][[:alnum:]]+)*)$ ]]; then
normalized_subject="td: bump to ${BASH_REMATCH[1]}"
else
if [[ ! "$subject" =~ ^([[:alpha:]][[:alnum:]-]*)[[:space:]]*:[[:space:]]*(.+)$ ]]; then
fail_invalid
fi

commit_type=$(lowercase "${BASH_REMATCH[1]}")
remainder=$(squeeze_spaces "${BASH_REMATCH[2]}")
token=""
summary=""

if [[ "$remainder" =~ ^(.*[^[:space:]])[[:space:]]*\(([Tt][Dd]-[[:alnum:]][[:alnum:]-]*)\)[[:space:]]*$ ]]; then
summary=$(squeeze_spaces "${BASH_REMATCH[1]}")
token=$(lowercase "${BASH_REMATCH[2]}")
elif [[ "$remainder" =~ ^(.*[^[:space:]])[[:space:]]+([Tt][Dd]-[[:alnum:]][[:alnum:]-]*)[[:space:]]*$ ]]; then
summary=$(squeeze_spaces "${BASH_REMATCH[1]}")
token=$(lowercase "${BASH_REMATCH[2]}")
fi

if [[ -z "$token" || -z "$summary" ]]; then
fail_invalid
fi

normalized_subject="${commit_type}: ${summary} (${token})"
fi

tmp=$(mktemp)
printf '%s\n' "$normalized_subject" > "$tmp"
if [[ -n "$rest" ]]; then
printf '%s\n' "$rest" >> "$tmp"
fi
mv "$tmp" "$MSG_FILE"
86 changes: 86 additions & 0 deletions scripts/test_commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
HOOK="$ROOT/scripts/commit-msg.sh"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

pass_count=0

run_success_case() {
local name=$1
local input=$2
local expected=$3
local file="$TMPDIR/${name}.txt"

printf '%s' "$input" > "$file"
"$HOOK" "$file"

local actual
actual=$(cat "$file")
if [[ "$actual" != "$expected" ]]; then
echo "FAIL: $name"
echo "expected:"
printf '%s\n' "$expected"
echo "actual:"
printf '%s\n' "$actual"
exit 1
fi

pass_count=$((pass_count + 1))
}

run_failure_case() {
local name=$1
local input=$2
local file="$TMPDIR/${name}.txt"

printf '%s' "$input" > "$file"
if "$HOOK" "$file" >/dev/null 2>&1; then
echo "FAIL: $name"
echo "expected hook failure"
exit 1
fi

pass_count=$((pass_count + 1))
}

run_success_case \
canonical \
$'feat: add commit hook (td-a1b2)\n\nNightshift-Task: commit-normalize\nNightshift-Ref: https://github.com/marcus/nightshift\n' \
$'feat: add commit hook (td-a1b2)\n\nNightshift-Task: commit-normalize\nNightshift-Ref: https://github.com/marcus/nightshift'

run_success_case \
normalize_subject \
$' Feat : add commit hook td-A1B2 \n' \
$'feat: add commit hook (td-a1b2)'

run_success_case \
preserve_body_and_trailers \
$'fix: preserve trailers td-c3d4\n\nBody line\n\nNightshift-Task: commit-normalize\nNightshift-Ref: https://github.com/marcus/nightshift\n' \
$'fix: preserve trailers (td-c3d4)\n\nBody line\n\nNightshift-Task: commit-normalize\nNightshift-Ref: https://github.com/marcus/nightshift'

run_success_case \
keep_summary_task_mentions \
$'feat: mention td-a1b2 parsing before final ref (td-c3d4)\n' \
$'feat: mention td-a1b2 parsing before final ref (td-c3d4)'

run_success_case \
release_exception \
$'TD: bump to v1.2.3\n' \
$'td: bump to v1.2.3'

run_failure_case \
missing_task_id \
$'feat: add commit hook\n'

run_failure_case \
missing_summary \
$'feat: td-a1b2\n'

run_failure_case \
ambiguous_non_trailing_task_id \
$'feat: mention td-a1b2 parsing before final ref\n'

echo "commit-msg hook tests passed: $pass_count"