Euler source The code that runs the solver shell and this site, version 4.34.2 Repository on GitHub

Generated by scripts/setup/githooks.sh — do not edit directly.

221 lines of Bash Raw file Open on GitHub

#!/usr/bin/env bash
# Generated by scripts/setup/githooks.sh — do not edit directly.
# Source of truth: scripts/setup/hooks/pre-commit.template
#
# Output contract: every check announces itself and the hook always ends with
# a single PASS/FAIL verdict naming exactly which checks failed, so a buried
# error can't be masked by a later tool's "Success" line.
set -uo pipefail

# Resolve the working tree at runtime so the hook operates on the checkout
# actually being committed — including linked worktrees. A hardcoded path would
# point the sed/git-add fixer at the wrong tree and silently drop new files.
# VENV stays absolute: the virtualenv lives only in the main checkout.
REPO_ROOT="$(git rev-parse --show-toplevel)"
VENV="__VENV__"

cd "$REPO_ROOT" || exit 1

# ── Reporting helpers ────────────────────────────────────────────────────
FAILURES=()

section() { printf '\n── %s ──\n' "$1" >&2; }
note()    { printf '   %s\n' "$1" >&2; }
fail()    { FAILURES+=("$1"); printf '   ✗ %s\n' "$1" >&2; }
pass()    { printf '   ✓ %s\n' "$1" >&2; }

