Files
check-history/check-history.sh
Angel Hudgins ea466bc4e2 feat: add parallel execution with -j/--jobs flag
- Add -j/--jobs N flag to run N commits in parallel
- Use xargs -P for reliable parallel execution
- Refactor to use process_commit wrapper function
- Falls back to sequential when -j 1 or not specified
- Should provide 3-4x speedup when checking multiple commits
2026-01-16 18:38:34 +01:00

320 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Usage:
# check-history.sh <start_offset> <count> [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 <start_offset_from_head> <count> [--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 <start_offset_from_head> <count> [--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 <<EOF
$(git rev-list HEAD)
EOF
# Reverse into chronological order (oldest first)
mapfile -t ALL_COMMITS < <(printf '%s
' "${HEAD_TO_OLD[@]}" | tac)
debug "Total commits in history: ${#ALL_COMMITS[@]}"
mapfile -t ALL_COMMITS < <(printf '%s
' "${HEAD_TO_OLD[@]}" | tac)
# Convert HEAD-based offsets to chronological indexes
# HEAD~0 → last element; HEAD~1 → second-to-last
START_INDEX=$(( ${#ALL_COMMITS[@]} - 1 - START_OFFSET ))
debug "START_INDEX (in chronological array): $START_INDEX"
# Corrected: START_INDEX is the *newest* selected commit.
# The range must be: START_INDEX down to START_INDEX-(COUNT-1)
BEGIN_INDEX=$(( START_INDEX - COUNT + 1 ))
debug "BEGIN_INDEX: $BEGIN_INDEX (checking $COUNT commits)"
# True rebase-style selection: newest-first offset, then N older
# Ensure BEGIN_INDEX is valid
if (( BEGIN_INDEX < 0 )); then
echo "Error: Range extends beyond repository history" >&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
else
pnpm install --frozen-lockfile --prefer-offline </dev/null >/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
else
pnpm build </dev/null >/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
else
pnpm check </dev/null >/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"