mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
354 lines
13 KiB
Bash
Executable file
354 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ── Re-merge parallel branches via chronological cherry-pick ────────
|
|
#
|
|
# Instead of merging 44 branches (which causes massive conflicts on
|
|
# hotspot files), this script cherry-picks every individual commit
|
|
# in the order it was originally authored. Since each commit is a
|
|
# small, focused refactoring change, conflicts are minimal (~2.5%)
|
|
# and easy to resolve.
|
|
#
|
|
# Usage:
|
|
# ./scripts/remerge-parallel.sh # dry-run (default)
|
|
# ./scripts/remerge-parallel.sh --execute # actually do it
|
|
# ./scripts/remerge-parallel.sh --continue # resume after fixing a conflict
|
|
# ./scripts/remerge-parallel.sh --status # show progress
|
|
#
|
|
# Prerequisites:
|
|
# - On the pulse/v6-release branch
|
|
# - All parallel branches exist locally
|
|
|
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$REPO_DIR"
|
|
|
|
PRE_MERGE_BASE="27bf6ab7" # v6 before any parallel merges
|
|
STATE_DIR="$REPO_DIR/scripts/.remerge"
|
|
TIMELINE_FILE="$STATE_DIR/timeline.txt"
|
|
PROGRESS_FILE="$STATE_DIR/progress"
|
|
LOG_FILE="$STATE_DIR/remerge.log"
|
|
BUILD_ERRORS="$STATE_DIR/build-errors.txt"
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
log() {
|
|
local msg="[$(date '+%H:%M:%S')] $*"
|
|
echo "$msg"
|
|
echo "$msg" >> "$LOG_FILE"
|
|
}
|
|
|
|
check_build() {
|
|
log " Checking build..."
|
|
if go build ./... 2>"$BUILD_ERRORS"; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
get_progress() {
|
|
if [ -f "$PROGRESS_FILE" ]; then
|
|
cat "$PROGRESS_FILE"
|
|
else
|
|
echo "0"
|
|
fi
|
|
}
|
|
|
|
save_progress() {
|
|
echo "$1" > "$PROGRESS_FILE"
|
|
}
|
|
|
|
# ── Build the timeline ───────────────────────────────────────────────
|
|
|
|
build_timeline() {
|
|
log "Building chronological timeline of all commits..."
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Collect every commit from every parallel branch, sorted by author date
|
|
> "$TIMELINE_FILE"
|
|
for b in $(git branch --list '*parallel*' | sed 's/^[+* ]*//'); do
|
|
git log --format="%ai|%H|$b|%s" "$PRE_MERGE_BASE".."$b" 2>/dev/null
|
|
done | sort >> "$TIMELINE_FILE"
|
|
|
|
local total
|
|
total=$(wc -l < "$TIMELINE_FILE" | tr -d ' ')
|
|
log "Timeline built: $total commits from $(git branch --list '*parallel*' | wc -l | tr -d ' ') branches"
|
|
}
|
|
|
|
# ── Mode parsing ─────────────────────────────────────────────────────
|
|
|
|
MODE="dry-run"
|
|
BUILD_GATE="final" # "every" = build after each commit, "batch" = every 25, "final" = end only
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--execute) MODE="execute"; shift ;;
|
|
--continue) MODE="continue"; shift ;;
|
|
--dry-run) MODE="dry-run"; shift ;;
|
|
--status) MODE="status"; shift ;;
|
|
--build-every) BUILD_GATE="every"; shift ;;
|
|
--build-batch) BUILD_GATE="batch"; shift ;;
|
|
--build-final) BUILD_GATE="final"; shift ;;
|
|
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# ── Status ───────────────────────────────────────────────────────────
|
|
|
|
if [ "$MODE" = "status" ]; then
|
|
if [ ! -f "$TIMELINE_FILE" ]; then
|
|
echo "No re-merge in progress."
|
|
exit 0
|
|
fi
|
|
total=$(wc -l < "$TIMELINE_FILE" | tr -d ' ')
|
|
done=$(get_progress)
|
|
remaining=$((total - done))
|
|
echo "Re-merge progress: $done / $total commits ($remaining remaining)"
|
|
if [ "$done" -gt 0 ] && [ "$done" -lt "$total" ]; then
|
|
next_line=$(sed -n "$((done + 1))p" "$TIMELINE_FILE")
|
|
echo "Next: $(echo "$next_line" | cut -d'|' -f4)"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ── Dry run ──────────────────────────────────────────────────────────
|
|
|
|
if [ "$MODE" = "dry-run" ]; then
|
|
# Build a temporary timeline for display
|
|
tmpfile=$(mktemp)
|
|
for b in $(git branch --list '*parallel*' | sed 's/^[+* ]*//'); do
|
|
git log --format="%ai|%H|$b|%s" "$PRE_MERGE_BASE".."$b" 2>/dev/null
|
|
done | sort > "$tmpfile"
|
|
|
|
total=$(wc -l < "$tmpfile" | tr -d ' ')
|
|
branches=$(git branch --list '*parallel*' | wc -l | tr -d ' ')
|
|
|
|
# Count potential conflicts (adjacent commits from different branches touching same files)
|
|
prev_branch=""
|
|
prev_hash=""
|
|
conflict_count=0
|
|
while IFS='|' read -r ts hash branch msg; do
|
|
if [ -n "$prev_hash" ] && [ "$branch" != "$prev_branch" ]; then
|
|
overlap=$(comm -12 \
|
|
<(git diff --name-only "${hash}^" "$hash" 2>/dev/null | sort) \
|
|
<(git diff --name-only "${prev_hash}^" "$prev_hash" 2>/dev/null | sort) \
|
|
| wc -l | tr -d ' ')
|
|
[ "$overlap" -gt 0 ] && conflict_count=$((conflict_count + 1))
|
|
fi
|
|
prev_branch="$branch"
|
|
prev_hash="$hash"
|
|
done < "$tmpfile"
|
|
|
|
echo "╔══════════════════════════════════════════════════════════╗"
|
|
echo "║ Chronological Cherry-Pick Re-merge (DRY RUN) ║"
|
|
echo "╠══════════════════════════════════════════════════════════╣"
|
|
echo "║ Pre-merge base: $PRE_MERGE_BASE"
|
|
echo "║ Branches: $branches"
|
|
echo "║ Total commits: $total"
|
|
echo "║ Potential conflicts: ~$conflict_count ($(echo "scale=1; $conflict_count * 100 / $total" | bc)%)"
|
|
echo "║ Strategy: cherry-pick in author-date order"
|
|
echo "╚══════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo "First 10 commits:"
|
|
head -10 "$tmpfile" | while IFS='|' read -r ts hash branch msg; do
|
|
echo " $(echo "$ts" | cut -d' ' -f1,2) $(git log --oneline -1 "$hash") [$(echo "$branch" | sed 's|refactor/parallel-||')]"
|
|
done
|
|
echo " ..."
|
|
echo ""
|
|
echo "Last 5 commits:"
|
|
tail -5 "$tmpfile" | while IFS='|' read -r ts hash branch msg; do
|
|
echo " $(echo "$ts" | cut -d' ' -f1,2) $(git log --oneline -1 "$hash") [$(echo "$branch" | sed 's|refactor/parallel-||')]"
|
|
done
|
|
echo ""
|
|
echo "Build gate options (default: --build-final):"
|
|
echo " --build-every Check build after each commit (slowest, safest)"
|
|
echo " --build-batch Check build every 25 commits"
|
|
echo " --build-final Check build only at the end (fastest)"
|
|
echo ""
|
|
echo "To execute: $0 --execute [--build-batch]"
|
|
echo "To resume: $0 --continue"
|
|
|
|
rm -f "$tmpfile"
|
|
exit 0
|
|
fi
|
|
|
|
# ── Execute mode ─────────────────────────────────────────────────────
|
|
|
|
if [ "$MODE" = "execute" ]; then
|
|
current_branch=$(git branch --show-current)
|
|
if [ "$current_branch" != "pulse/v6-release" ]; then
|
|
echo "Error: must be on pulse/v6-release branch (currently on: $current_branch)"
|
|
exit 1
|
|
fi
|
|
|
|
if ! git diff --quiet HEAD 2>/dev/null; then
|
|
echo ""
|
|
echo "WARNING: You have uncommitted changes in the working tree."
|
|
read -rp "Continue? [y/N] " confirm
|
|
[ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || exit 1
|
|
fi
|
|
|
|
# Build the timeline
|
|
build_timeline
|
|
total=$(wc -l < "$TIMELINE_FILE" | tr -d ' ')
|
|
|
|
echo ""
|
|
echo "This will reset pulse/v6-release to $PRE_MERGE_BASE and cherry-pick $total commits."
|
|
echo "Current HEAD: $(git log --oneline -1 HEAD)"
|
|
echo ""
|
|
read -rp "Type 'yes' to proceed: " confirm
|
|
[ "$confirm" = "yes" ] || { echo "Aborted."; exit 1; }
|
|
|
|
# Save backup
|
|
git branch -f pulse/v6-release-pre-remerge-backup HEAD
|
|
log "Backup saved: pulse/v6-release-pre-remerge-backup -> $(git rev-parse --short HEAD)"
|
|
|
|
# Reset
|
|
git reset --hard "$PRE_MERGE_BASE"
|
|
log "Reset pulse/v6-release to $PRE_MERGE_BASE"
|
|
|
|
save_progress 0
|
|
fi
|
|
|
|
# ── Cherry-pick loop (used by both --execute and --continue) ─────────
|
|
|
|
if [ ! -f "$TIMELINE_FILE" ]; then
|
|
echo "Error: no timeline found. Run with --execute first."
|
|
exit 1
|
|
fi
|
|
|
|
total=$(wc -l < "$TIMELINE_FILE" | tr -d ' ')
|
|
start_idx=$(get_progress)
|
|
|
|
if [ "$MODE" = "continue" ]; then
|
|
# Check if there's a cherry-pick in progress that needs finishing
|
|
if [ -d "$REPO_DIR/.git/sequencer" ] || [ -f "$REPO_DIR/.git/CHERRY_PICK_HEAD" ]; then
|
|
echo "There's an unfinished cherry-pick. Please resolve it first:"
|
|
echo " git add <resolved files>"
|
|
echo " git cherry-pick --continue"
|
|
echo "Then run: $0 --continue"
|
|
exit 1
|
|
fi
|
|
|
|
# The progress file points to the commit that failed.
|
|
# If we're continuing, the user has resolved it, so advance.
|
|
start_idx=$((start_idx + 1))
|
|
save_progress "$start_idx"
|
|
log "Resuming from commit $((start_idx + 1))/$total"
|
|
fi
|
|
|
|
succeeded=0
|
|
failed=0
|
|
skipped=0
|
|
|
|
for (( i=start_idx; i<total; i++ )); do
|
|
line=$(sed -n "$((i + 1))p" "$TIMELINE_FILE")
|
|
IFS='|' read -r ts hash branch msg <<< "$line"
|
|
|
|
short=$(git log --oneline -1 "$hash" 2>/dev/null || echo "$hash")
|
|
pass_name=$(echo "$branch" | sed 's|refactor/parallel-||')
|
|
|
|
printf "\r[%d/%d] %s" $((i + 1)) "$total" "$short"
|
|
|
|
# Skip if this commit is already an ancestor (e.g., from a resumed run)
|
|
if git merge-base --is-ancestor "$hash" HEAD 2>/dev/null; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
|
|
# Cherry-pick
|
|
if git cherry-pick --no-commit "$hash" 2>>"$LOG_FILE"; then
|
|
git commit --no-edit -m "$msg" --author="$(git log --format='%an <%ae>' -1 "$hash")" 2>>"$LOG_FILE" || true
|
|
succeeded=$((succeeded + 1))
|
|
else
|
|
# Conflict — check if it's trivially resolvable (empty diff = already applied)
|
|
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
|
|
# No actual changes — skip this commit (content already present)
|
|
git cherry-pick --abort 2>/dev/null || git reset --hard HEAD 2>/dev/null
|
|
skipped=$((skipped + 1))
|
|
save_progress "$i"
|
|
continue
|
|
fi
|
|
|
|
echo ""
|
|
log "CONFLICT at commit $((i + 1))/$total: $short [$pass_name]"
|
|
echo ""
|
|
echo " Conflicted files:"
|
|
git diff --name-only --diff-filter=U 2>/dev/null | sed 's/^/ /'
|
|
echo ""
|
|
echo " To resolve:"
|
|
echo " 1. Fix the conflicts in the files above"
|
|
echo " 2. git add <resolved files>"
|
|
echo " 3. git cherry-pick --continue"
|
|
echo " 4. $0 --continue"
|
|
echo ""
|
|
echo " To skip this commit:"
|
|
echo " git cherry-pick --abort"
|
|
echo " $0 --continue"
|
|
echo ""
|
|
save_progress "$i"
|
|
failed=$((failed + 1))
|
|
|
|
echo "Progress: $succeeded applied, $skipped skipped, $failed conflicts, $((total - i - 1)) remaining"
|
|
exit 1
|
|
fi
|
|
|
|
save_progress "$i"
|
|
|
|
# Build gates
|
|
if [ "$BUILD_GATE" = "every" ]; then
|
|
if ! check_build; then
|
|
echo ""
|
|
log "BUILD FAILED after commit $((i + 1))/$total: $short"
|
|
cat "$BUILD_ERRORS"
|
|
echo ""
|
|
echo "Fix the build, commit the fix, then: $0 --continue"
|
|
exit 1
|
|
fi
|
|
elif [ "$BUILD_GATE" = "batch" ] && [ $(( (i + 1) % 25 )) -eq 0 ]; then
|
|
echo ""
|
|
log "Build check at commit $((i + 1))/$total..."
|
|
if ! check_build; then
|
|
log "BUILD FAILED at batch checkpoint $((i + 1))/$total"
|
|
cat "$BUILD_ERRORS"
|
|
echo ""
|
|
echo "Fix the build, commit the fix, then: $0 --continue"
|
|
exit 1
|
|
fi
|
|
log "Build: PASS ($((i + 1))/$total)"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo ""
|
|
|
|
# Final build check
|
|
log "Running final build check..."
|
|
if check_build; then
|
|
log "Final build: PASS"
|
|
else
|
|
log "Final build: FAIL"
|
|
echo ""
|
|
echo "All commits applied but the build has errors:"
|
|
cat "$BUILD_ERRORS"
|
|
echo ""
|
|
echo "Fix the build issues and commit the fixes."
|
|
exit 1
|
|
fi
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────────
|
|
|
|
log ""
|
|
log "╔══════════════════════════════════════════════════════════╗"
|
|
log "║ Re-merge Complete! ║"
|
|
log "╠══════════════════════════════════════════════════════════╣"
|
|
log "║ Commits applied: $succeeded"
|
|
log "║ Commits skipped: $skipped"
|
|
log "║ Final build: PASS"
|
|
log "║ HEAD: $(git log --oneline -1 HEAD)"
|
|
log "╚══════════════════════════════════════════════════════════╝"
|
|
log ""
|
|
log "Backup of old v6: pulse/v6-release-pre-remerge-backup"
|
|
log "To delete: git branch -D pulse/v6-release-pre-remerge-backup"
|
|
|
|
# Clean up state
|
|
rm -rf "$STATE_DIR"
|