- Add --frontend and --backend flags for custom directory paths - Auto-detect common frontend directory patterns if not specified - Default backend to repository root if not specified - Replace hardcoded 'rfc-edge-frontend' with dynamic variables - Support any project structure with pnpm frontend and Rust backend Examples: check-history.sh 2 4 # auto-detect check-history.sh 2 4 --frontend web --backend . # custom dirs check-history.sh 2 4 --backend server -j8 # no frontend 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
385 lines
13 KiB
Bash
Executable File
385 lines
13 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
|
|
# check-history.sh 2 4 --frontend web --backend . # custom directories
|
|
|
|
VERBOSE=0
|
|
JOBS=1
|
|
FRONTEND_DIR=""
|
|
BACKEND_DIR=""
|
|
|
|
# Parse arguments
|
|
if [ $# -lt 2 ]; then
|
|
echo "Usage: $0 <start_offset_from_head> <count> [OPTIONS]"
|
|
echo "Options:"
|
|
echo " -v, --verbose Enable verbose debug logging"
|
|
echo " -j, --jobs N Run N jobs in parallel"
|
|
echo " --frontend DIR Frontend directory to build (default: auto-detect)"
|
|
echo " --backend DIR Backend directory to check (default: repository root)"
|
|
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
|
|
;;
|
|
--frontend)
|
|
if [ $# -lt 2 ]; then
|
|
echo "Error: --frontend requires a directory path" >&2
|
|
exit 1
|
|
fi
|
|
FRONTEND_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--backend)
|
|
if [ $# -lt 2 ]; then
|
|
echo "Error: --backend requires a directory path" >&2
|
|
exit 1
|
|
fi
|
|
BACKEND_DIR="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
echo "Error: Unknown option $1" >&2
|
|
echo "Usage: $0 <start_offset_from_head> <count> [OPTIONS]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Debug logging helper
|
|
debug() {
|
|
if [ "$VERBOSE" -eq 1 ]; then
|
|
echo "[DEBUG] $*" >&2
|
|
fi
|
|
}
|
|
|
|
# Auto-detect frontend directory if not specified
|
|
if [ -z "$FRONTEND_DIR" ]; then
|
|
# Try common frontend directory patterns
|
|
for dir in "frontend" "web" "client" "ui" "app" "*-frontend"; do
|
|
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
|
FRONTEND_DIR="$dir"
|
|
debug "Auto-detected frontend directory: $FRONTEND_DIR"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Default backend to repository root if not specified
|
|
if [ -z "$BACKEND_DIR" ]; then
|
|
BACKEND_DIR="."
|
|
debug "Using repository root as backend directory"
|
|
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() {
|
|
local frontend_dir="$1"
|
|
|
|
(
|
|
[ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Entering $frontend_dir directory" || true
|
|
cd "$frontend_dir" || {
|
|
echo "❌ Failed to cd into $frontend_dir"
|
|
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/$frontend_dir/.env" ]; then
|
|
[ "$VERBOSE" -eq 1 ] && echo "[BUILD_DEBUG] Copying .env file from main repo" || true
|
|
cp "$REPO_ROOT/$frontend_dir/.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
|
|
|
|
# Build frontend if specified
|
|
if [ -n "$FRONTEND_DIR" ]; then
|
|
if [ -d "$FRONTEND_DIR" ]; then
|
|
[ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] Found $FRONTEND_DIR directory" || true
|
|
echo "→ Building frontend…"
|
|
if ! build_frontend "$FRONTEND_DIR"; 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] Frontend directory $FRONTEND_DIR not found, skipping" || true
|
|
fi
|
|
else
|
|
[ "$VERBOSE" -eq 1 ] && echo "[CHECK_DEBUG] No frontend directory specified, skipping frontend build" || true
|
|
fi
|
|
|
|
# Check backend if Rust/Cargo files changed in this commit
|
|
if [ -n "$BACKEND_DIR" ]; then
|
|
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 in $BACKEND_DIR" || true
|
|
echo "→ Running cargo check…"
|
|
cd "$BACKEND_DIR" || {
|
|
echo "❌ Failed to cd into $BACKEND_DIR"
|
|
exit 1
|
|
}
|
|
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
|
|
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 FRONTEND_DIR BACKEND_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 - stop on first failure
|
|
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"
|
|
# Use || true to continue even if jobs fail, then check exit codes
|
|
for i in "${!COMMITS[@]}"; do
|
|
echo "$i ${COMMITS[$i]}"
|
|
done | xargs -P "$JOBS" -n 2 bash -c 'process_commit "$@" || exit 0' _
|
|
|
|
# Check if any commits failed by looking for failure markers in logs
|
|
for i in "${!COMMITS[@]}"; do
|
|
commit="${COMMITS[$i]}"
|
|
log="$LOGS_DIR/$commit.log"
|
|
if [ -f "$log" ] && grep -q "❌" "$log"; then
|
|
FAIL=1
|
|
fi
|
|
done
|
|
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"
|
|
|