#!/usr/bin/env bash set -euo pipefail # Usage: # check-history.sh [OPTIONS] # Example: # check-history.sh 2 4 # checks commits 2,3,4,5 # check-history.sh 5 1 -v # verbose mode # check-history.sh 2 28 -j4 # check 28 commits, 4 at a time VERBOSE=0 JOBS=1 # Parse arguments if [ $# -lt 2 ]; then echo "Usage: $0 [--verbose|-v] [-j|--jobs N]" exit 1 fi START_ARG="$1" COUNT_ARG="$2" shift 2 # Parse optional flags while [ $# -gt 0 ]; do case "$1" in -v|--verbose) VERBOSE=1 shift ;; -j|--jobs) if [ $# -lt 2 ]; then echo "Error: -j/--jobs requires a number" >&2 exit 1 fi JOBS="$2" if ! [[ "$JOBS" =~ ^[0-9]+$ ]] || [ "$JOBS" -lt 1 ]; then echo "Error: -j/--jobs must be a positive integer" >&2 exit 1 fi shift 2 ;; *) echo "Error: Unknown option $1" >&2 echo "Usage: $0 [--verbose|-v] [-j|--jobs N]" exit 1 ;; esac done # Debug logging helper debug() { if [ "$VERBOSE" -eq 1 ]; then echo "[DEBUG] $*" >&2 fi } # Convert user-friendly 1-indexed "Nth from HEAD" into 0-indexed offset START_OFFSET="$(( START_ARG - 1 ))" COUNT="$COUNT_ARG" REPO_ROOT="$(git rev-parse --show-toplevel)" TMP_BASE="/tmp/check-history-$$" WORKTREES_DIR="$TMP_BASE/worktrees" LOGS_DIR="$TMP_BASE/logs" PNPM_STORE="$HOME/.cache/check-history/pnpm-store" debug "REPO_ROOT: $REPO_ROOT" debug "TMP_BASE: $TMP_BASE" debug "PNPM_STORE: $PNPM_STORE" mkdir -p "$WORKTREES_DIR" "$LOGS_DIR" "$PNPM_STORE" if [ "$JOBS" -gt 1 ]; then echo "Checking commits from offset $START_ARG (HEAD~$START_OFFSET) for $COUNT commits with $JOBS parallel jobs…" else echo "Checking commits from offset $START_ARG (HEAD~$START_OFFSET) for $COUNT commits…" fi echo # Build list of commits as if by `git rebase -i HEAD~N` (HEAD=0 newest) as if by `git rebase -i HEAD~N` # HEAD = index 0, parent = index 1, etc. mapfile -t HEAD_TO_OLD <&2 exit 1 fi # Compute slice from BEGIN_INDEX to START_INDEX inclusive COMMITS=("${ALL_COMMITS[@]:$BEGIN_INDEX:$COUNT}") debug "Selected commits:" for c in "${COMMITS[@]}"; do debug " $c" done # ----------------------------- # Frontend builder # ----------------------------- build_frontend() { ( [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Entering rfc-edge-frontend directory" || true cd rfc-edge-frontend || { echo "❌ Failed to cd into rfc-edge-frontend" return 1 } [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] cd succeeded, PWD=$PWD" || true export PNPM_HOME="$PWD/.pnpm_home" export PNPM_STORE_PATH="$PNPM_STORE" export PNPM_HARD_LINKS=false # Copy .env file from main repo if it exists and isn't present in worktree if [ ! -f .env ] && [ -f "$REPO_ROOT/rfc-edge-frontend/.env" ]; then [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Copying .env file from main repo" || true cp "$REPO_ROOT/rfc-edge-frontend/.env" .env elif [ -f .env ]; then [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] .env file already exists in worktree" || true else [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] No .env file found in main repo" || true fi [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Environment variables set:" || true [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] PNPM_HOME=$PNPM_HOME" || true [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] PNPM_STORE_PATH=$PNPM_STORE_PATH" || true mkdir -p "$PNPM_HOME" "$PNPM_STORE_PATH" [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Created pnpm directories" || true [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Running pnpm install..." || true if [ "$VERBOSE" -eq 1 ]; then pnpm install --frozen-lockfile --prefer-offline /dev/null 2>&1 fi || { echo "❌ pnpm install failed" return 1 } [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] pnpm install succeeded" || true [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Running pnpm build..." || true if [ "$VERBOSE" -eq 1 ]; then pnpm build /dev/null 2>&1 fi || { echo "❌ pnpm build failed" return 1 } [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] pnpm build succeeded" || true [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Running pnpm check..." || true if [ "$VERBOSE" -eq 1 ]; then pnpm check /dev/null 2>&1 fi || { echo "❌ pnpm check failed" return 1 } [ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] pnpm check succeeded" || true ) } # ----------------------------- # Ctrl-C cleanup # ----------------------------- cleanup() { echo " Caught interrupt, killing background jobs and cleaning worktrees…" >&2 # Kill all background jobs jobs -p | xargs -r kill 2>/dev/null || true git worktree prune >/dev/null 2>&1 || true rm -rf "$TMP_BASE" >/dev/null 2>&1 || true exit 130 } trap cleanup INT # ----------------------------- # Commit checker # ----------------------------- check_commit() { local commit="$1" local worktree="$2" local log="$3" [ "$VERBOSE" -eq 1 ] && debug "check_commit() called for $commit" || true [ "$VERBOSE" -eq 1 ] && debug " worktree: $worktree" || true [ "$VERBOSE" -eq 1 ] && debug " log: $log" || true ( set -euo pipefail echo "===== COMMIT $commit =====" [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Adding worktree at $worktree" || true git worktree add --force --detach "$worktree" "$commit" >/dev/null [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Worktree created, changing directory" || true cd "$worktree" [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] In worktree, PWD=$PWD" || true if [ -d rfc-edge-frontend ]; then [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Found rfc-edge-frontend directory" || true echo "→ Building frontend…" if ! build_frontend; then echo "❌ Frontend build failed" exit 1 fi [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Frontend build completed successfully" || true else [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] No rfc-edge-frontend directory found, skipping frontend build" || true fi # Check if Rust files changed in this commit if git diff --name-only "${commit}^..${commit}" 2>/dev/null | grep -qE '\.(rs|toml)$|Cargo\.lock'; then [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Rust files changed, running cargo check" || true echo "→ Running cargo check…" if ! cargo check --quiet; then echo "❌ Cargo check failed" exit 1 fi [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Cargo check completed successfully" || true else [ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] No Rust files changed, skipping cargo check" || true echo "→ Skipping cargo check (no Rust changes)" fi echo "✓ OK" ) >"$log" 2>&1 return $? } # ----------------------------- # Execution (parallel or sequential) # ----------------------------- FAIL=0 TOTAL=${#COMMITS[@]} # Export functions and variables for parallel execution export -f check_commit build_frontend debug export VERBOSE REPO_ROOT PNPM_STORE WORKTREES_DIR LOGS_DIR # Process a single commit (for xargs) process_commit() { local idx="$1" local commit="$2" local worktree="$WORKTREES_DIR/$commit" local log="$LOGS_DIR/$commit.log" echo "[ $((idx + 1)) / $TOTAL ] Checking $commit…" if check_commit "$commit" "$worktree" "$log"; then echo "✓ [ $((idx + 1)) / $TOTAL ] $commit passed" sed 's/^/ /' "$log" git worktree remove --force "$worktree" >/dev/null 2>&1 || true echo return 0 else echo "❌ FAILED at commit $commit" sed 's/^/ /' "$log" echo return 1 fi } export -f process_commit export TOTAL if [ "$JOBS" -eq 1 ]; then # Sequential execution for i in "${!COMMITS[@]}"; do if ! process_commit "$i" "${COMMITS[$i]}"; then FAIL=1 break fi done else # Parallel execution using xargs debug "Starting parallel execution with $JOBS jobs" # Create input for xargs: "index commit" for i in "${!COMMITS[@]}"; do echo "$i ${COMMITS[$i]}" done | xargs -P "$JOBS" -n 2 bash -c 'process_commit "$@"' _ || FAIL=1 fi # ----------------------------- # Final output # ----------------------------- if [ "$FAIL" -eq 1 ]; then echo "❌ Stopped early due to failure." debug "Cleaning up: removing $TMP_BASE" else echo "🎉 All commits passed!" debug "All commits verified successfully" debug "Cleaning up: removing $TMP_BASE" fi rm -rf "$TMP_BASE" >/dev/null 2>&1 debug "Cleanup complete"