#!/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