From 96a5e65b4a3da501fbccba2e1dc9cc8630cde5c2 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Tue, 14 Apr 2026 13:49:12 -0400 Subject: [PATCH] =?UTF-8?q?refactor(hooks):=20retire=20dev/scripts/git-hoo?= =?UTF-8?q?ks=20=E2=80=94=20crates=20is=20the=20only=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 consolidation. Canonical hook content is now owned exclusively by resq-software/crates (crates/resq-cli/templates/git-hooks/). scripts/install-hooks.sh: - When `resq` is on PATH, delegate to `resq dev install-hooks` — the binary embeds the templates via include_str! so there's no network round-trip. - Otherwise fall back to curl against raw.githubusercontent.com/resq-software/crates/master/.../templates/git-hooks/ (was: resq-software/dev/main/scripts/git-hooks/ — retired). - RESQ_CRATES_REF replaces RESQ_DEV_REF for pinning the fallback. scripts/git-hooks/ — removed entirely (6 hook files + README.md). tests/hooks/helpers.bash: - Fetch templates from the crates raw URL (once per bats session, cached under /tmp). - RESQ_HOOK_SRC_DIR lets you point at a local template dir for pre-push testing. 33/33 tests still pass. .github/workflows/hooks-sync.yml — rewritten: - No more diff-check (nothing to diff against on this side). - `lint` job shellchecks install-hooks.sh + install-resq.sh. - New `smoke` job: spins up a throwaway git repo, runs the installer with a scrubbed PATH (forcing the raw-fetch fallback), and asserts the 6 hooks are present, executable, and parse. README.md + AGENTS.md updated to reflect the single-source story and drop the "three copies" note. Verified: - shellcheck -S warning + bash -n clean - resq-preferred path: `resq dev install-hooks` scaffolds 6 hooks - raw-fetch fallback: curl from crates master installs 6 hooks - bats tests/hooks/: 33/33 pass (uses cached templates from crates raw) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/hooks-sync.yml | 63 ++++++++++------------- AGENTS.md | 19 +++++-- README.md | 9 +++- scripts/git-hooks/README.md | 53 -------------------- scripts/git-hooks/commit-msg | 46 ----------------- scripts/git-hooks/post-checkout | 30 ----------- scripts/git-hooks/post-merge | 24 --------- scripts/git-hooks/pre-commit | 42 ---------------- scripts/git-hooks/pre-push | 74 ---------------------------- scripts/git-hooks/prepare-commit-msg | 31 ------------ scripts/install-hooks.sh | 67 ++++++++++++------------- tests/hooks/helpers.bash | 24 +++++++-- 12 files changed, 97 insertions(+), 385 deletions(-) delete mode 100644 scripts/git-hooks/README.md delete mode 100755 scripts/git-hooks/commit-msg delete mode 100755 scripts/git-hooks/post-checkout delete mode 100755 scripts/git-hooks/post-merge delete mode 100755 scripts/git-hooks/pre-commit delete mode 100755 scripts/git-hooks/pre-push delete mode 100755 scripts/git-hooks/prepare-commit-msg diff --git a/.github/workflows/hooks-sync.yml b/.github/workflows/hooks-sync.yml index 072b5a4..2f82b96 100644 --- a/.github/workflows/hooks-sync.yml +++ b/.github/workflows/hooks-sync.yml @@ -1,13 +1,13 @@ # Copyright 2026 ResQ Software # SPDX-License-Identifier: Apache-2.0 # -# Validate canonical git hooks shipped from this repo: -# - shellcheck + bash-parse every hook and the installer -# - diff scripts/git-hooks/ against the embedded templates in -# resq-software/crates (crates/resq-cli/templates/git-hooks/) at master +# Validate the installer that ships canonical ResQ git hooks. # -# The templates must be byte-identical — they are the single source of -# truth that installs into every ResQ repo. +# Canonical template content lives in resq-software/crates +# (crates/resq-cli/templates/git-hooks/). This repo no longer ships its own +# copy — install-hooks.sh prefers `resq dev install-hooks` when the binary +# is available, and falls back to fetching templates from the crates repo. +# So the only thing to validate here is the installer itself. name: hooks-sync @@ -15,13 +15,13 @@ on: push: branches: [main] paths: - - 'scripts/git-hooks/**' - 'scripts/install-hooks.sh' + - 'scripts/install-resq.sh' - '.github/workflows/hooks-sync.yml' pull_request: paths: - - 'scripts/git-hooks/**' - 'scripts/install-hooks.sh' + - 'scripts/install-resq.sh' - '.github/workflows/hooks-sync.yml' workflow_dispatch: @@ -42,47 +42,34 @@ jobs: run: | shellcheck -S warning \ scripts/install-hooks.sh \ - scripts/git-hooks/pre-commit \ - scripts/git-hooks/commit-msg \ - scripts/git-hooks/prepare-commit-msg \ - scripts/git-hooks/pre-push \ - scripts/git-hooks/post-checkout \ - scripts/git-hooks/post-merge + scripts/install-resq.sh - name: bash -n parse check run: | set -e - for f in scripts/install-hooks.sh scripts/git-hooks/pre-commit scripts/git-hooks/commit-msg scripts/git-hooks/prepare-commit-msg scripts/git-hooks/pre-push scripts/git-hooks/post-checkout scripts/git-hooks/post-merge; do + for f in scripts/install-hooks.sh scripts/install-resq.sh; do bash -n "$f" done - drift-check: - name: drift vs crates templates + smoke: + name: smoke — raw-fetch fallback path runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Fetch canonical templates from resq-software/crates@main + - name: Spin up a throwaway git repo and install hooks run: | - mkdir -p /tmp/crates-templates - for h in pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge; do - curl -fsSL \ - "https://raw.githubusercontent.com/resq-software/crates/master/crates/resq-cli/templates/git-hooks/$h" \ - -o "/tmp/crates-templates/$h" - done - - - name: Diff against scripts/git-hooks/ - run: | - rc=0 + set -e + TMP="$(mktemp -d)" + cd "$TMP" + git init -q + git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init + # Force the raw-fetch fallback by ensuring no `resq` on PATH. + env -i HOME="$HOME" PATH=/usr/bin:/bin \ + bash "$GITHUB_WORKSPACE/scripts/install-hooks.sh" "$TMP" + ls -la "$TMP/.git-hooks/" for h in pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge; do - if ! diff -u "scripts/git-hooks/$h" "/tmp/crates-templates/$h"; then - echo "::error file=scripts/git-hooks/$h::drifts from resq-software/crates@master:crates/resq-cli/templates/git-hooks/$h" - rc=1 - fi + test -x "$TMP/.git-hooks/$h" || { echo "missing: $h"; exit 1; } + bash -n "$TMP/.git-hooks/$h" done - if [ "$rc" -ne 0 ]; then - echo "::error::Canonical hook templates in dev/ and crates/ must be byte-identical." - echo "::error::Sync them in both repos or retire one copy (Phase 4 of the hardening plan)." - exit 1 - fi - echo "✅ All hook templates match crates/resq-cli/templates/git-hooks/" + [ "$(git -C "$TMP" config core.hooksPath)" = ".git-hooks" ] diff --git a/AGENTS.md b/AGENTS.md index 514dee9..f4f25d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,10 @@ scripts/ install-hooks.sh — Installs canonical git hooks into a repo (local or curl-piped) install-hooks.ps1 — PowerShell mirror install-resq.sh — Installs the `resq` CLI binary from GitHub Releases (SHA-verified) - git-hooks/ — Canonical hook shims (pre-commit, commit-msg, pre-push, …) + # Canonical hook templates are owned by resq-software/crates + # (crates/resq-cli/templates/git-hooks/). install-hooks.sh fetches them + # from there (or lets `resq dev install-hooks` scaffold offline). No copy + # lives in this repo. lib/ log.{sh,ps1} — Colored log helpers platform.{sh,ps1} — OS / arch detection, command_exists @@ -58,10 +61,16 @@ shellcheck install.sh # Lint the bash script ## Git hooks -Canonical hooks live in `scripts/git-hooks/` and are installed into any ResQ -repo by `scripts/install-hooks.sh` (or `.ps1`). The hooks are thin shims that -delegate heavy lifting to the `resq` CLI binary from -[`resq-software/crates`](https://github.com/resq-software/crates): +Canonical hook templates live in +[`resq-software/crates`](https://github.com/resq-software/crates/tree/master/crates/resq-cli/templates/git-hooks) +and are installed into any ResQ repo by `scripts/install-hooks.sh` (or +`.ps1`). When the `resq` binary is on PATH, the installer calls +`resq dev install-hooks` which scaffolds from the embedded templates — +offline, no network round-trip. Without `resq`, it falls back to fetching +the templates from the crates repo via raw.githubusercontent.com. + +The hooks are thin shims that delegate heavy lifting back to the `resq` +binary: - `pre-commit` → `resq pre-commit` (copyright, secrets, audit, polyglot format) - `commit-msg` → Conventional Commits + fixup/WIP guard on main/master diff --git a/README.md b/README.md index 9c88b3c..a8a7345 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,12 @@ Everything is pinned via Nix flakes. No "works on my machine" issues. ## ✅ Quality gates — canonical git hooks -Six hook shims live in [`scripts/git-hooks/`](scripts/git-hooks/) and ship with `install-hooks.sh`. They delegate logic to the [`resq`](https://github.com/resq-software/crates) binary so updates roll out via a `cargo install --git` (or `install-resq.sh`) instead of by editing every repo. +Six hook shims live in [`resq-software/crates`](https://github.com/resq-software/crates/tree/master/crates/resq-cli/templates/git-hooks) — embedded in the `resq` binary *and* served at a stable raw URL. `install-hooks.sh` picks the best path automatically: + +1. **`resq` on PATH** → calls `resq dev install-hooks`, which scaffolds the 6 canonical hooks from the templates embedded in the binary. Offline, versioned with the installed `resq`. +2. **No `resq`** → falls back to `curl` from `resq-software/crates/master/.../templates/git-hooks/`. + +The hooks delegate logic back to the `resq` binary (`resq pre-commit`, etc.), so updates roll out via `cargo install --git` (or `install-resq.sh`) without editing every repo. | Hook | What it gates | |---|---| @@ -249,7 +254,7 @@ resq dev scaffold-local-hook --kind auto # detects rust/python/node/dotnet/cp `resq hooks doctor` reports drift, `resq hooks update` re-syncs from the embedded canonical, `resq hooks status` prints a one-line shell-friendly summary. -The same content lives in three places (`scripts/git-hooks/` here, `crates/resq-cli/templates/git-hooks/` in the `resq` binary, `crates/.git-hooks/` in the crates repo). All three are kept byte-identical by `hooks-sync.yml` workflows in both `dev/` and `crates/`. Bats + Rust integration tests cover the hook behavior end-to-end. +The canonical content lives in exactly one place: [`crates/resq-cli/templates/git-hooks/`](https://github.com/resq-software/crates/tree/master/crates/resq-cli/templates/git-hooks). The crates repo's own `.git-hooks/` (for dog-fooding) is kept identical via `hooks-sync.yml`. The `dev/` repo used to ship a third copy and was retired in Phase 4 — `install-hooks.sh` now fetches from the crates source (or lets `resq dev install-hooks` do it offline). Bats + Rust integration tests cover the hook behavior end-to-end. ## 📄 License diff --git a/scripts/git-hooks/README.md b/scripts/git-hooks/README.md deleted file mode 100644 index 5c47a20..0000000 --- a/scripts/git-hooks/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Canonical ResQ Git Hooks - -Thin shims that dispatch heavy lifting to the `resq` CLI binary from -[`resq-software/crates`](https://github.com/resq-software/crates) and expose a -per-repo escape hatch via `local-` files. - -## Files - -| Hook | What it does | -|-----------------------|------------------------------------------------------------------------------| -| `pre-commit` | Delegates to `resq pre-commit` (copyright, secrets, audit, format) | -| `commit-msg` | Conventional Commits; blocks fixup/squash/WIP on main/master | -| `prepare-commit-msg` | Prepends `[TICKET-123]` from branch name | -| `pre-push` | Force-push guard, branch naming convention | -| `post-checkout` | Notifies on lock-file changes (Cargo, bun, uv, flake) | -| `post-merge` | Same, post-merge | - -## Install into a repo - -From `dev/`: - -```sh -scripts/install-hooks.sh /path/to/repo -``` - -Curl-piped from anywhere: - -```sh -cd /path/to/repo -curl -fsSL https://raw.githubusercontent.com/resq-software/dev/main/scripts/install-hooks.sh | sh -``` - -## Per-repo escape hatch - -Each hook invokes `.git-hooks/local-` (if executable) after its canonical -checks pass. Put repo-specific logic there: - -```sh -# Example: .git-hooks/local-pre-push in a Rust repo -#!/usr/bin/env bash -set -e -cargo check --workspace --quiet -``` - -Commit `local-*` files; the canonical hooks are managed by `install-hooks.sh`. - -## Bypass - -| Scope | How | -|-------------------------------|----------------------------------------| -| Single commit/push | `git commit --no-verify` / `git push --no-verify` | -| All hooks in this shell | `export GIT_HOOKS_SKIP=1` | -| Whole repo | `git config --unset core.hooksPath` | diff --git a/scripts/git-hooks/commit-msg b/scripts/git-hooks/commit-msg deleted file mode 100755 index 8e62e20..0000000 --- a/scripts/git-hooks/commit-msg +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 ResQ Software -# SPDX-License-Identifier: Apache-2.0 -# -# Canonical ResQ commit-msg shim — source: resq-software/dev. -# Enforces Conventional Commits; blocks fixup/squash/WIP on main/master. - -set -euo pipefail - -[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0 - -INPUT_FILE="${1:-}" -PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+$" - -FIRST_LINE=$(head -1 "$INPUT_FILE") -# Ticket-prefix regex matches what prepare-commit-msg prepends ({2,} chars). -SUBJECT=$(sed -E 's/^\[[A-Z]{2,}-[0-9]+\][[:space:]]*//' <<<"$FIRST_LINE") - -# WIP / fixup! / squash! guard runs *before* the format check so users get -# a branch-specific error message on main/master instead of the generic -# "Invalid commit message format". -BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") -case "$BRANCH" in - main|master) - if grep -qiE "^(\[[A-Z]{2,}-[0-9]+\] )?(fixup!|squash!|wip[: ])" <<<"$FIRST_LINE"; then - echo "Error: fixup!/squash!/WIP commits are not allowed on $BRANCH." - echo "Create a feature branch instead." - exit 1 - fi - ;; -esac - -if ! grep -qE "$PATTERN" <<<"$SUBJECT"; then - echo "Error: Invalid commit message format." - echo "Expected: type(scope)(!): subject" - echo "Examples:" - echo " feat(core): add new feature" - echo " feat!: remove deprecated API (breaking change marker)" - echo " fix(ui): fix button color" - exit 1 -fi - -LOCAL_HOOK="$(git rev-parse --show-toplevel)/.git-hooks/local-commit-msg" -if [ -x "$LOCAL_HOOK" ]; then - exec "$LOCAL_HOOK" "$@" -fi diff --git a/scripts/git-hooks/post-checkout b/scripts/git-hooks/post-checkout deleted file mode 100755 index 6627396..0000000 --- a/scripts/git-hooks/post-checkout +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 ResQ Software -# SPDX-License-Identifier: Apache-2.0 -# -# Canonical ResQ post-checkout shim — source: resq-software/dev. -# Notifies when lock files change so devs know to resync dependencies. - -set -euo pipefail - -[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0 - -PREV_HEAD="${1:-}" -NEW_HEAD="${2:-}" -IS_BRANCH_CHECKOUT="${3:-}" - -if [ "$IS_BRANCH_CHECKOUT" = "1" ] && [ "$PREV_HEAD" != "$NEW_HEAD" ]; then - CHANGED=$(git diff --name-only "$PREV_HEAD" "$NEW_HEAD" 2>/dev/null || true) - - grep -q "^Cargo\.lock$" <<<"$CHANGED" && echo "📦 Cargo.lock changed — run: cargo build" - grep -q "^bun\.lockb\?$" <<<"$CHANGED" && echo "📦 bun.lock changed — run: bun install" - grep -q "^uv\.lock$" <<<"$CHANGED" && echo "📦 uv.lock changed — run: uv sync" - grep -q "^flake\.lock$" <<<"$CHANGED" && echo "📦 flake.lock changed — exit and re-enter: nix develop" -fi - -LOCAL_HOOK="$(git rev-parse --show-toplevel)/.git-hooks/local-post-checkout" -if [ -x "$LOCAL_HOOK" ]; then - exec "$LOCAL_HOOK" "$@" -fi - -exit 0 diff --git a/scripts/git-hooks/post-merge b/scripts/git-hooks/post-merge deleted file mode 100755 index d95996f..0000000 --- a/scripts/git-hooks/post-merge +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 ResQ Software -# SPDX-License-Identifier: Apache-2.0 -# -# Canonical ResQ post-merge shim — source: resq-software/dev. -# Notifies when lock files change after a merge. - -set -euo pipefail - -[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0 - -CHANGED=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD 2>/dev/null || true) - -grep -q "^Cargo\.lock$" <<<"$CHANGED" && echo "📦 Cargo.lock changed after merge — run: cargo build" -grep -q "^bun\.lockb\?$" <<<"$CHANGED" && echo "📦 bun.lock changed after merge — run: bun install" -grep -q "^uv\.lock$" <<<"$CHANGED" && echo "📦 uv.lock changed after merge — run: uv sync" -grep -q "^flake\.lock$" <<<"$CHANGED" && echo "📦 flake.lock changed after merge — exit and re-enter: nix develop" - -LOCAL_HOOK="$(git rev-parse --show-toplevel)/.git-hooks/local-post-merge" -if [ -x "$LOCAL_HOOK" ]; then - exec "$LOCAL_HOOK" "$@" -fi - -exit 0 diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit deleted file mode 100755 index dd24586..0000000 --- a/scripts/git-hooks/pre-commit +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 ResQ Software -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Canonical ResQ pre-commit shim — source: resq-software/dev. -# Delegates all logic to `resq pre-commit`; runs `.git-hooks/local-pre-commit` -# as a repo-specific escape hatch if present and executable. - -set -euo pipefail - -[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0 - -PROJECT_ROOT="$(git rev-parse --show-toplevel)" - -# ── Resolve resq binary ───────────────────────────────────────────────────── -# PATH first (covers nix develop + ~/.cargo/bin on PATH). Soft-skip otherwise -# so a missing backend never blocks a commit silently — the user sees a hint. -RESQ_BIN="" -if command -v resq >/dev/null 2>&1; then - RESQ_BIN="resq" -elif [ -x "$HOME/.cargo/bin/resq" ]; then - RESQ_BIN="$HOME/.cargo/bin/resq" -fi - -if [ -n "$RESQ_BIN" ]; then - "$RESQ_BIN" pre-commit --root "$PROJECT_ROOT" "$@" -else - echo "⚠️ resq not found — skipping ResQ pre-commit checks." - echo " Install: enter 'nix develop', or run:" - echo " cargo install --git https://github.com/resq-software/crates resq-cli" -fi - -# ── Local override ────────────────────────────────────────────────────────── -LOCAL_HOOK="$PROJECT_ROOT/.git-hooks/local-pre-commit" -if [ -x "$LOCAL_HOOK" ]; then - exec "$LOCAL_HOOK" "$@" -fi diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push deleted file mode 100755 index e1e868b..0000000 --- a/scripts/git-hooks/pre-push +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 ResQ Software -# SPDX-License-Identifier: Apache-2.0 -# -# Canonical ResQ pre-push shim — source: resq-software/dev. -# Force-push guard on main/master, branch naming convention, and an optional -# per-repo local-pre-push hook for language-specific checks -# (e.g. cargo check, ruff check, anchor build). - -set -euo pipefail - -[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0 - -echo "🚀 Running pre-push checks..." - -BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") - -# Capture stdin (the ref info git passes to pre-push) so both the force-push -# guard and any local-pre-push hook can read it. -PUSH_REFS="" -if [ ! -t 0 ]; then - PUSH_REFS=$(cat) -fi - -# ── Force-push guard on main/master ───────────────────────────────────────── -case "$BRANCH" in - main|master) - # Only iterate when git actually gave us refs — an empty $PUSH_REFS - # would otherwise feed one blank line through the heredoc and trip - # the guard. - if [ -n "$PUSH_REFS" ]; then - while read -r _local_ref local_sha _remote_ref remote_sha; do - [ -z "$local_sha" ] && continue - if [ "$remote_sha" != "0000000000000000000000000000000000000000" ] && \ - [ "$local_sha" != "0000000000000000000000000000000000000000" ]; then - if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then - echo "❌ Force push to $BRANCH is not allowed." - echo " Override with: git push --no-verify" - exit 1 - fi - fi - done </dev/null || echo "") -[ -z "$BRANCH" ] && exit 0 - -TICKET=$(grep -oE '[A-Z]{2,}-[0-9]+' <<<"$BRANCH" | head -1 || true) -if [ -n "$TICKET" ]; then - COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") - if ! grep -qF -- "$TICKET" <<<"$COMMIT_MSG"; then - printf '[%s] %s\n' "$TICKET" "$COMMIT_MSG" > "$COMMIT_MSG_FILE" - fi -fi - -LOCAL_HOOK="$(git rev-parse --show-toplevel)/.git-hooks/local-prepare-commit-msg" -if [ -x "$LOCAL_HOOK" ]; then - exec "$LOCAL_HOOK" "$@" -fi diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index 3608ce9..3f27d17 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -4,15 +4,25 @@ # # Install canonical ResQ git hooks into a repository. # -# Usage (local — from dev/): -# scripts/install-hooks.sh [target_dir] -# -# Usage (curl-piped — from any repo): +# Usage (curl-piped): # cd /path/to/repo # curl -fsSL https://raw.githubusercontent.com/resq-software/dev/main/scripts/install-hooks.sh | sh # +# Usage (local): +# scripts/install-hooks.sh [target_dir] +# +# Canonical hook content is owned by resq-software/crates (the resq-cli crate +# that also powers `resq pre-commit`). This installer picks the best path: +# +# 1. `resq` on PATH → `resq dev install-hooks` scaffolds from the embedded +# templates in the binary (offline, versioned with the +# user's installed resq). +# 2. Fallback → fetch templates from +# resq-software/crates/master/crates/resq-cli/templates/git-hooks +# via raw.githubusercontent.com. +# # Env: -# RESQ_DEV_REF — git ref for raw hook fetch (default: main) +# RESQ_CRATES_REF — git ref for raw fallback (default: master) # GIT_HOOKS_SKIP — set to skip installation entirely # YES=1 — auto-accept the local-hook scaffold prompt # RESQ_SKIP_LOCAL_SCAFFOLD — set to opt out of the local-hook prompt @@ -20,7 +30,7 @@ set -eu TARGET_DIR="${1:-$PWD}" -RESQ_DEV_REF="${RESQ_DEV_REF:-main}" +RESQ_CRATES_REF="${RESQ_CRATES_REF:-master}" if ! git -C "$TARGET_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then printf 'fail Not a git repository: %s\n' "$TARGET_DIR" >&2 @@ -30,24 +40,22 @@ TARGET_ROOT="$(git -C "$TARGET_DIR" rev-parse --show-toplevel)" HOOKS_DIR="$TARGET_ROOT/.git-hooks" mkdir -p "$HOOKS_DIR" -# Detect whether we're running alongside the source tree (local dev/) or curl-piped. -SCRIPT_DIR="" -if [ -n "${BASH_SOURCE:-}" ] && [ -f "${BASH_SOURCE:-}" ]; then - SCRIPT_DIR="$(cd "$(dirname "$BASH_SOURCE")" && pwd)" -elif [ -f "$0" ]; then - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# ── Resolve resq binary ───────────────────────────────────────────────────── +RESQ_BIN="" +if command -v resq >/dev/null 2>&1; then + RESQ_BIN="resq" +elif [ -x "$HOME/.cargo/bin/resq" ]; then + RESQ_BIN="$HOME/.cargo/bin/resq" fi -HOOKS="pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge" - -if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR/git-hooks" ]; then - printf 'info Installing hooks from %s\n' "$SCRIPT_DIR/git-hooks" >&2 - for h in $HOOKS; do - cp "$SCRIPT_DIR/git-hooks/$h" "$HOOKS_DIR/$h" - chmod +x "$HOOKS_DIR/$h" - done +# ── Path 1: use resq when present (preferred — offline, no raw fetch) ─────── +if [ -n "$RESQ_BIN" ]; then + printf 'info Installing hooks via %s dev install-hooks\n' "$RESQ_BIN" >&2 + (cd "$TARGET_ROOT" && "$RESQ_BIN" dev install-hooks) else - RAW_BASE="https://raw.githubusercontent.com/resq-software/dev/$RESQ_DEV_REF/scripts/git-hooks" + # ── Path 2: fall back to raw fetch from crates templates ──────────────── + HOOKS="pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge" + RAW_BASE="https://raw.githubusercontent.com/resq-software/crates/$RESQ_CRATES_REF/crates/resq-cli/templates/git-hooks" printf 'info Fetching hooks from %s\n' "$RAW_BASE" >&2 for h in $HOOKS; do if ! curl -fsSL "$RAW_BASE/$h" -o "$HOOKS_DIR/$h"; then @@ -56,32 +64,21 @@ else fi chmod +x "$HOOKS_DIR/$h" done + git -C "$TARGET_ROOT" config core.hooksPath .git-hooks fi -git -C "$TARGET_ROOT" config core.hooksPath .git-hooks - printf ' ok ResQ hooks installed in %s\n' "$HOOKS_DIR" >&2 printf ' Bypass once: git commit --no-verify\n' >&2 printf ' Disable all hooks: export GIT_HOOKS_SKIP=1\n' >&2 printf ' Add repo logic: %s/local-\n' "$HOOKS_DIR" >&2 -RESQ_BIN="" -if command -v resq >/dev/null 2>&1; then - RESQ_BIN="resq" -elif [ -x "$HOME/.cargo/bin/resq" ]; then - RESQ_BIN="$HOME/.cargo/bin/resq" -fi - if [ -z "$RESQ_BIN" ]; then printf 'warn resq backend not found. Hooks will soft-skip until you install it:\n' >&2 - printf ' nix develop (if your flake provides it)\n' >&2 - printf ' cargo install --git https://github.com/resq-software/crates resq-cli\n' >&2 + printf ' curl -fsSL https://raw.githubusercontent.com/resq-software/dev/main/scripts/install-resq.sh | sh\n' >&2 exit 0 fi -# Offer to scaffold a per-repo local-pre-push if none exists yet and resq -# supports the scaffold subcommand. Skipped silently when the resq binary -# pre-dates the subcommand or the user opts out. +# ── Local-hook scaffold prompt (only when resq supports it) ───────────────── if [ -f "$HOOKS_DIR/local-pre-push" ] || [ -n "${RESQ_SKIP_LOCAL_SCAFFOLD:-}" ]; then exit 0 fi diff --git a/tests/hooks/helpers.bash b/tests/hooks/helpers.bash index 8008d65..10b7e9d 100644 --- a/tests/hooks/helpers.bash +++ b/tests/hooks/helpers.bash @@ -1,19 +1,33 @@ # shellcheck shell=bash # Common helpers for bats tests over the canonical ResQ git hooks. # -# Each test gets a fresh tempdir initialized as a git repo with the canonical -# hooks copied in. core.hooksPath is set so `git` invokes the hooks naturally. +# Canonical hook content is owned by resq-software/crates (resq-cli embeds +# the same templates). This helper fetches them once per bats session from +# the crates repo via raw, caches them under /tmp, and copies them into +# each test's fresh repo. +# +# Override the source with RESQ_HOOK_SRC_DIR=/path/to/local/templates to +# test a local change before pushing it to crates. + +HOOK_SRC_CACHE="${RESQ_HOOK_SRC_DIR:-/tmp/resq-canonical-hooks}" +HOOK_RAW_BASE="${RESQ_HOOK_RAW_BASE:-https://raw.githubusercontent.com/resq-software/crates/master/crates/resq-cli/templates/git-hooks}" -# Absolute path to the canonical hook templates shipped by this repo. -HOOK_SRC="${BATS_TEST_DIRNAME}/../../scripts/git-hooks" +_ensure_hook_cache() { + [ -d "$HOOK_SRC_CACHE" ] && [ -e "$HOOK_SRC_CACHE/pre-commit" ] && return 0 + mkdir -p "$HOOK_SRC_CACHE" + for h in pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge; do + curl -fsSL "$HOOK_RAW_BASE/$h" -o "$HOOK_SRC_CACHE/$h" + done +} # Initialize a fresh git repo in $1 with canonical hooks installed. init_repo_with_hooks() { local dir="$1" + _ensure_hook_cache git -C "$dir" init -q git -C "$dir" -c user.email=t@t.io -c user.name=t commit --allow-empty -m "init" -q mkdir -p "$dir/.git-hooks" - cp "$HOOK_SRC"/{pre-commit,commit-msg,prepare-commit-msg,pre-push,post-checkout,post-merge} "$dir/.git-hooks/" + cp "$HOOK_SRC_CACHE"/{pre-commit,commit-msg,prepare-commit-msg,pre-push,post-checkout,post-merge} "$dir/.git-hooks/" chmod +x "$dir/.git-hooks"/* git -C "$dir" config core.hooksPath .git-hooks git -C "$dir" config user.email t@t.io