diff --git a/scripts/pre-commit-repl-check b/scripts/pre-commit-repl-check new file mode 100755 index 0000000..0169c01 --- /dev/null +++ b/scripts/pre-commit-repl-check @@ -0,0 +1,93 @@ +#!/bin/bash +# Pre-commit hook: verify all defuns in staged .org files compile in the daemon. +# For each changed .org file, it tangles to .lisp then sends the entire file +# to the daemon for compilation. This catches undefined symbol references, +# syntax errors, and broken function bodies. +# +# Install: +# ln -sf ../../scripts/pre-commit-repl-check .git/hooks/pre-commit +# +# Requires: running daemon on port 9105, repl script, emacs with ob-tangle. +# +# Returns 0 (pass) or 1 (fail). + +set -euo pipefail +IFS=$'\n\t' + +REPL=$(command -v repl 2>/dev/null || echo "/home/user/.opencode/bin/repl") +PORT=9105 +PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo "/home/user/memex/projects/passepartout") + +# Check daemon connectivity +if ! timeout 2 bash -c "echo >/dev/tcp/127.0.0.1/$PORT" 2>/dev/null; then + echo "ERROR: Daemon not reachable on 127.0.0.1:$PORT. Start it first." >&2 + exit 1 +fi + +# Collect changed .org files from the index +CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep '\.org$' || true) +if [ -z "$CHANGED" ]; then + exit 0 +fi + +FAILED=0 +for orgfile in $CHANGED; do + [ -f "$orgfile" ] || continue + + # Determine the tangle target from the org file's PROPERTY line + TANGLE=$(grep 'header-args.*:tangle' "$orgfile" | sed "s/.*:tangle //" | head -1) + if [ -z "$TANGLE" ]; then + echo "SKIP: $orgfile — no :tangle header" >&2 + continue + fi + + # Resolve relative tangle path + ORG_DIR=$(dirname "$orgfile") + LISP_FILE=$(cd "$ORG_DIR" && realpath -m "$TANGLE" 2>/dev/null || echo "$ORG_DIR/$TANGLE") + + # Tangle the org file to lisp + if ! emacs --batch -L "$PROJECT_DIR" --eval "(require 'ob-tangle)" \ + --eval "(org-babel-tangle-file \"$ORG_DIR/$(basename "$orgfile")\")" \ + 2>/dev/null; then + echo "FAIL: $orgfile — tangling failed" >&2 + FAILED=1 + continue + fi + + if [ ! -f "$LISP_FILE" ]; then + echo "SKIP: $orgfile — tangle target $LISP_FILE not found" >&2 + continue + fi + + # Compile the lisp file in the daemon. + # We send a Lisp form that compiles the file and returns T or an error string. + # Using format to avoid backquote/comma issues. + LISP_ABS=$(realpath "$LISP_FILE") + CODE=$(cat <<-LISPEOF + (let ((*standard-output* (make-broadcast-stream)) + (*error-output* (make-broadcast-stream))) + (handler-case + (progn + (compile-file "$LISP_ABS") + (load (compile-file-pathname "$LISP_ABS")) + (format nil "OK")) + (error (c) + (format nil "COMPILE-ERROR: ~a" c)))) +LISPEOF +) + RESULT=$(printf '%s' "$CODE" | timeout 10 "$REPL" 2>/dev/null || echo "DAEMON-UNREACHABLE") + + if echo "$RESULT" | grep -q '^COMPILE-ERROR:\|^DAEMON-UNREACHABLE\|^$'; then + echo "REJECT: $(basename "$orgfile") — compilation failed: $RESULT" >&2 + FAILED=1 + else + echo "OK: $(basename "$orgfile")" >&2 + fi +done + +if [ "$FAILED" -eq 1 ]; then + echo "" >&2 + echo "COMMIT REJECTED: REPL compilation check failed." >&2 + echo "Fix errors, or bypass with: git commit --no-verify" >&2 + exit 1 +fi