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
This commit is contained in:
2026-01-16 18:38:34 +01:00
parent 6b14131930
commit ea466bc4e2

View File

@@ -2,21 +2,51 @@
set -euo pipefail set -euo pipefail
# Usage: # Usage:
# check-history.sh <start_offset> <count> [--verbose] # check-history.sh <start_offset> <count> [OPTIONS]
# Example: # Example:
# check-history.sh 2 4 # checks commits 2,3,4,5 # check-history.sh 2 4 # checks commits 2,3,4,5
# check-history.sh 5 1 # checks only commit 5 # check-history.sh 5 1 -v # verbose mode
# check-history.sh 1 1 --verbose # verbose mode # check-history.sh 2 28 -j4 # check 28 commits, 4 at a time
VERBOSE=0 VERBOSE=0
if [ $# -lt 2 ] || [ $# -gt 3 ]; then JOBS=1
echo "Usage: $0 <start_offset_from_head> <count> [--verbose|-v]"
# Parse arguments
if [ $# -lt 2 ]; then
echo "Usage: $0 <start_offset_from_head> <count> [--verbose|-v] [-j|--jobs N]"
exit 1 exit 1
fi fi
if [ $# -eq 3 ] && { [ "$3" = "--verbose" ] || [ "$3" = "-v" ]; }; then START_ARG="$1"
COUNT_ARG="$2"
shift 2
# Parse optional flags
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=1 VERBOSE=1
fi 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 logging helper
debug() { debug() {
@@ -26,8 +56,8 @@ debug() {
} }
# Convert user-friendly 1-indexed "Nth from HEAD" into 0-indexed offset # Convert user-friendly 1-indexed "Nth from HEAD" into 0-indexed offset
START_OFFSET="$(( $1 - 1 ))" START_OFFSET="$(( START_ARG - 1 ))"
COUNT="$2" COUNT="$COUNT_ARG"
REPO_ROOT="$(git rev-parse --show-toplevel)" REPO_ROOT="$(git rev-parse --show-toplevel)"
TMP_BASE="/tmp/check-history-$$" TMP_BASE="/tmp/check-history-$$"
WORKTREES_DIR="$TMP_BASE/worktrees" WORKTREES_DIR="$TMP_BASE/worktrees"
@@ -40,7 +70,11 @@ debug "PNPM_STORE: $PNPM_STORE"
mkdir -p "$WORKTREES_DIR" "$LOGS_DIR" "$PNPM_STORE" mkdir -p "$WORKTREES_DIR" "$LOGS_DIR" "$PNPM_STORE"
echo "Checking commits from offset $1 (HEAD~$START_OFFSET) for $COUNT commits…" 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 echo
# Build list of commits as if by `git rebase -i HEAD~N` (HEAD=0 newest) as if by `git rebase -i HEAD~N` # Build list of commits as if by `git rebase -i HEAD~N` (HEAD=0 newest) as if by `git rebase -i HEAD~N`
@@ -153,7 +187,9 @@ build_frontend() {
# ----------------------------- # -----------------------------
cleanup() { cleanup() {
echo " echo "
Caught interrupt, cleaning worktrees…" >&2 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 git worktree prune >/dev/null 2>&1 || true
rm -rf "$TMP_BASE" >/dev/null 2>&1 || true rm -rf "$TMP_BASE" >/dev/null 2>&1 || true
exit 130 exit 130
@@ -215,36 +251,57 @@ check_commit() {
} }
# ----------------------------- # -----------------------------
# Sequential execution # Execution (parallel or sequential)
# ----------------------------- # -----------------------------
FAIL=0 FAIL=0
i=1
TOTAL=${#COMMITS[@]} TOTAL=${#COMMITS[@]}
for commit in "${COMMITS[@]}"; do
worktree="$WORKTREES_DIR/$commit"
log="$LOGS_DIR/$commit.log"
debug "===== Processing commit $i/$TOTAL =====" # Export functions and variables for parallel execution
debug "Commit: $commit" export -f check_commit build_frontend debug
debug "Worktree: $worktree" export VERBOSE REPO_ROOT PNPM_STORE WORKTREES_DIR LOGS_DIR
debug "Log file: $log"
echo "[ $i / $TOTAL ] Checking $commit" # Process a single commit (for xargs)
if ! check_commit "$commit" "$worktree" "$log"; then 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" echo "❌ FAILED at commit $commit"
sed 's/^/ /' "$log" 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 FAIL=1
break break
fi fi
debug "Commit $commit passed" done
sed 's/^/ /' "$log" else
# Parallel execution using xargs
debug "Starting parallel execution with $JOBS jobs"
debug "Removing worktree at $worktree" # Create input for xargs: "index commit"
git worktree remove --force "$worktree" >/dev/null 2>&1 for i in "${!COMMITS[@]}"; do
echo "$i ${COMMITS[$i]}"
((i++)) done | xargs -P "$JOBS" -n 2 bash -c 'process_commit "$@"' _ || FAIL=1
echo fi
done
# ----------------------------- # -----------------------------
# Final output # Final output