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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 3 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 changes (td-<id>)

Details here

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
Nightshift-Task: <task-id>
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 Down
26 changes: 21 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 install-hooks # install git pre-commit and commit-msg hooks" \
" make test # go test ./..." \
" make test-hooks # run hook regression 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 @@ -25,6 +26,9 @@ fmt:
test:
go test ./...

test-hooks:
./scripts/test-commit-msg.sh

install:
@V="$(GIT_DESCRIBE)"; V=$${V:-dev}; \
echo "Installing td $$V"; \
Expand Down Expand Up @@ -52,6 +56,18 @@ 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"; \
rm -f "$$hook_path"; \
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"
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id>)`. 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
Expand Down
6 changes: 4 additions & 2 deletions docs/guides/releasing-new-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id>)` when the release prep is tied to a task.

### 3. Verify Tests Pass

```bash
Expand Down Expand Up @@ -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
Expand Down
233 changes: 233 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#!/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 <<EOF
commit-msg: could not safely normalize this subject:
$subject

Use one of:
type: summary
type(scope): summary

Optional suffix:
type: summary (td-1234)

Examples:
feat: add commit message normalizer
fix(sync): preserve trailers (td-527bd4-0006)
chore(homebrew): bump td to v1.2.3
EOF
}

is_valid_td_suffix() {
local value

value=$(trim "${1-}")
[[ "$value" =~ ^[Tt][Dd]-[A-Za-z0-9._-]+$ ]]
}

is_td_like_suffix() {
local value

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

[[ "$value" =~ ^td($|[^[:alpha:]].*) ]]
}

normalize_subject() {
local subject trimmed base td_suffix type scope summary trailing_paren

subject=${1-}
trimmed=$(trim "$subject")

if [[ -z "$trimmed" ]]; then
print_error "$subject"
return 1
fi

case "$trimmed" in
Merge\ *|fixup!\ *|squash!\ *|Revert\ \"*\")
printf '%s' "$trimmed"
return 0
;;
esac

td_suffix=""
base="$trimmed"
if [[ "$base" =~ ^(.+)[[:space:]]+\(([^()]*)\)[[:space:]]*$ ]]; then
trailing_paren=$(trim "${BASH_REMATCH[2]}")

if ! is_valid_td_suffix "$trailing_paren" && is_td_like_suffix "$trailing_paren"; then
print_error "$subject"
return 1
fi
fi

if [[ "$base" =~ ^(.+)[[:space:]]+\(([Tt][Dd]-[A-Za-z0-9._-]+)\)[[:space:]]*$ ]]; then
base=$(trim "${BASH_REMATCH[1]}")
td_suffix=" ($(printf '%s' "${BASH_REMATCH[2]}" | tr '[:upper:]' '[:lower:]'))"
fi

scope=""
summary=""

if [[ "$base" =~ ^([[:alpha:]][[:alnum:]_-]*)(\(([^()]*)\))?[[:space:]]*:[[:space:]]*(.+)$ ]]; then
type=${BASH_REMATCH[1]}
scope=${BASH_REMATCH[3]:-}
summary=${BASH_REMATCH[4]}
elif [[ "$base" =~ ^([[:alpha:]][[:alnum:]_-]*)(\(([^()]*)\))?[[:space:]]*-[[:space:]]*(.+)$ ]]; then
type=${BASH_REMATCH[1]}
scope=${BASH_REMATCH[3]:-}
summary=${BASH_REMATCH[4]}
elif [[ "$base" =~ ^([[:alpha:]][[:alnum:]_-]*)[[:space:]]+([[:alnum:]_./-]+)[[:space:]]*[:-][[:space:]]*(.+)$ ]]; then
type=${BASH_REMATCH[1]}
scope=${BASH_REMATCH[2]}
summary=${BASH_REMATCH[3]}
elif [[ "$base" =~ ^([[:alpha:]][[:alnum:]_-]*)[[:space:]]+(.+)$ ]]; then
type=${BASH_REMATCH[1]}
summary=${BASH_REMATCH[2]}
else
print_error "$subject"
return 1
fi

if ! type=$(normalize_type "$type"); then
print_error "$subject"
return 1
fi

if [[ -n "$scope" ]]; then
if ! scope=$(normalize_scope "$scope"); then
print_error "$subject"
return 1
fi
fi

summary=$(normalize_summary "$summary")
if [[ -z "$summary" ]]; then
print_error "$subject"
return 1
fi

if [[ -n "$scope" ]]; then
printf '%s(%s): %s%s' "$type" "$scope" "$summary" "$td_suffix"
else
printf '%s: %s%s' "$type" "$summary" "$td_suffix"
fi
}

if [[ -z "$message_file" || ! -f "$message_file" ]]; then
echo "commit-msg: expected path to the commit message file" >&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
2 changes: 1 addition & 1 deletion scripts/loop-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> "Blocked: <reason>"` then `td block <id>`.
- **Commit messages reference td.** Format: `feat|fix|chore: <summary> (td-<id>)`
- **Commit messages follow the repo hook.** Format: `type: <summary>` or `type(scope): <summary>`, with optional ` (td-<id>)`.
2 changes: 1 addition & 1 deletion scripts/pre-commit.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading