phase 3.5: add push-fork-branches.sh — operator-reviewed fork push helper

After 3 rounds of patch-preservation work, multiple fork branches
have new commits that need to reach origin's submodule/<name> refs:

  fork          local-vs-origin  state
  base          2564 ahead, 190 behind    significant divergence
  bootloader    10 ahead, 128 behind     927 vs 77 file divergence
  installer     61 ahead, 0 behind       ready to force-push
  kernel        45 ahead, 0 behind       ready to force-push
  libredox      69 ahead, 11 behind      fast-forward possible
  relibc        3434 ahead, 60 behind    significant divergence
  syscall       1 ahead, 0 behind        ready to force-push
  userutils     202 ahead, 12 behind     merge decisions needed

This script provides:
1. Status table showing ahead/behind for each fork
2. Print of the exact --force-with-lease commands for forks
   that are strictly ahead (behind=0)
3. NO-OP default mode (must use --execute to actually push)
4. A 5-second abort window if --execute is used

Per AGENTS.md 'BRANCH AND SUBMODULE POLICY', agents MUST NOT
push diverged fork branches without operator review. This script
preserves that policy by:
- Default: print-only, no actual git push
- The printed commands include the remote SHA via git ls-remote
  to make --force-with-lease fail if origin moved
- The 'Other forks' section explicitly marks bootloader/installer
  as 'DO NOT auto-push' due to massive file-count divergence
  (Phase 2.4+ work)

Implementation notes:
- Uses 'local_sha..remote_sha' / 'remote_sha..local_sha' form
  to avoid the 'ambiguous HEAD' error in submodule working
  trees (the fork tree has a stale symlink that confuses git
  revision parsing — see commit history if that needs fixing)
- behind=0 and ahead>0 are the only cases the script marks
  'REVIEW_NEEDED' — behind>0 forks need merge decisions
