#!/usr/bin/env bash
#
# The repo's own guards, before a commit rather than after it.
#
# ## Why this exists in the repository and not in .git/hooks
#
# `.git/hooks` is not versioned, so a hook living there protects exactly one
# checkout and silently protects nothing anywhere else. This directory is
# committed, and `core.hooksPath` points at it:
#
#   git config core.hooksPath .githooks
#
# That one line is the only setup, and it is in the README beside the test
# command.
#
# ## ADAPTED FOR THIS PROJECT
#
# The template's version runs `npx tsc --noEmit` and then `npx vitest run`. This
# project has neither: no TypeScript, no tsconfig, no test runner, and every
# source file is plain .jsx. Installed unchanged, that hook refuses every commit
# on a project where nothing is wrong.
#
# So it runs the two things that exist:
#
# 1. The secret scan, on the staged diff. This is the reason the hook earns its
#    place here at all. The Zoho WebToLead form identifiers were hardcoded in
#    index.html and later src/pages/Contact.jsx and reached four commits of a
#    then-PUBLIC repository before anyone noticed. A secret caught here costs a
#    `git reset`; the same secret caught after a push costs a rotation, because
#    deleting the line does not remove it from a commit that already exists.
#
# 2. `npm run build`, when source is staged. Three steps — client bundle, SSR
#    bundle, then prerender across every route — and it fails on a broken
#    import, a syntax error, or a page component that cannot render in Node.
#
# **The second is a build, not a test.** It proves the imports resolve. A form
# that posts to the wrong URL builds perfectly. Do not read a green hook as
# "the change works"; `docs/qa/ClaudeQACoverage.md` is honest about what is
# actually untested here, which is nearly everything.
#
# ## It warns about unstaged changes rather than failing on them
#
# Both commands run against the **working tree**, not against the index. So a
# clean run proves the working tree is good, which is only the same thing as the
# commit being good when nothing is left unstaged.
#
# That distinction matters in this checkout specifically: it is edited by more
# than one person at a time, and commits are staged by explicit path. A green
# hook beside three unstaged files has verified something other than what is
# about to be committed, and the honest thing is to say so rather than imply a
# guarantee that was not made.
#
# ## Escape hatch
#
#   SKIP_GUARDS=1 git commit ...     skips both, loudly
#   git commit --no-verify ...       skips the hook entirely, silently
#
# The first is preferred: it leaves a line in the terminal saying the guards did
# not run, which is the difference between a deliberate exception and a habit.

set -uo pipefail

cd "$(git rev-parse --show-toplevel)" || exit 1

say() { printf '\033[1mpre-commit:\033[0m %s\n' "$*" >&2; }

if [ -n "${SKIP_GUARDS:-}" ]; then
  say "SKIP_GUARDS set — typecheck and tests did NOT run for this commit."
  exit 0
fi

# Nothing staged is not this hook's problem; git will refuse on its own.
if git diff --cached --quiet; then
  exit 0
fi

# Only worth running when source or tests changed. A commit that touches docs or
# migrations alone still gets the typecheck, because a migration can be
# referenced from a test, but it should not wait on the whole suite.
#
# The four files after the `|` are not source, and they are here because of what
# `tests/version.test.ts` guards: package.json, the Dockerfile, and the two
# image pins in README.md and docker-compose.example.yml must all name the same
# version. With `src|tests` alone, a commit that hand-edits only the README pin
# — precisely the drift that left those pins six versions stale — would get the
# typecheck and skip the one test that would have caught it. The guard has to
# run on the commits it exists to police.
staged=$(git diff --cached --name-only)

# What is worth a build. src/ and server/ are the obvious ones; the other four
# are here because each can break the build on its own — index.html is the Vite
# entry, vite.config.js owns the alias resolution and the build inputs,
# package.json can remove a dependency the bundle imports, and prerender.js runs
# as the third build step against every route.
touches_code=$(printf '%s\n' "$staged" \
  | grep -cE '^(src|server|scripts)/|^(package\.json|index\.html|vite\.config\.js|tailwind\.config\.js|postcss\.config\.js)$' || true)

# Credentials, before the commit exists. FIRST, and cheap: it reads the staged
# diff only, and it runs on every commit including a docs-only one — a key
# pasted into a markdown file is still a key.
if [ -x scripts/secrets.sh ] || [ -f scripts/secrets.sh ]; then
  say "secrets…"
  if ! bash scripts/secrets.sh; then
    say "possible credential in the staged changes — commit refused."
    say "If it is real, ROTATE IT FIRST. Deleting the line does not remove it"
    say "from a commit that already exists. If it is not, --allow the path or"
    say "adjust the patterns in scripts/secrets.sh; do not silence the check."
    exit 1
  fi
else
  # Said out loud. A missing scanner and a clean scan look identical from here,
  # and this hook has exactly two jobs.
  say "WARNING: scripts/secrets.sh is missing — NOTHING was scanned for"
  say "         credentials. That is not a pass."
fi

if [ "$touches_code" -gt 0 ]; then
  say "build…"

  if ! command -v npm >/dev/null 2>&1; then
    say "npm is not on PATH, so the build did not run — commit refused."
    say "This hook will not pass a commit it could not check."
    exit 1
  fi

  if ! npm run build; then
    say "build failed — commit refused."
    exit 1
  fi
else
  say "no source staged — skipping the build."
fi

# Said last so it is the thing still on screen when the editor opens.
if ! git diff --quiet; then
  say "NOTE: unstaged changes are present. The guards ran against the working"
  say "      tree, so they did not verify this commit in isolation."
fi

exit 0