print_verdict() {
    printf '\n' >&2
    if (( ${#FAILURES[@]} == 0 )); then
        printf '✓ pre-commit: all checks passed.\n' >&2
        return 0
    fi
    printf '════════════════════════════════════════════════════════════\n' >&2
    printf '✗ pre-commit BLOCKED — %d check(s) failed:\n' "${#FAILURES[@]}" >&2
    local f
    for f in "${FAILURES[@]}"; do
        printf '    • %s\n' "$f" >&2
    done
    printf '  (scroll up to the matching "── ... ──" section for details)\n' >&2
    printf '════════════════════════════════════════════════════════════\n' >&2
    return 1
}

# ── trailing-whitespace / conflict markers: check only, no auto-fix ──────
# Every staged file is checked, solutions/private included. Those blobs are
# gitfilter ciphertext (solver.crypto.gitfilter) which contains NUL bytes, so
# git classifies them as binary and skips the whitespace check automatically —
# no exclude needed, and no risk of flagging random ciphertext bytes.
section "trailing whitespace"
if git diff --check --cached --no-color; then
    pass "no trailing-whitespace / conflict markers in staged changes"
else
    fail "staged changes contain trailing whitespace or conflict markers"
fi

# ── compiled binaries: drop staged build artifacts ───────────────────────
# The C runner compiles each solution to an executable (p<NNNN>_s<K>_c, no
# extension); these are gitignored build artifacts regenerated on demand and
# must never enter a commit. Inspect each staged blob (not the worktree file)
# and unstage any that begins with the ELF magic (7f 45 4c 46).
section "compiled binaries"
removed_bins=()
while IFS= read -r f; do
    [[ -z "$f" ]] && continue
    head_hex=$(git cat-file -p ":$f" 2>/dev/null | head -c4 | xxd -p)
    if [[ "$head_hex" == "7f454c46" ]]; then
        git rm --cached --quiet -- "$f"
        removed_bins+=("$f")
    fi
done < <(git diff --cached --name-only --diff-filter=d)
if (( ${#removed_bins[@]} > 0 )); then
    note "unstaged compiled binaries (excluded from this commit):"
    for b in "${removed_bins[@]}"; do note "  $b"; done
    pass "compiled binaries removed from the index"
else
    pass "no compiled binaries staged"
fi

# ── gitignored files: block staged paths matched by .gitignore ───────────
# `git check-ignore` reports a path as NOT ignored once it is in the index, so
# --no-index is required to evaluate the staged set against the ignore rules
# directly. That also drops git's "tracked files are never ignored" rule, so a
# tracked file matching a pattern would be a false positive — but .gitignore is
# kept correct (its `!.gitignore` / `!.gitattributes` negations follow every
# broad dot rule, so no tracked file matches), which is why the whole staged
# set can be checked. Compiled binaries were already dropped above; this catches
# anything else gitignored (other build artifacts, .env, logs, …) force-added.
section "gitignored files"
staged=$(git diff --cached --name-only --diff-filter=d)
ignored=""
if [[ -n "$staged" ]]; then
    ignored=$(printf '%s\n' "$staged" | git check-ignore --no-index --stdin 2>/dev/null || true)
fi
if [[ -z "$ignored" ]]; then
    pass "no gitignored files staged"
else
    note "staged files are matched by .gitignore:"
    while IFS= read -r g; do note "  $g"; done <<< "$ignored"
    fail "gitignored files are staged — unstage them (or commit deliberately)"
fi

# ── audit/: gitignored whenever it exists ─────────────────────────────────
# audit/ holds the run logs (scripts/ops/capture-run.sh, release.sh) and review material
# that name collaborators and the host's layout. It is safe only while .gitignore covers
# it, so check the rule itself (--no-index), not just what happens to be staged — and
# that nothing under it is already tracked.
section "audit/ folder (gitignored)"
if [[ ! -e audit ]]; then
    pass "no audit/ folder in this checkout"
elif ! git check-ignore --no-index -q audit/; then
    fail "audit/ exists but is not gitignored — restore '/audit/' in .gitignore"
elif [[ -n "$(git ls-files -- audit)" ]]; then
    fail "files under audit/ are tracked — 'git rm -r --cached audit' before committing"
else
    pass "audit/ is gitignored"
fi

# ── encryption filter: staged private blobs must be ciphertext ───────────
# The commit-side twin of pre-push's encryption-at-rest audit: every STAGED
# blob under solutions/private must begin with the gitfilter MAGIC. With the
# clean filter wired and working, staging plaintext produces ciphertext, so
# this passes untouched; a blob lacking the magic means the filter is unwired
# or broken in this clone — the exact state that would commit (and later push)
# private plaintext to the public repo. Ciphertext staged with the filter
# unwired (an unauthorized per-user clone moving files) is fine: the property
# gated is the blob's content, not the wiring.
section "encryption filter (solutions/private)"
magic_hex=$("$VENV/bin/python" -c 'from solver.crypto import wire; print(wire.MAGIC.hex())' 2>/dev/null)
if [[ -z "$magic_hex" ]]; then
    fail "could not read the gitfilter magic (is the venv intact?)"
else
    mlen=$(( ${#magic_hex} / 2 ))
    plain_private=()
    staged_private=0
    while IFS= read -r f; do
        [[ -z "$f" ]] && continue
        staged_private=$(( staged_private + 1 ))
        head_hex=$(git cat-file -p ":$f" 2>/dev/null | head -c "$mlen" | xxd -p)
        [[ "$head_hex" == "$magic_hex" ]] || plain_private+=("$f")
    done < <(git diff --cached --name-only --diff-filter=d -- solutions/private)
    if (( staged_private == 0 )); then
        pass "no private files staged"
    elif (( ${#plain_private[@]} == 0 )); then
        pass "$staged_private staged private blob(s), all ciphertext (magic=0x$magic_hex)"
    else
        note "staged PLAINTEXT under solutions/private:"
        for p in "${plain_private[@]}"; do note "  !! $p"; done
        note "the encryption filter is not wired/working in this clone. Fix it, then re-stage:"
        note "  solver shell:  git-filter install    (verifies key access first)"
        note "  or directly:   $VENV/bin/python -m solver.crypto.gitfilter install"
        note "  then:          git add --renormalize -- solutions/private"
        fail "plaintext private files staged — the gitfilter is not working"
    fi
fi

# ── shellcheck: staged shell scripts ─────────────────────────────────────
# Scoped to what you staged, unlike the flake8/mypy sweeps below: a shell script is
# checked on its own with no cross-file resolution, so sweeping the tree here buys
# nothing — pre-push does that as the backstop, the same commit-side/push-side pairing
# the audits above use. The hook templates are shell too (they only become
# .git/hooks/* once githooks.sh substitutes __VENV__), so they are linted with the
# same rules as the scripts that render them.
#
# Like flake8/mypy, this reads the WORKTREE file rather than the staged blob, so a
# finding names a real path and line you can jump to. shellcheck is a base apt package
# (scripts/setup/dev_env.sh) — a missing binary is a broken install, not an opt-out.
#
# No options here: they live in .shellcheckrc at the repo root, so this lane, pre-push's
# whole-tree lane, scripts/linters/check.sh and your editor all judge a script the same
# way. `external-sources` there is what keeps THIS lane honest — without it, a script
# would pass or fail on whether the file it sources happened to be staged alongside it.
section "shellcheck (staged shell scripts)"
mapfile -t staged_sh < <(git diff --cached --name-only --diff-filter=d -- '*.sh' '*.template')
if (( ${#staged_sh[@]} == 0 )); then
    pass "no shell scripts staged"
elif ! command -v shellcheck > /dev/null 2>&1; then
    note "install it with: make install-system"
    fail "shellcheck is not installed"
elif shellcheck -- "${staged_sh[@]}"; then
    pass "shellcheck clean (${#staged_sh[@]} script(s))"
else
    fail "shellcheck reported issues"
fi

# ── flake8: packages + solutions ───────────────────────────────────────────
# The whole solutions tree (public + decrypted private) is a proper package —
# every directory carries an __init__.py — so module resolution is unambiguous
# and the lint scope is simply `solutions packages`.
section "flake8 (solutions, packages)"
if "$VENV/bin/flake8" solutions packages; then
    pass "flake8 clean"
else
    fail "flake8 reported issues"
fi

# ── mypy: packages + solutions + scripts + tests ───────────────────────────
# `scripts` is in scope because it holds tracked Python that ships and runs:
# scripts/setup/euler-smtp.py is installed to /usr/local/bin and run as a service.
# Type-checking the shell half of the tree (shellcheck, above) while leaving that
# unchecked was the gap. `tests` is in scope so a refactor that changes a signature is
# caught in the tests that still call the old one, before the suite has to run.
section "mypy (solutions, packages, scripts, tests)"
if "$VENV/bin/mypy" solutions packages scripts tests; then
    pass "mypy clean"
else
    fail "mypy reported type errors"
fi

# ── layering: the solver shell must not import the web tier ──────────────
# docs/package-guide.md § 1. Known couplings sit in scripts/linters/layers.allow,
# which only ratchets down: a new import fails, and so does a stale allowlist line.
section "layering (webshell → site → solver)"
if "$VENV/bin/python" scripts/linters/layers.py .; then
    pass "no import of a higher layer"
else
    fail "a package imports a layer above it (or layers.allow has a stale line)"
fi

# ── Verdict ──────────────────────────────────────────────────────────────
print_verdict
exit $?