From 4e88d1915f7ac9af0c46aa7cac4a92c17e0935bf Mon Sep 17 00:00:00 2001 From: vasilito Date: Sat, 11 Jul 2026 11:20:29 +0300 Subject: [PATCH] build system: add upstream drift detection and fork upgrade tooling - check-fork-drift.sh: compares each submodule HEAD against upstream/master, reports behind/ahead counts, exits 1 if any fork exceeds threshold - upgrade-forks.sh: fetches latest upstream, saves RB commits, resets to upstream, cherry-picks RB patches with conflict handling and backups - build-preflight.sh: wired drift check into preflight (threshold=50, suppressible via REDBEAR_SKIP_DRIFT_CHECK=1) - bootloader submodule pointer updated to latest upstream rebase --- local/scripts/build-preflight.sh | 12 ++ local/scripts/check-fork-drift.sh | 165 +++++++++++++++++++ local/scripts/upgrade-forks.sh | 262 ++++++++++++++++++++++++++++++ local/sources/bootloader | 2 +- 4 files changed, 440 insertions(+), 1 deletion(-) create mode 100755 local/scripts/check-fork-drift.sh create mode 100755 local/scripts/upgrade-forks.sh diff --git a/local/scripts/build-preflight.sh b/local/scripts/build-preflight.sh index 3397c7467b..674c1e4020 100755 --- a/local/scripts/build-preflight.sh +++ b/local/scripts/build-preflight.sh @@ -57,6 +57,18 @@ if [ -x "$SCRIPT_DIR/verify-fork-versions.sh" ]; then fi fi +if [ -x "$SCRIPT_DIR/check-fork-drift.sh" ] && [ "${REDBEAR_SKIP_DRIFT_CHECK:-0}" != "1" ]; then + if ! "$SCRIPT_DIR/check-fork-drift.sh" --no-fetch --threshold=50 >/tmp/fork-drift.out 2>&1; then + echo ">>> WARNING: Upstream drift detected in local forks:" >&2 + grep -E 'DRIFT' /tmp/fork-drift.out >&2 || cat /tmp/fork-drift.out >&2 + echo ">>> Run ./local/scripts/check-fork-drift.sh for full report." >&2 + echo ">>> Run ./local/scripts/upgrade-forks.sh to upgrade." >&2 + echo ">>> Set REDBEAR_SKIP_DRIFT_CHECK=1 to suppress this warning." >&2 + else + echo ">>> Preflight: fork drift check passed." + fi +fi + if [ -n "$RELEASE" ]; then bash "$SCRIPT_DIR/build-release-mode.sh" --release="$RELEASE" --config="$CONFIG" "${EXTRA_PACKAGES[@]/#/--extra-package=}" fi diff --git a/local/scripts/check-fork-drift.sh b/local/scripts/check-fork-drift.sh new file mode 100755 index 0000000000..9c6f9366cc --- /dev/null +++ b/local/scripts/check-fork-drift.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# check-fork-drift.sh — Detect upstream drift in Red Bear OS local forks. +# +# For each submodule in local/sources/, fetches upstream and reports: +# - Commits behind upstream (new upstream commits we don't have) +# - Commits ahead of upstream (our Red Bear patches) +# - Upstream HEAD date vs our HEAD date +# +# Exits 1 if any fork is behind upstream beyond the threshold. +# +# Usage: +# ./local/scripts/check-fork-drift.sh # Report all +# ./local/scripts/check-fork-drift.sh --threshold=20 # Fail if any fork >20 behind +# ./local/scripts/check-fork-drift.sh --fetch-depth=100 # Deeper fetch (default: 200) +# ./local/scripts/check-fork-drift.sh --no-fetch # Skip network fetch (use cached refs) +# ./local/scripts/check-fork-drift.sh --json Machine-readable output +# ./local/scripts/check-fork-drift.sh --quiet Only print violations + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +THRESHOLD=10 +FETCH_DEPTH=200 +NO_FETCH=0 +JSON=0 +QUIET=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --threshold=*) THRESHOLD="${1#*=}" ;; + --fetch-depth=*) FETCH_DEPTH="${1#*=}" ;; + --no-fetch) NO_FETCH=1 ;; + --json) JSON=1 ;; + --quiet) QUIET=1 ;; + -h|--help) + echo "Usage: $0 [--threshold=N] [--fetch-depth=N] [--no-fetch] [--json] [--quiet]" + exit 0 + ;; + *) echo "Unknown: $1" >&2; exit 1 ;; + esac + shift +done + +cd "$PROJECT_ROOT" + +# Submodules that have upstream remotes +declare -a FORKS=() +declare -A FORK_PATHS +while IFS= read -r line; do + name=$(basename "$line") + FORKS+=("$name") + FORK_PATHS["$name"]="$line" +done < <(find local/sources -maxdepth 1 -mindepth 1 -type d -o -type l 2>/dev/null | sort) + +if [[ ${#FORKS[@]} -eq 0 ]]; then + echo "No local forks found under local/sources/" >&2 + exit 0 +fi + +VIOLATIONS=0 +declare -a JSON_LINES=() + +if [[ "$JSON" -eq 0 && "$QUIET" -eq 0 ]]; then + printf "%-16s %-8s %-8s %-12s %-12s %s\n" "FORK" "BEHIND" "AHEAD" "UPSTREAM" "OUR_HEAD" "STATUS" + printf "%-16s %-8s %-8s %-12s %-12s %s\n" "----" "-------" "------" "--------" "--------" "------" +fi + +for fork in "${FORKS[@]}"; do + fork_dir="${FORK_PATHS[$fork]}" + if [[ ! -d "$fork_dir/.git" ]]; then + continue + fi + + upstream_remote=$(cd "$fork_dir" && git remote get-url upstream 2>/dev/null || true) + if [[ -z "$upstream_remote" ]]; then + continue + fi + + # Determine upstream branch (master or main) + upstream_branch="master" + 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 + # Need to fetch first + if [[ "$NO_FETCH" -eq 0 ]]; then + (cd "$fork_dir" && git fetch upstream --depth="$FETCH_DEPTH" --quiet 2>/dev/null) || true + fi + if (cd "$fork_dir" && git rev-parse --verify upstream/master >/dev/null 2>&1); then + upstream_branch="master" + elif (cd "$fork_dir" && git rev-parse --verify upstream/main >/dev/null 2>&1); then + upstream_branch="main" + else + continue + fi + fi + fi + + # Fetch if needed + if [[ "$NO_FETCH" -eq 0 ]]; then + (cd "$fork_dir" && git fetch upstream --depth="$FETCH_DEPTH" --quiet 2>/dev/null) || true + fi + + ref="upstream/$upstream_branch" + behind=$(cd "$fork_dir" && git log --oneline "HEAD..$ref" 2>/dev/null | wc -l) + ahead=$(cd "$fork_dir" && git log --oneline "$ref..HEAD" 2>/dev/null | wc -l) + upstream_sha=$(cd "$fork_dir" && git rev-parse --short "$ref" 2>/dev/null || echo "?") + our_sha=$(cd "$fork_dir" && git rev-parse --short HEAD 2>/dev/null || echo "?") + upstream_date=$(cd "$fork_dir" && git log -1 --format='%ci' "$ref" 2>/dev/null | cut -d' ' -f1 || echo "?") + our_date=$(cd "$fork_dir" && git log -1 --format='%ci' HEAD 2>/dev/null | cut -d' ' -f1 || echo "?") + + status="OK" + if [[ "$behind" -gt "$THRESHOLD" ]]; then + status="DRIFT" + VIOLATIONS=$((VIOLATIONS + 1)) + elif [[ "$behind" -gt 0 ]]; then + status="MINOR" + fi + + if [[ "$JSON" -eq 1 ]]; then + json_line=$(printf '{"fork":"%s","behind":%d,"ahead":%d,"upstream_sha":"%s","our_sha":"%s","upstream_date":"%s","our_date":"%s","status":"%s"}' \ + "$fork" "$behind" "$ahead" "$upstream_sha" "$our_sha" "$upstream_date" "$our_date" "$status") + JSON_LINES+=("$json_line") + elif [[ "$QUIET" -eq 0 ]] || [[ "$status" != "OK" ]]; then + if [[ "$status" == "DRIFT" ]]; then + printf "%-16s \033[1;31m%-8s\033[0m %-8s %-12s %-12s %s\n" "$fork" "$behind" "$ahead" "$upstream_sha" "$our_sha" "$status" + elif [[ "$status" == "MINOR" ]]; then + printf "%-16s \033[1;33m%-8s\033[0m %-8s %-12s %-12s %s\n" "$fork" "$behind" "$ahead" "$upstream_sha" "$our_sha" "$status" + else + printf "%-16s \033[1;32m%-8s\033[0m %-8s %-12s %-12s %s\n" "$fork" "$behind" "$ahead" "$upstream_sha" "$our_sha" "$status" + fi + fi +done + +if [[ "$JSON" -eq 1 ]]; then + echo "[" + for i in "${!JSON_LINES[@]}"; do + if [[ $i -lt $((${#JSON_LINES[@]} - 1)) ]]; then + echo " ${JSON_LINES[$i]}," + else + echo " ${JSON_LINES[$i]}" + fi + done + echo "]" +else + echo "" + if [[ "$VIOLATIONS" -gt 0 ]]; then + echo -e "\033[1;31mDRIFT DETECTED: $VIOLATIONS fork(s) exceed threshold ($THRESHOLD commits behind upstream).\033[0m" + echo "" + echo "To upgrade forks to latest upstream:" + echo " ./local/scripts/upgrade-forks.sh" + echo "" + echo "To suppress this check in builds:" + echo " REDBEAR_SKIP_DRIFT_CHECK=1" + else + echo -e "\033[1;32mAll forks within drift threshold ($THRESHOLD commits).\033[0m" + fi +fi + +if [[ "$VIOLATIONS" -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/local/scripts/upgrade-forks.sh b/local/scripts/upgrade-forks.sh new file mode 100755 index 0000000000..c8fd232ada --- /dev/null +++ b/local/scripts/upgrade-forks.sh @@ -0,0 +1,262 @@ +#!/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. 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 +# +# Safety: +# - Requires clean working tree in each fork (no uncommitted changes) +# - Creates backup branch before reset +# - Stops on first conflict (interactive resolution or --abort) +# +# Environment: +# REDBEAR_UPGRADE_FORCE=1 Same as --force + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +DRY_RUN=0 +FORCE=0 +declare -a TARGET_FORKS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + --force) FORCE=1 ;; + -h|--help) + echo "Usage: $0 [--dry-run] [--force] [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" + 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 + +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 + upstream_branch="master" + (cd "$fork_dir" && git fetch upstream --quiet 2>/dev/null) || true + 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" + 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 ]]; then + echo -e " ${GREEN}Already up to date${NC}" + SKIP_COUNT=$((SKIP_COUNT + 1)) + echo "" + continue + 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" + + # Collect RB commit list (oldest first for cherry-pick) + mapfile -t rb_commits < <(cd "$fork_dir" && git log --reverse --format='%H' "${upstream_ref}..HEAD" 2>/dev/null) + + if [[ "$DRY_RUN" -eq 1 ]]; then + echo -e " ${YELLOW}PLAN: reset to $upstream_sha, cherry-pick $ahead commits${NC}" + for c in "${rb_commits[@]}"; do + short=$(cd "$fork_dir" && git log -1 --format='%h %s' "$c") + echo " + $short" + done + echo "" + continue + fi + + # Create backup branch + backup_branch="rb-backup/$(date +%Y%m%d-%H%M%S)" + (cd "$fork_dir" && git branch "$backup_branch" HEAD 2>/dev/null) + echo " Backup: $backup_branch" + + # Reset to upstream + (cd "$fork_dir" && git reset --hard "$upstream_ref" 2>/dev/null) + echo " Reset to: $upstream_sha" + + # Cherry-pick RB commits + 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: cherry-pick conflict, restored from backup${NC}" + echo -e " ${YELLOW}Manual rebase needed: cd $fork_dir && git rebase $upstream_ref $backup_branch${NC}" + (cd "$fork_dir" && git reset --hard "$backup_branch" 2>/dev/null) + FAIL_COUNT=$((FAIL_COUNT + 1)) + FAILED_FORKS+=("$fork") + echo "" + continue + fi + + new_sha=$(cd "$fork_dir" && git rev-parse --short HEAD) + echo -e " ${GREEN}DONE: $old_sha → $new_sha ($ahead patches reapplied)${NC}" + echo -e " ${YELLOW}Backup at $backup_branch${NC}" + 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 diff --git a/local/sources/bootloader b/local/sources/bootloader index 5d6f8346b4..7b28db166e 160000 --- a/local/sources/bootloader +++ b/local/sources/bootloader @@ -1 +1 @@ -Subproject commit 5d6f8346b47d4df8276e07e0f0636dc06f501612 +Subproject commit 7b28db166e8ab27b8074c8df790383a3e7b99f35