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: update td to ${{ steps.version.outputs.version }}"
git push
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)" \
Expand Down Expand Up @@ -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"
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 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"
```

### 3. Verify Tests Pass
Expand Down Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
commit-msg: invalid commit subject:
$first_line

Expected: type: subject, type(scope): subject, type!: subject, or type(scope)!: subject
Allowed types: feat, fix, docs, test, refactor, chore, ci, build, perf, style, td
Pass-through subjects: Merge, Revert, fixup!, squash!
EOF
exit 1
fi

type="${BASH_REMATCH[1]}"
lower_type=$(
printf "%s" "$type" | tr '[:upper:]' '[:lower:]'
)

case "$lower_type" in
feat|fix|docs|test|refactor|chore|ci|build|perf|style|td)
;;
*)
cat >&2 <<EOF
commit-msg: invalid commit type: $type

Allowed types: feat, fix, docs, test, refactor, chore, ci, build, perf, style, td
Expected subject format: type: subject
EOF
exit 1
;;
esac

normalized="${lower_type}${trimmed:${#type}}"

if [[ "$normalized" != "$first_line" ]]; then
tmp_file=$(mktemp "${msg_file}.XXXXXX")
{
printf "%s\n" "$normalized"
sed '1d' "$msg_file"
} > "$tmp_file"
mv "$tmp_file" "$msg_file"
fi
113 changes: 113 additions & 0 deletions scripts/test-commit-msg-hook.sh
Original file line number Diff line number Diff line change
@@ -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)"