#!/usr/bin/env bash # upgrade-forks.sh — Upgrade local forks to latest upstream + reapply RB patches. # # For each fork in local/sources/: # 1. Fetch latest upstream (full depth for accurate merge base) # 2. Identify Red Bear commits (ahead of upstream merge-base) # 3. Save RB commits to a patch file # 4. Reset to upstream/master (or upstream/main) # 5. Reapply RB commits via cherry-pick (or patch fallback) # 6. Verify no upstream functions were dropped (verify-fork-functions.sh) # 7. Report success/failure per fork # # Usage: # ./local/scripts/upgrade-forks.sh # Upgrade all forks # ./local/scripts/upgrade-forks.sh kernel redoxfs # Upgrade specific forks # ./local/scripts/upgrade-forks.sh --dry-run # Show plan, don't execute # ./local/scripts/upgrade-forks.sh --force # Skip safety checks # ./local/scripts/upgrade-forks.sh --no-fetch # Use cached upstream refs (no network) # ./local/scripts/upgrade-forks.sh --force-reapply # Reapply even when 0 commits behind # # Safety: # - Requires clean working tree in each fork (no uncommitted changes) # - Creates backup branch before reset # - Stops on first conflict (interactive resolution or --abort) # - Post-upgrade: verifies all upstream functions are present # # Environment: # REDBEAR_UPGRADE_FORCE=1 Same as --force # REDBEAR_UPGRADE_NO_FETCH=1 Same as --no-fetch # REDBEAR_UPGRADE_FORCE_REAPPLY=1 Same as --force-reapply set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" DRY_RUN=0 FORCE=0 NO_FETCH=0 FORCE_REAPPLY=0 declare -a TARGET_FORKS=() while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1 ;; --force) FORCE=1 ;; --no-fetch) NO_FETCH=1 ;; --force-reapply) FORCE_REAPPLY=1 ;; -h|--help) echo "Usage: $0 [--dry-run] [--force] [--no-fetch] [--force-reapply] [fork1 fork2 ...]" echo "" echo "Upgrades local forks to latest upstream and reapplies Red Bear patches." echo "" echo "Options:" echo " --dry-run Show what would happen without executing" echo " --force Skip safety checks (dirty tree, etc.)" echo " --no-fetch Use cached upstream refs (no network required)" echo " --force-reapply Reapply even when 0 commits behind upstream" echo "" echo "Forks (default: all in local/sources/ with upstream remotes):" for d in "$PROJECT_ROOT"/local/sources/*/; do [[ -d "$d/.git" ]] && git -C "$d" remote get-url upstream >/dev/null 2>&1 && echo " $(basename "$d")" done 2>/dev/null || true exit 0 ;; --*) echo "Unknown: $1" >&2; exit 1 ;; *) TARGET_FORKS+=("$1") ;; esac shift done [[ "${REDBEAR_UPGRADE_FORCE:-0}" == "1" ]] && FORCE=1 [[ "${REDBEAR_UPGRADE_NO_FETCH:-0}" == "1" ]] && NO_FETCH=1 [[ "${REDBEAR_UPGRADE_FORCE_REAPPLY:-0}" == "1" ]] && FORCE_REAPPLY=1 cd "$PROJECT_ROOT" GREEN='\033[1;32m' RED='\033[1;31m' YELLOW='\033[1;33m' BLUE='\033[1;34m' NC='\033[0m' # Discover forks if not specified if [[ ${#TARGET_FORKS[@]} -eq 0 ]]; then while IFS= read -r line; do TARGET_FORKS+=("$line") done < <(for d in local/sources/*/; do [[ -d "$d/.git" ]] || continue name=$(basename "$d") git -C "$d" remote get-url upstream >/dev/null 2>&1 && echo "$name" done | sort) fi if [[ ${#TARGET_FORKS[@]} -eq 0 ]]; then echo -e "${RED}No forks with upstream remotes found.${NC}" >&2 exit 1 fi echo -e "${BLUE}Red Bear OS — Fork Upgrade${NC}" echo "" if [[ "$DRY_RUN" -eq 1 ]]; then echo -e "${YELLOW}DRY RUN — no changes will be made${NC}" echo "" fi SUCCESS_COUNT=0 FAIL_COUNT=0 SKIP_COUNT=0 declare -a FAILED_FORKS=() declare -a SUCCEEDED_FORKS=() for fork in "${TARGET_FORKS[@]}"; do fork_dir="local/sources/${fork}" echo -e "${BLUE}=== ${fork} ===${NC}" if [[ ! -d "$fork_dir/.git" ]]; then echo -e " ${YELLOW}SKIP: not a git repo${NC}" SKIP_COUNT=$((SKIP_COUNT + 1)) echo "" continue fi upstream_url=$(cd "$fork_dir" && git remote get-url upstream 2>/dev/null || true) if [[ -z "$upstream_url" ]]; then echo -e " ${YELLOW}SKIP: no upstream remote${NC}" SKIP_COUNT=$((SKIP_COUNT + 1)) echo "" continue fi # Safety: check working tree is clean if [[ "$FORCE" -eq 0 ]]; then dirty=$(cd "$fork_dir" && git status --porcelain 2>/dev/null | wc -l) if [[ "$dirty" -gt 0 ]]; then echo -e " ${RED}FAIL: working tree has $dirty uncommitted change(s)${NC}" echo -e " Commit or stash first, or use --force" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi fi # Determine upstream branch and ref upstream_branch="master" if [[ "$NO_FETCH" -eq 1 ]]; then # Use cached upstream refs — verify they exist if ! (cd "$fork_dir" && git rev-parse --verify upstream/master >/dev/null 2>&1); then if (cd "$fork_dir" && git rev-parse --verify upstream/main >/dev/null 2>&1); then upstream_branch="main" else echo -e " ${RED}FAIL: --no-fetch but no cached upstream/master or upstream/main${NC}" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi fi else if ! (cd "$fork_dir" && git fetch upstream --quiet 2>&1); then echo -e " ${RED}FAIL: git fetch upstream failed (network error?)${NC}" echo -e " ${YELLOW}Use --no-fetch if upstream refs are already cached${NC}" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi if ! (cd "$fork_dir" && git rev-parse --verify upstream/master >/dev/null 2>&1); then if (cd "$fork_dir" && git rev-parse --verify upstream/main >/dev/null 2>&1); then upstream_branch="main" else echo -e " ${RED}FAIL: cannot find upstream/master or upstream/main${NC}" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi fi fi upstream_ref="upstream/$upstream_branch" old_sha=$(cd "$fork_dir" && git rev-parse --short HEAD) upstream_sha=$(cd "$fork_dir" && git rev-parse --short "$upstream_ref") behind=$(cd "$fork_dir" && git log --oneline "HEAD..$upstream_ref" 2>/dev/null | wc -l) ahead=$(cd "$fork_dir" && git log --oneline "$upstream_ref..HEAD" 2>/dev/null | wc -l) echo " Current: $old_sha ($ahead commits ahead of upstream)" echo " Upstream: $upstream_sha ($behind commits behind)" if [[ "$behind" -eq 0 && "$FORCE_REAPPLY" -eq 0 ]]; then # Up to date in git history — but check if RB commits dropped upstream functions need_reapply=0 if [[ -x "$SCRIPT_DIR/verify-fork-functions.sh" ]]; then if ! "$SCRIPT_DIR/verify-fork-functions.sh" "$fork" --no-fetch --quiet 2>/dev/null; then need_reapply=1 echo -e " ${YELLOW}Upstream functions are MISSING (RB cherry-pick dropped code)${NC}" echo -e " ${YELLOW}Forcing reapply to restore missing functions...${NC}" fi fi if [[ "$need_reapply" -eq 0 ]]; then echo -e " ${GREEN}Already up to date${NC}" SKIP_COUNT=$((SKIP_COUNT + 1)) echo "" continue fi elif [[ "$behind" -eq 0 && "$FORCE_REAPPLY" -eq 1 ]]; then echo -e " ${YELLOW}--force-reapply: reapplying RB commits on clean upstream${NC}" fi if [[ "$ahead" -eq 0 ]]; then echo " No RB patches — fast-forward to upstream" if [[ "$DRY_RUN" -eq 0 ]]; then (cd "$fork_dir" && git merge --ff-only "$upstream_ref" 2>/dev/null) || { echo -e " ${RED}FAIL: fast-forward failed${NC}" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue } fi echo -e " ${GREEN}DONE: fast-forwarded to $upstream_sha${NC}" SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) SUCCEEDED_FORKS+=("$fork") echo "" continue fi # Has RB patches — need to rebase # Find merge base merge_base=$(cd "$fork_dir" && git merge-base HEAD "$upstream_ref" 2>/dev/null || echo "") if [[ -z "$merge_base" ]]; then echo -e " ${RED}FAIL: cannot find merge base (unrelated histories)${NC}" echo -e " ${YELLOW}This fork may need manual migration (snapshot import)${NC}" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi merge_base_short=$(cd "$fork_dir" && git rev-parse --short "$merge_base") echo " Merge base: $merge_base_short" echo " RB commits: $ahead" if [[ "$DRY_RUN" -eq 1 ]]; then echo -e " ${YELLOW}PLAN: reset to $upstream_sha, apply net RB diff${NC}" echo "" continue fi # Create backup branch backup_branch="rb-backup/${fork}-$(date +%Y%m%d-%H%M%S)-$$" (cd "$fork_dir" && git branch "$backup_branch" HEAD 2>/dev/null) echo " Backup: $backup_branch" # Save old HEAD for diff generation old_head=$(cd "$fork_dir" && git rev-parse HEAD) # Reset to upstream (cd "$fork_dir" && git reset --hard "$upstream_ref" 2>/dev/null) echo " Reset to: $upstream_sha" # Generate net diff of all RB changes as a single patch rb_patch="/tmp/rb-upgrade-${fork}-$$.patch" (cd "$fork_dir" && git diff "$upstream_ref" "$old_head" -- . ':!Cargo.lock' > "$rb_patch" 2>/dev/null) # Apply net RB diff if (cd "$fork_dir" && git apply --reject "$rb_patch" 2>/dev/null); then echo " Net diff: applied ($(wc -l < "$rb_patch") lines)" else # Net diff failed — try individual cherry-picks as fallback echo -e " ${YELLOW}Net diff had conflicts, falling back to cherry-pick...${NC}" (cd "$fork_dir" && git reset --hard "$upstream_ref" 2>/dev/null) mapfile -t rb_commits < <(cd "$fork_dir" && git log --reverse --format='%H' "${upstream_ref}..${old_head}" 2>/dev/null) pick_fail=0 for c in "${rb_commits[@]}"; do short=$(cd "$fork_dir" && git log -1 --format='%h %s' "$c") if (cd "$fork_dir" && git cherry-pick "$c" 2>/dev/null); then echo " ✓ $short" else echo -e " ${RED}✗ CONFLICT: $short${NC}" (cd "$fork_dir" && git cherry-pick --abort 2>/dev/null) || true pick_fail=1 break fi done if [[ "$pick_fail" -ne 0 ]]; then echo -e " ${RED}FAIL: both net-diff and cherry-pick failed, restored from backup${NC}" (cd "$fork_dir" && git reset --hard "$backup_branch" 2>/dev/null) rm -f "$rb_patch" FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue fi fi # Regenerate Cargo.lock (avoids cherry-pick conflicts on generated file) if (cd "$fork_dir" && [ -f Cargo.toml ] && cargo generate-lockfile 2>/dev/null); then echo " Lockfile: regenerated" fi # Stage all changes and commit as a single RB reapply (cd "$fork_dir" && git add -A 2>/dev/null) if ! (cd "$fork_dir" && git diff --cached --quiet 2>/dev/null); then (cd "$fork_dir" && git commit -m "rb: reapply Red Bear patches on upstream $upstream_sha" 2>/dev/null) echo " Committed: RB patches reapplied" fi rm -f "$rb_patch" new_sha=$(cd "$fork_dir" && git rev-parse --short HEAD) echo -e " ${GREEN}DONE: $old_sha → $new_sha ($ahead RB changes reapplied)${NC}" echo -e " ${YELLOW}Backup at $backup_branch${NC}" # Post-upgrade verification: check no upstream functions were dropped if [[ -x "$SCRIPT_DIR/verify-fork-functions.sh" ]]; then if ! "$SCRIPT_DIR/verify-fork-functions.sh" "$fork" --no-fetch --quiet 2>/dev/null; then echo -e " ${RED}VERIFICATION FAILED: upstream functions missing after upgrade!${NC}" "$SCRIPT_DIR/verify-fork-functions.sh" "$fork" --no-fetch 2>&1 | grep -E "^ " | head -20 echo -e " ${YELLOW}Restoring from backup — manual rebase needed${NC}" (cd "$fork_dir" && git reset --hard "$backup_branch" 2>/dev/null) FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_FORKS+=("$fork") echo "" continue else echo -e " ${GREEN}VERIFIED: all upstream functions present${NC}" fi fi SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) SUCCEEDED_FORKS+=("$fork") echo "" done echo "" echo -e "${BLUE}=== SUMMARY ===${NC}" echo -e " ${GREEN}Upgraded: $SUCCESS_COUNT${NC}" $([[ ${#SUCCEEDED_FORKS[@]} -gt 0 ]] && echo "(${SUCCEEDED_FORKS[*]})") echo -e " ${RED}Failed: $FAIL_COUNT${NC}" $([[ ${#FAILED_FORKS[@]} -gt 0 ]] && echo "(${FAILED_FORKS[*]})") echo " Skipped: $SKIP_COUNT" if [[ "$FAIL_COUNT" -gt 0 ]]; then exit 1 fi exit 0