7e7ef73a8c
The script cd's into the fork dir for the clean-tree check (line ~202) and never returns to PROJECT_ROOT. Every subsequent relative 'cd "$fork_dir"' subshell then fails with 'No such file or directory', making single-fork and multi-fork runs always FAIL at the upstream-ref resolution step. Make fork_dir absolute at assignment so all subshell cd/git calls work regardless of current directory. Found while attempting the base fork sync (verify-fork-functions gate).
453 lines
19 KiB
Bash
Executable File
453 lines
19 KiB
Bash
Executable File
#!/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, or refs/tags/<tag> with --to)
|
|
# 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 --to=0.5.12 kernel # Pin to a specific tag
|
|
# ./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
|
|
# ./local/scripts/upgrade-forks.sh --force-diverged # Allow running on diverged-mode forks
|
|
#
|
|
# --to=<X.Y.Z> (release-bump-pipeline.md Deliverable B):
|
|
# After fetching upstream, pin upstream_ref to refs/tags/<tag>. The script
|
|
# tries refs/tags/<tag> first, then refs/tags/v<tag>, resolving via
|
|
# `git rev-parse`. When --to is given, the master/main default-branch
|
|
# logic is skipped entirely.
|
|
#
|
|
# Diverged-mode guard (release-bump-pipeline.md Deliverable B):
|
|
# At the top of each fork's iteration, the script looks up the fork's mode
|
|
# (4th column of local/fork-upstream-map.toml). If mode is `diverged` and
|
|
# neither --force nor --force-diverged is given, the script REFUSES with
|
|
# exit 1 and a message explaining that diverged rebases are operator-manual
|
|
# per policy (Phase 2.4+). A missing map entry is a warning, not a refusal.
|
|
#
|
|
# Safety:
|
|
# - Requires clean working tree in each fork (no uncommitted changes)
|
|
# - Creates backup branch (rb-backup/<fork>-<ts>-<pid>) 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
|
|
# REDBEAR_UPGRADE_FORCE_DIVERGED=1 Same as --force-diverged
|
|
|
|
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
|
|
FORCE_DIVERGED=0
|
|
PIN_TO_TAG=""
|
|
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 ;;
|
|
--force-diverged) FORCE_DIVERGED=1 ;;
|
|
--to=*)
|
|
PIN_TO_TAG="${1#*=}"
|
|
if [[ ! "$PIN_TO_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "ERROR: --to requires an anchored semver X.Y.Z (got: $PIN_TO_TAG)" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
-h|--help)
|
|
echo "Usage: $0 [--dry-run] [--force] [--no-fetch] [--force-reapply]"
|
|
echo " [--to=<X.Y.Z>] [--force-diverged] [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 " --to=<X.Y.Z> Pin upstream_ref to refs/tags/<tag> (or refs/tags/v<tag>)"
|
|
echo " instead of upstream/master or upstream/main."
|
|
echo " Resolved via git rev-parse. Skips default-branch logic."
|
|
echo " --force-diverged Allow running on forks whose map mode is 'diverged'."
|
|
echo " Without this (or --force), diverged forks are refused"
|
|
echo " because their rebase is operator-manual (Phase 2.4+)."
|
|
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
|
|
[[ "${REDBEAR_UPGRADE_FORCE_DIVERGED:-0}" == "1" ]] && FORCE_DIVERGED=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="$PROJECT_ROOT/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
|
|
|
|
# ---- Diverged-mode guard (release-bump-pipeline.md Deliverable B) ----
|
|
# Look up the fork's mode (4th column of local/fork-upstream-map.toml).
|
|
# If mode is `diverged` and neither --force nor --force-diverged is given,
|
|
# REFUSE: diverged rebases are operator-manual per policy (Phase 2.4+).
|
|
# A missing map entry is a warning, not a refusal.
|
|
fork_mode=""
|
|
if [[ -f "$PROJECT_ROOT/local/fork-upstream-map.toml" ]]; then
|
|
fork_mode=$(awk -v want="$fork" '
|
|
$1 == want && /^[^#[:space:]]/ {
|
|
mode = $4
|
|
sub(/[ \t]*#.*/, "", mode)
|
|
print mode
|
|
exit
|
|
}
|
|
' "$PROJECT_ROOT/local/fork-upstream-map.toml" 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "$fork_mode" ]]; then
|
|
echo -e " ${YELLOW}WARN: '$fork' not found in local/fork-upstream-map.toml${NC}"
|
|
echo -e " ${YELLOW} (proceeding without diverged-mode check)${NC}"
|
|
elif [[ "$fork_mode" == "diverged" && "$FORCE" -eq 0 && "$FORCE_DIVERGED" -eq 0 ]]; then
|
|
echo -e " ${RED}REFUSE: '$fork' is mode=diverged in the upstream map${NC}"
|
|
echo -e " ${YELLOW}Diverged forks track upstream via merge commits and cannot be${NC}"
|
|
echo -e " ${YELLOW}auto-rebased by this script. Their rebase is operator-manual${NC}"
|
|
echo -e " ${YELLOW}(Phase 2.4+ work). Override with --force-diverged (or --force).${NC}"
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
FAILED_FORKS+=("$fork")
|
|
echo ""
|
|
continue
|
|
elif [[ "$fork_mode" == "diverged" ]]; then
|
|
echo -e " ${YELLOW}WARN: '$fork' is mode=diverged; proceeding due to --force-diverged${NC}"
|
|
fi
|
|
|
|
# Safety: check working tree is clean
|
|
if [[ "$FORCE" -eq 0 ]]; then
|
|
if ! cd "$fork_dir" 2>/dev/null; then
|
|
echo -e " ${RED}FAIL: cannot enter fork dir $fork_dir${NC}"
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
FAILED_FORKS+=("$fork")
|
|
echo ""
|
|
continue
|
|
fi
|
|
dirty=$(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 [[ -n "$PIN_TO_TAG" ]]; then
|
|
# --to=<X.Y.Z>: pin upstream_ref to refs/tags/<tag>, skipping the
|
|
# default master/main logic entirely. Try refs/tags/<tag> first,
|
|
# then refs/tags/v<tag> (some upstreams prefix tags with `v`).
|
|
# Resolve via `git rev-parse` so we end up with a concrete SHA-bearing ref.
|
|
if [[ "$NO_FETCH" -eq 0 ]]; then
|
|
if ! (cd "$fork_dir" && git fetch upstream --quiet --tags 2>&1); then
|
|
echo -e " ${RED}FAIL: git fetch upstream --tags 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
|
|
fi
|
|
pinned_ref=""
|
|
for candidate in "refs/tags/${PIN_TO_TAG}" "refs/tags/v${PIN_TO_TAG}"; do
|
|
if (cd "$fork_dir" && git rev-parse --verify "$candidate" >/dev/null 2>&1); then
|
|
pinned_ref="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "$pinned_ref" ]]; then
|
|
echo -e " ${RED}FAIL: --to=${PIN_TO_TAG}: neither refs/tags/${PIN_TO_TAG}${NC}"
|
|
echo -e " ${RED} nor refs/tags/v${PIN_TO_TAG} exists after fetch${NC}"
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
FAILED_FORKS+=("$fork")
|
|
echo ""
|
|
continue
|
|
fi
|
|
upstream_ref="$pinned_ref"
|
|
elif [[ "$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
|
|
upstream_ref="upstream/$upstream_branch"
|
|
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
|
|
upstream_ref="upstream/$upstream_branch"
|
|
fi
|
|
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
|