Files
settled-reach/.config/hooks/pre-push
T
jpmschweitzerandClaude Opus 4.6 db3daecfaf chore(config): add JSON syntax validation to pre-push hook
Validates changed JSON files using python3 -m json.tool (zero
dependencies). Same diff-based scoping as existing checks — only
files changed vs remote are validated. Blocks push on syntax errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:00:29 +02:00

149 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pre-push hook: lint GDScript and Rust before pushing.
# Installed via: git config core.hooksPath .config/hooks
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
ERRORS=0
echo "pre-push: running lint checks..."
# --- Detect which directories have changes vs remote ---
BRANCH=$(git branch --show-current)
REMOTE_REF="origin/$BRANCH"
if git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then
CLIENT_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- client/ 2>/dev/null | wc -l)
SERVER_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- server/ 2>/dev/null | wc -l)
TOOLING_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- tooling/ pyproject.toml 2>/dev/null | wc -l)
else
# New branch or no remote ref — fall through to directory checks
CLIENT_CHANGED=1
SERVER_CHANGED=1
TOOLING_CHANGED=1
fi
# --- GDScript parse check (headless Godot) ---
GODOT="${GODOT:-godot}"
if [ "$CLIENT_CHANGED" -eq 0 ]; then
echo "pre-push: no client/ changes — skipping GDScript checks"
elif command -v "$GODOT" >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/.godot" ]; then
echo "pre-push: checking GDScript (parse)..."
SCRIPT_ERRORS=$("$GODOT" --headless --path "$REPO_ROOT/client" --quit 2>&1 | grep -ci "SCRIPT ERROR" || true)
if [ "$SCRIPT_ERRORS" -gt 0 ]; then
echo "pre-push: FAIL — $SCRIPT_ERRORS GDScript error(s) found"
"$GODOT" --headless --path "$REPO_ROOT/client" --quit 2>&1 | grep -i "SCRIPT ERROR"
ERRORS=$((ERRORS + 1))
else
echo "pre-push: GDScript parse — OK"
fi
else
echo "pre-push: skipping GDScript parse (no .godot/ import — run Godot once to enable)"
fi
# --- GDScript lint (gdlint static analysis) — advisory only until codebase is clean ---
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdlint >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
echo "pre-push: checking GDScript (gdlint — advisory)..."
LINT_COUNT=$(gdlint "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "Error:" || true)
if [ "$LINT_COUNT" -gt 0 ]; then
echo "pre-push: gdlint — $LINT_COUNT issue(s) (advisory, not blocking)"
else
echo "pre-push: gdlint — OK"
fi
fi
# --- GDScript format check (gdformat) — advisory only until codebase is clean ---
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdformat >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
echo "pre-push: checking GDScript (gdformat — advisory)..."
FORMAT_COUNT=$(gdformat --check "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "would reformat" || true)
if [ "$FORMAT_COUNT" -gt 0 ]; then
echo "pre-push: gdformat — $FORMAT_COUNT file(s) need formatting (advisory, not blocking)"
else
echo "pre-push: gdformat — OK"
fi
fi
# --- Rust lint (clippy + fmt) ---
if [ "$SERVER_CHANGED" -eq 0 ]; then
echo "pre-push: no server/ changes — skipping Rust checks"
elif command -v cargo >/dev/null 2>&1 && [ -d "$REPO_ROOT/server" ]; then
# fmt only needs source files — always safe to run
echo "pre-push: checking Rust (fmt)..."
if ! (cd "$REPO_ROOT/server" && cargo fmt --check 2>&1); then
ERRORS=$((ERRORS + 1))
else
echo "pre-push: fmt — OK"
fi
# clippy needs a build — skip if target/ doesn't exist (cold worktree)
if [ -d "$REPO_ROOT/server/target" ]; then
echo "pre-push: checking Rust (clippy)..."
if ! (cd "$REPO_ROOT/server" && cargo clippy -- -D warnings 2>&1); then
ERRORS=$((ERRORS + 1))
else
echo "pre-push: clippy — OK"
fi
else
echo "pre-push: skipping clippy (no target/ — run 'cargo build' once to enable)"
fi
# --- Rust dependency audit (cargo deny) — requires deny.toml config ---
if command -v cargo-deny >/dev/null 2>&1 && [ -f "$REPO_ROOT/server/deny.toml" ]; then
echo "pre-push: checking Rust (cargo deny)..."
if ! (cd "$REPO_ROOT/server" && cargo deny check 2>&1); then
ERRORS=$((ERRORS + 1))
else
echo "pre-push: cargo deny — OK"
fi
fi
else
echo "pre-push: WARNING — cargo not found or server/ missing, skipping Rust lint"
fi
# --- Python lint (ruff) ---
if [ "$TOOLING_CHANGED" -eq 0 ]; then
echo "pre-push: no tooling/ changes — skipping Python lint"
elif command -v ruff >/dev/null 2>&1 && [ -d "$REPO_ROOT/tooling" ]; then
echo "pre-push: checking Python (ruff)..."
if ! (cd "$REPO_ROOT" && ruff check tooling/ 2>&1); then
ERRORS=$((ERRORS + 1))
else
echo "pre-push: ruff — OK"
fi
else
echo "pre-push: skipping Python lint (ruff not found — install with: pip install 'ruff>=0.9')"
fi
# --- JSON syntax validation ---
if git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then
JSON_FILES=$(git diff --name-only "$REMOTE_REF"..HEAD -- '*.json' 2>/dev/null || true)
else
JSON_FILES=$(git ls-files '*.json')
fi
if [ -n "$JSON_FILES" ]; then
echo "pre-push: checking JSON syntax..."
JSON_FAIL=0
while IFS= read -r f; do
if [ -f "$REPO_ROOT/$f" ] && ! python3 -m json.tool "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo " FAIL: $f"
JSON_FAIL=$((JSON_FAIL + 1))
fi
done <<< "$JSON_FILES"
if [ "$JSON_FAIL" -gt 0 ]; then
echo "pre-push: FAIL — $JSON_FAIL JSON file(s) have syntax errors"
ERRORS=$((ERRORS + 1))
else
echo "pre-push: JSON — OK ($(echo "$JSON_FILES" | wc -l) file(s))"
fi
else
echo "pre-push: no JSON changes — skipping"
fi
if [ "$ERRORS" -gt 0 ]; then
echo ""
echo "pre-push: $ERRORS check(s) failed. Push aborted."
echo " Fix the errors above, then try again."
exit 1
fi
echo "pre-push: all checks passed."