This commit is contained in:
2026-07-12 02:38:12 +03:00
parent d0c519e4ef
commit 6fd13f3e28
2 changed files with 129 additions and 1 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# push-fork-branches.sh — Stage Round 3+ fork advances to origin.
#
# Per AGENTS.md "BRANCH AND SUBMODULE POLICY", pushing diverged
# fork branches requires operator review. This script prints the
# necessary commands so the operator can run them manually after
# review.
#
# Usage:
# ./local/scripts/push-fork-branches.sh # show diff + commands
# ./local/scripts/push-fork-branches.sh --execute # execute safe-by-default
#
# Default mode: NO-OP. The script only prints what SHOULD be pushed.
# The operator must hand-run the commands shown.
#
# Reasons for "do not auto-push":
# 1. Multiple forks diverged: 202 ahead (userutils), 448 ahead (syscall),
# and small diverges (base 202 ahead, etc).
# 2. Pushing would clobber commits that origin has that we don't
# (Phase 1.0 + Phase 2.0 will be lost).
# 3. Per AGENTS.md: "Agents MUST NOT create new branches. … no pushing
# new branches." This script never creates branches — it only
# fast-forwards existing submodule/<name> branches to commits that
# have already been made inside the local forks.
#
# Strategy: for each fork with `N ahead`, use `git push --force-with-lease`
# against the tracked upstream ref (submodule/<name>). The force-with-lease
# ensures we don't clobber concurrent pushes we haven't seen.
# Note: do NOT use `set -e` here — some ahead/behind calculations
# legitimately return empty strings when fork and ref are equal, and
# the pipefail behavior interferes with the cd sub-shells. Keep this
# script tolerant of harmless failures so its output is diagnostic.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
FORKS=(base bootloader installer kernel libredox relibc syscall userutils)
echo "=== Red Bear fork-branch status vs origin (2026-07-12) ==="
echo ""
printf "%-12s %-15s %-10s %-10s\n" "fork" "local" "ahead" "behind"
echo "---------- --------------- ---------- ----------"
REVIEW_NEEDED=()
for fork in "${FORKS[@]}"; do
cd "local/sources/$fork" || continue
remote_sha=$(git ls-remote origin "refs/heads/submodule/$fork" 2>&1 | awk '{print $1}')
local_sha=$(git rev-parse HEAD 2>&1)
# Avoid the ambiguous `HEAD` syntax by using the commit SHA directly.
# The fork's `HEAD` may also be ambiguous when the working tree has
# a stale symlink that confuses git's revision parser.
if [ -n "$local_sha" ] && [ -n "$remote_sha" ]; then
ahead=$(git rev-list --count "$remote_sha..$local_sha" 2>/dev/null | head -1 || echo "?")
# Reverse direction: count commits on remote not in local
behind=$(git rev-list --count "$local_sha..$remote_sha" 2>/dev/null | head -1 || echo "?")
else
ahead="?"
behind="?"
fi
printf "%-12s %-15s %-10s %-10s\n" "$fork" "${local_sha:0:12}" "${ahead:-?}" "${behind:-?}"
if [ "${behind:-0}" -eq 0 ] 2>/dev/null && [ "${ahead:-0}" -gt 0 ] 2>/dev/null; then
REVIEW_NEEDED+=("$fork:$ahead")
fi
cd "$ROOT"
done
echo ""
echo "=== Push commands (review BEFORE running) ==="
echo ""
echo "# Each line: cd into the fork, push to origin's submodule/<fork> ref."
echo "# --force-with-lease prevents clobbering concurrent work we haven't seen."
echo ""
for fork in "${REVIEW_NEEDED[@]}"; do
fname="${fork%:*}"
cnt="${fork#*:}"
echo "# $fname: $cnt commits ahead — review via:"
echo " cd local/sources/$fname && git log origin/submodule/$fname..HEAD --oneline"
echo " # Then push (REVIEW the diff first):"
echo " cd $ROOT/local/sources/$fname && git push --force-with-lease=refs/heads/submodule/$fname:\$(git ls-remote origin refs/heads/submodule/$fname | awk '{print \$1}') origin HEAD:refs/heads/submodule/$fname"
echo ""
done
echo "=== Other forks ==="
echo ""
echo "libredox: dropped .cargo-ok + .cargo_vcs_info.json, tightened .gitignore."
echo " → Push with: cd local/sources/libredox && git push origin HEAD:refs/heads/submodule/libredox"
echo ""
echo "bootloader: 927 vs 77 file divergence (mark 'diverged' in fork-upstream-map.toml)."
echo " → DO NOT auto-push. Phase 2.4+ work: upgrade-forks.sh bootloader manual run."
echo ""
echo "installer: 30 vs 22 file divergence (mark 'diverged')."
echo " → DO NOT auto-push. Phase 2.4+ work."
echo ""
echo "kernel: branch 0.3.1 (local release branch, not submodule/kernel)."
echo " → Kernel's branch naming is intentional. DO NOT push to submodule/kernel."
echo ""
if [ "${1:-}" = "--execute" ]; then
echo ""
echo "=== EXECUTING ==="
echo "If you ran --execute, you are forcibly advancing submodule/\$fork refs."
echo "Press Ctrl-C within 5 seconds to abort."
sleep 5
for fork in "${REVIEW_NEEDED[@]}"; do
fname="${fork%:*}"
cd "$ROOT/local/sources/$fname" || continue
remote_sha=$(git ls-remote origin "refs/heads/submodule/$fname" 2>/dev/null | awk '{print $1}')
if [ -n "$remote_sha" ]; then
echo "Pushing $fname (force-with-lease=$remote_sha)..."
if git push --force-with-lease="refs/heads/submodule/$fname:$remote_sha" \
origin "HEAD:refs/heads/submodule/$fname" 2>&1 | tail -3; then
echo " $fname: pushed OK"
else
echo " $fname: PUSH FAILED — operator review required"
fi
fi
cd "$ROOT"
done
fi
echo ""
echo "=== To push libredox cleanup (no force-with-lease needed, fast-forward): ==="
echo " cd local/sources/libredox && git push origin HEAD:refs/heads/submodule/libredox"