2026-02-13 12:41:44 +03:00
#!/usr/bin/env bash
2026-05-27 03:32:04 +01:00
# lib/onboard.sh — Unified onboard orchestration (reset + adopt strategies)
2026-02-13 12:41:44 +03:00
#
# Provides:
2026-05-27 03:32:04 +01:00
# onboard_flow() — Interactive: import → wait → finalize
# finalize = subrepo_reset (rewrite) or
# adopt_branch (preserve)
# resolve_onboard_strategy() — Pick reset|adopt from --mode / config /
# ls-remote auto-detection
# migrate_one_pr() — Migrate one PR from archived to new subrepo
2026-02-13 12:41:44 +03:00
#
2026-05-27 03:32:04 +01:00
# State is stored on the josh-sync-state branch at <target>/onboard.json:
# { step, strategy, import_prs, ... }
# read_onboard_state falls back to legacy <target>/adopt.json (with implicit
# strategy="adopt") so in-flight v2.1 adoptions continue to resume correctly.
2026-02-13 12:41:44 +03:00
#
2026-05-27 03:32:04 +01:00
# Steps:
# start → importing → waiting-for-merge → resetting | adopting → complete
#
# Requires: lib/core.sh, lib/config.sh, lib/auth.sh, lib/state.sh, lib/sync.sh,
# lib/adopt.sh sourced
# Expects: JOSH_SYNC_TARGET_NAME, BOT_NAME, BOT_EMAIL, SUBREPO_API,
# SUBREPO_TOKEN, etc.
2026-02-13 12:41:44 +03:00
2026-05-27 03:32:04 +01:00
# ─── State Helpers ────────────────────────────────────────────────
# Same pattern as read_state()/write_state() in lib/state.sh.
2026-02-13 12:41:44 +03:00
read_onboard_state( ) {
local target_name = " ${ 1 :- $JOSH_SYNC_TARGET_NAME } "
git fetch origin " $STATE_BRANCH " 2>/dev/null || true
2026-05-27 03:32:04 +01:00
local json
json = $( git show " origin/ ${ STATE_BRANCH } : ${ target_name } /onboard.json " 2>/dev/null || echo "" )
if [ -n " $json " ] ; then
# Pre-unification onboard.json had no .strategy field — the only flow that
# existed was the destructive reset path; treat missing/empty .strategy as
# reset so resume doesn't silently flip via re-resolution.
echo " $json " | jq '. + {strategy: ((.strategy // "") | if . == "" then "reset" else . end)}'
return
fi
# Backward-compat: fall back to v2.1 <target>/adopt.json so in-flight
# adoptions started before the unification continue to resume. The legacy
# schema lacks `.strategy`; inject "adopt" so resolve/dispatch downstream
# treats it correctly. Also drop the legacy `.mode` field (v2.1 carried it
# redundantly) to avoid future schema collisions.
json = $( git show " origin/ ${ STATE_BRANCH } : ${ target_name } /adopt.json " 2>/dev/null || echo "" )
if [ -n " $json " ] ; then
echo " $json " | jq 'del(.mode) + {strategy: ((.strategy // "") | if . == "" then "adopt" else . end)}'
return
fi
echo '{}'
2026-02-13 12:41:44 +03:00
}
write_onboard_state( ) {
local target_name = " ${ 1 :- $JOSH_SYNC_TARGET_NAME } "
local state_json = " $2 "
local key = " ${ target_name } /onboard "
local tmp_dir
tmp_dir = $( mktemp -d)
if git rev-parse " origin/ ${ STATE_BRANCH } " >/dev/null 2>& 1; then
git worktree add " $tmp_dir " " origin/ ${ STATE_BRANCH } " 2>/dev/null
else
git worktree add --detach " $tmp_dir " 2>/dev/null
( cd " $tmp_dir " && git checkout --orphan " $STATE_BRANCH " && { git rm -rf . 2>/dev/null || true; } )
fi
mkdir -p " $( dirname " ${ tmp_dir } / ${ key } .json " ) "
echo " $state_json " | jq '.' > " ${ tmp_dir } / ${ key } .json "
(
cd " $tmp_dir " || exit
git add -A
if ! git diff --cached --quiet 2>/dev/null; then
git -c user.name= " $BOT_NAME " -c user.email= " $BOT_EMAIL " \
commit -m " onboard: update ${ target_name } "
git push origin " HEAD: ${ STATE_BRANCH } " || log "WARN" "Failed to push onboard state"
fi
)
git worktree remove " $tmp_dir " 2>/dev/null || rm -rf " $tmp_dir "
}
2026-05-27 03:32:04 +01:00
# Remove the v2.1 <target>/adopt.json from the state branch if present, so
# that a --restart durably clears legacy state on the next read. Called by
# onboard_flow's restart path.
_onboard_state_remove_legacy( ) {
local target_name = " ${ 1 :- $JOSH_SYNC_TARGET_NAME } "
git fetch origin " $STATE_BRANCH " 2>/dev/null || true
git show " origin/ ${ STATE_BRANCH } : ${ target_name } /adopt.json " >/dev/null 2>& 1 || return 0
local tmp_dir
tmp_dir = $( mktemp -d)
if ! git worktree add " $tmp_dir " " origin/ ${ STATE_BRANCH } " 2>/dev/null; then
rm -rf " $tmp_dir "
return 0
fi
(
cd " $tmp_dir " || exit
git rm -q " ${ target_name } /adopt.json " 2>/dev/null || true
if ! git diff --cached --quiet 2>/dev/null; then
git -c user.name= " $BOT_NAME " -c user.email= " $BOT_EMAIL " \
commit -q -m " onboard: drop legacy ${ target_name } /adopt.json on --restart "
git push -q origin " HEAD: ${ STATE_BRANCH } " || log "WARN" "Failed to push legacy adopt.json removal"
fi
)
git worktree remove " $tmp_dir " 2>/dev/null || rm -rf " $tmp_dir "
}
# ─── Strategy Resolution ──────────────────────────────────────────
# Precedence (highest first):
# 1. explicit --mode flag
# 2. targets[].history_lock (preserve → adopt, rewrite → reset)
# 3. auto-detect: subrepo has any branches → adopt, otherwise reset
#
# Prints the chosen strategy (reset|adopt) on stdout; logs the source on
# stderr so the preflight line in cmd_onboard surfaces the choice.
resolve_onboard_strategy( ) {
local target_json = " $1 "
local explicit_mode = " ${ 2 :- } "
if [ -n " $explicit_mode " ] ; then
case " $explicit_mode " in
reset| adopt) ; ;
*) die " Invalid --mode ' ${ explicit_mode } ' (must be 'reset' or 'adopt') " ; ;
esac
log "INFO" " Strategy: ${ explicit_mode } (from --mode flag) "
echo " $explicit_mode "
return
fi
local history_lock
history_lock = $( echo " $target_json " | jq -r '.history_lock // empty' )
if [ -n " $history_lock " ] ; then
case " $history_lock " in
preserve)
log "INFO" "Strategy: adopt (from config history_lock: preserve)"
echo "adopt" ; return ; ;
rewrite)
log "INFO" "Strategy: reset (from config history_lock: rewrite)"
echo "reset" ; return ; ;
*)
die " Invalid history_lock ' ${ history_lock } ' in target config (must be 'preserve' or 'rewrite') " ; ;
esac
fi
# Critical: distinguish a genuinely empty subrepo from a failed ls-remote.
# Treating an auth/network failure as "empty" would silently pick reset and
# force-push a live subrepo's history away.
local heads
if heads = $( git ls-remote --heads " $( subrepo_auth_url) " 2>/dev/null) ; then
if [ -n " $heads " ] ; then
log "INFO" "Strategy: adopt (auto-detected — subrepo has branches)"
echo "adopt"
else
log "INFO" "Strategy: reset (auto-detected — subrepo has no branches)"
echo "reset"
fi
else
die "Cannot auto-detect strategy: 'git ls-remote' against the subrepo failed (auth/network). Pass --mode=reset|adopt explicitly, or set targets[].history_lock for this target."
fi
}
2026-02-13 12:41:44 +03:00
# ─── Derive Archived API URL ─────────────────────────────────────
# Given a URL like "git@host:org/repo-archived.git" or
# "https://host/org/repo-archived.git", derive the Gitea API URL.
_archived_api_from_url( ) {
local url = " $1 "
url = " ${ url %.git } "
local host repo_path
if echo " $url " | grep -qE '^(ssh://|git@)' ; then
if echo " $url " | grep -q '^ssh://' ; then
host = $( echo " $url " | sed -E 's|ssh://[^@]*@([^/]+)/.*|\1|' )
repo_path = $( echo " $url " | sed -E 's|ssh://[^@]*@[^/]+/(.+)$|\1|' )
else
host = $( echo " $url " | sed -E 's|git@([^:/]+)[:/].*|\1|' )
repo_path = $( echo " $url " | sed -E 's|git@[^:/]+[:/](.+)$|\1|' )
fi
else
host = $( echo " $url " | sed -E 's|https?://([^/]+)/.*|\1|' )
repo_path = $( echo " $url " | sed -E 's|https?://[^/]+/(.+)$|\1|' )
fi
echo " https:// ${ host } /api/v1/repos/ ${ repo_path } "
}
# ─── Onboard Flow ────────────────────────────────────────────────
# Interactive orchestrator with checkpoint/resume.
2026-05-27 03:32:04 +01:00
# Usage: onboard_flow <target_json> <restart> [explicit_mode]
# explicit_mode — "reset", "adopt", or "" for auto-resolve.
2026-02-13 12:41:44 +03:00
onboard_flow( ) {
local target_json = " $1 "
local restart = " ${ 2 :- false } "
2026-05-27 03:32:04 +01:00
local explicit_mode = " ${ 3 :- } "
2026-02-13 12:41:44 +03:00
local target_name = " $JOSH_SYNC_TARGET_NAME "
2026-05-27 03:32:04 +01:00
local state current_step strategy
state = $( read_onboard_state " $target_name " )
current_step = $( echo " $state " | jq -r '.step // "start"' )
strategy = $( echo " $state " | jq -r '.strategy // empty' )
2026-02-13 12:41:44 +03:00
if [ " $restart " = true ] ; then
log "INFO" "Restarting onboard from scratch"
current_step = "start"
2026-05-27 03:32:04 +01:00
state = '{}'
strategy = ""
# Durably clear any legacy v2.1 adopt.json so the next read doesn't
# silently fall back to it.
_onboard_state_remove_legacy " $target_name "
2026-02-13 12:41:44 +03:00
fi
2026-05-27 03:32:04 +01:00
if [ -z " $strategy " ] ; then
strategy = $( resolve_onboard_strategy " $target_json " " $explicit_mode " )
elif [ -n " $explicit_mode " ] && [ " $strategy " != " $explicit_mode " ] ; then
die " Saved strategy is ' ${ strategy } ' (mid-flow); cannot switch to --mode= ${ explicit_mode } without --restart. "
else
log "INFO" " Strategy: ${ strategy } (resumed from state) "
fi
case " $strategy " in
reset| adopt) ; ;
*) die " Unknown strategy ' ${ strategy } ' in state (corrupt?). Re-run with --restart to start over. " ; ;
esac
2026-02-13 12:41:44 +03:00
log "INFO" " Onboard step: ${ current_step } "
2026-05-27 03:32:04 +01:00
# ── Step 1: Strategy-specific intro and (for reset) archived repo info ──
2026-02-13 12:41:44 +03:00
if [ " $current_step " = "start" ] ; then
echo "" >& 2
2026-05-27 03:32:04 +01:00
echo " === Onboarding ${ target_name } (strategy: ${ strategy } ) === " >& 2
2026-02-13 12:41:44 +03:00
echo "" >& 2
2026-05-27 03:32:04 +01:00
if [ " $strategy " = "reset" ] ; then
echo "Before proceeding, you should have:" >& 2
echo " 1. Renamed the existing subrepo (e.g., storefront → storefront-archived)" >& 2
echo " 2. Created a new EMPTY repo at the original URL" >& 2
echo "" >& 2
2026-02-13 12:41:44 +03:00
2026-05-27 03:32:04 +01:00
if git ls-remote " $( subrepo_auth_url) " >/dev/null 2>& 1; then
# shellcheck disable=SC2001 # sed is clearer for URL pattern replacement
log "INFO" " New subrepo is reachable at $( echo " $SUBREPO_URL " | sed 's|://[^@]*@|://***@|' ) "
else
log "WARN" "New subrepo is not reachable — make sure you created the new empty repo"
fi
2026-02-13 12:41:44 +03:00
2026-05-27 03:32:04 +01:00
echo "Enter the archived repo URL (e.g., git@host:org/repo-archived.git):" >& 2
local archived_url
read -r archived_url
[ -n " $archived_url " ] || die "Archived URL is required"
local archived_auth = " ${ SUBREPO_AUTH :- https } "
local archived_api
archived_api = $( _archived_api_from_url " $archived_url " )
if curl -sf -H " Authorization: token ${ SUBREPO_TOKEN } " \
" ${ archived_api } " >/dev/null 2>& 1; then
log "INFO" " Archived repo reachable: ${ archived_api } "
else
log "WARN" "Cannot reach archived repo API — check URL and token"
echo "Continue anyway? (y/N):" >& 2
local confirm
read -r confirm
[ " $confirm " = "y" ] || [ " $confirm " = "Y" ] || die "Aborted"
fi
2026-02-13 12:41:44 +03:00
2026-05-27 03:32:04 +01:00
state = $( jq -n \
--arg step "importing" \
--arg strategy "reset" \
--arg archived_api " $archived_api " \
--arg archived_url " $archived_url " \
--arg archived_auth " $archived_auth " \
--arg ts " $( date -u +%Y-%m-%dT%H:%M:%SZ) " \
' { step:$step , strategy:$strategy , archived_api:$archived_api ,
archived_url:$archived_url , archived_auth:$archived_auth ,
import_prs:{ } , reset_branches:[ ] , migrated_prs:[ ] , timestamp:$ts } ' )
2026-02-13 12:41:44 +03:00
else
2026-05-27 03:32:04 +01:00
# strategy = adopt
echo "This keeps the existing subrepo history and adds one adoption merge commit per branch." >& 2
echo "No force-push is used. Existing open PR branches remain based on the old history." >& 2
echo "" >& 2
state = $( jq -n \
--arg step "importing" \
--arg strategy "adopt" \
--arg ts " $( date -u +%Y-%m-%dT%H:%M:%SZ) " \
'{step:$step, strategy:$strategy, import_prs:{}, adopted_branches:[], timestamp:$ts}' )
2026-02-13 12:41:44 +03:00
fi
2026-05-27 03:32:04 +01:00
write_onboard_state " $target_name " " $state "
2026-02-13 12:41:44 +03:00
current_step = "importing"
fi
2026-05-27 03:32:04 +01:00
# ── Step 2: Import (reset uses archived clone URL; adopt uses subrepo URL) ──
2026-02-13 12:41:44 +03:00
if [ " $current_step " = "importing" ] ; then
echo "" >& 2
log "INFO" "Step 2: Importing subrepo content into monorepo..."
2026-05-27 03:32:04 +01:00
local branches import_prs
2026-02-13 12:41:44 +03:00
branches = $( echo " $target_json " | jq -r '.branches | keys[]' )
2026-05-27 03:32:04 +01:00
import_prs = $( echo " $state " | jq -r '.import_prs // {}' )
local archived_clone_url = ""
if [ " $strategy " = "reset" ] ; then
local archived_url
archived_url = $( echo " $state " | jq -r '.archived_url // empty' )
[ -n " $archived_url " ] || die "Reset strategy: archived_url missing from state (corrupt or interrupted before start step finished). Re-run with --restart."
if [ " ${ SUBREPO_AUTH :- https } " = "ssh" ] ; then
archived_clone_url = " $archived_url "
else
# shellcheck disable=SC2001
archived_clone_url = $( echo " $archived_url " | sed " s|https://|https:// ${ BOT_USER } : ${ SUBREPO_TOKEN } @| " )
fi
2026-02-13 19:48:46 +03:00
fi
2026-02-13 12:41:44 +03:00
for branch in $branches ; do
local mapped
mapped = $( echo " $target_json " | jq -r --arg b " $branch " '.branches[$b] // empty' )
[ -z " $mapped " ] && continue
if echo " $import_prs " | jq -e --arg b " $branch " 'has($b)' >/dev/null 2>& 1; then
log "INFO" " Import PR already recorded for ${ branch } — skipping "
continue
fi
export SYNC_BRANCH_MONO = " $branch "
export SYNC_BRANCH_SUBREPO = " $mapped "
log "INFO" " Importing branch: ${ branch } (subrepo: ${ mapped } ) "
local result
2026-05-27 03:32:04 +01:00
if [ -n " $archived_clone_url " ] ; then
result = $( initial_import " $archived_clone_url " )
else
result = $( initial_import)
fi
2026-02-13 12:41:44 +03:00
log "INFO" " Import result for ${ branch } : ${ result } "
if [ " $result " = "pr-created" ] ; then
local prs pr_number
prs = $( list_open_prs " $MONOREPO_API " " $GITEA_TOKEN " )
pr_number = $( echo " $prs " | jq -r --arg t " $target_name " --arg b " $branch " \
'[.[] | select(.title | test("\\[Import\\] " + $t + ":")) | select(.base.ref == $b)] | .[0].number // empty' )
if [ -n " $pr_number " ] ; then
import_prs = $( echo " $import_prs " | jq --arg b " $branch " --arg n " $pr_number " '. + {($b): ($n | tonumber)}' )
log "INFO" " Import PR for ${ branch } : # ${ pr_number } "
else
2026-05-27 03:32:04 +01:00
# Same hazard for both strategies: continuing without recording the
# PR number means the next resume re-runs initial_import and creates
# a duplicate import PR. Bail out and let the user reconcile.
die " Could not find import PR number for ${ branch } on monorepo. Re-running would create a duplicate import PR — check the monorepo's open PRs first. "
2026-02-13 12:41:44 +03:00
fi
fi
2026-05-27 03:32:04 +01:00
state = $( echo " $state " | jq --argjson prs " $import_prs " '.import_prs = $prs' )
write_onboard_state " $target_name " " $state "
2026-02-13 12:41:44 +03:00
done
2026-05-27 03:32:04 +01:00
state = $( echo " $state " | jq \
2026-02-13 12:41:44 +03:00
--arg step "waiting-for-merge" \
--argjson prs " $import_prs " \
'.step = $step | .import_prs = $prs' )
2026-05-27 03:32:04 +01:00
write_onboard_state " $target_name " " $state "
2026-02-13 12:41:44 +03:00
current_step = "waiting-for-merge"
fi
2026-05-27 03:32:04 +01:00
# ── Step 3: Wait for import PR(s) to merge ──
2026-02-13 12:41:44 +03:00
if [ " $current_step " = "waiting-for-merge" ] ; then
echo "" >& 2
log "INFO" "Step 3: Waiting for import PR(s) to be merged..."
2026-05-27 03:32:04 +01:00
local import_prs pr_count
import_prs = $( echo " $state " | jq -r '.import_prs' )
2026-02-13 12:41:44 +03:00
pr_count = $( echo " $import_prs " | jq 'length' )
2026-05-27 03:32:04 +01:00
if [ " $pr_count " -gt 0 ] ; then
2026-02-13 12:41:44 +03:00
echo "" >& 2
echo "Import PRs to merge:" >& 2
echo " $import_prs " | jq -r 'to_entries[] | " \(.key): PR #\(.value)"' >& 2
echo "" >& 2
echo "Merge the import PR(s) on the monorepo, then press Enter..." >& 2
read -r
local all_merged = true
for branch in $( echo " $import_prs " | jq -r 'keys[]' ) ; do
2026-05-27 03:32:04 +01:00
local pr_number pr_json merged
2026-02-13 12:41:44 +03:00
pr_number = $( echo " $import_prs " | jq -r --arg b " $branch " '.[$b]' )
pr_json = $( get_pr " $MONOREPO_API " " $GITEA_TOKEN " " $pr_number " )
merged = $( echo " $pr_json " | jq -r '.merged // false' )
if [ " $merged " = "true" ] ; then
log "INFO" " PR # ${ pr_number } ( ${ branch } ): merged "
else
log "ERROR" " PR # ${ pr_number } ( ${ branch } ): NOT merged — merge it first "
all_merged = false
fi
done
if [ " $all_merged " = false ] ; then
die " Not all import PRs are merged. Re-run 'josh-sync onboard ${ target_name } ' after merging. "
fi
2026-05-27 03:32:04 +01:00
else
log "INFO" "No import PRs recorded — subfolder already matched subrepo"
2026-02-13 12:41:44 +03:00
fi
2026-05-27 03:32:04 +01:00
local next_step
if [ " $strategy " = "reset" ] ; then
next_step = "resetting"
else
next_step = "adopting"
fi
state = $( echo " $state " | jq --arg s " $next_step " '.step = $s' )
write_onboard_state " $target_name " " $state "
current_step = " $next_step "
2026-02-13 12:41:44 +03:00
fi
2026-05-27 03:32:04 +01:00
# ── Step 4a: Finalize via reset (force-push josh-filtered history) ──
2026-02-13 12:41:44 +03:00
if [ " $current_step " = "resetting" ] ; then
echo "" >& 2
log "INFO" "Step 4: Pushing josh-filtered history to new subrepo..."
2026-05-27 03:32:04 +01:00
local branches already_reset
2026-02-13 12:41:44 +03:00
branches = $( echo " $target_json " | jq -r '.branches | keys[]' )
2026-05-27 03:32:04 +01:00
already_reset = $( echo " $state " | jq -r '.reset_branches // []' )
2026-02-13 12:41:44 +03:00
for branch in $branches ; do
if echo " $already_reset " | jq -e --arg b " $branch " 'index($b) != null' >/dev/null 2>& 1; then
log "INFO" " Branch ${ branch } already reset — skipping "
continue
fi
local mapped
mapped = $( echo " $target_json " | jq -r --arg b " $branch " '.branches[$b] // empty' )
[ -z " $mapped " ] && continue
export SYNC_BRANCH_MONO = " $branch "
export SYNC_BRANCH_SUBREPO = " $mapped "
local result
result = $( subrepo_reset)
log "INFO" " Reset result for ${ branch } : ${ result } "
2026-05-27 03:32:04 +01:00
state = $( echo " $state " | jq --arg b " $branch " '.reset_branches += [$b]' )
write_onboard_state " $target_name " " $state "
2026-02-13 12:41:44 +03:00
done
2026-05-27 03:32:04 +01:00
state = $( echo " $state " | jq \
2026-02-13 12:41:44 +03:00
--arg step "complete" \
--arg ts " $( date -u +%Y-%m-%dT%H:%M:%SZ) " \
'.step = $step | .timestamp = $ts' )
2026-05-27 03:32:04 +01:00
write_onboard_state " $target_name " " $state "
current_step = "complete"
fi
# ── Step 4b: Finalize via adoption merge ──
if [ " $current_step " = "adopting" ] ; then
echo "" >& 2
log "INFO" "Step 4: Creating adoption merge commit(s)..."
local branches already_adopted
branches = $( echo " $target_json " | jq -r '.branches | keys[]' )
already_adopted = $( echo " $state " | jq -r '.adopted_branches // []' )
for branch in $branches ; do
if echo " $already_adopted " | jq -e --arg b " $branch " 'index($b) != null' >/dev/null 2>& 1; then
log "INFO" " Branch ${ branch } already adopted — skipping "
continue
fi
local mapped
mapped = $( echo " $target_json " | jq -r --arg b " $branch " '.branches[$b] // empty' )
[ -z " $mapped " ] && continue
export SYNC_BRANCH_MONO = " $branch "
export SYNC_BRANCH_SUBREPO = " $mapped "
local result
result = $( adopt_branch)
log "INFO" " Adopt result for ${ branch } : ${ result } "
case " $result " in
adopted| already-adopted)
state = $( echo " $state " | jq --arg b " $branch " '.adopted_branches += [$b]' )
write_onboard_state " $target_name " " $state "
; ;
tree-mismatch)
die " Tree mismatch for ${ branch } . Merge the import PR and make sure subrepo/ ${ mapped } matches the Josh-filtered monorepo tree. "
; ;
push-rejected)
die " Subrepo branch ${ mapped } changed during adoption. Re-run 'josh-sync onboard ${ target_name } --mode=adopt' to retry. "
; ;
missing-branch)
die " Subrepo branch ${ mapped } does not exist. "
; ;
*)
die " Unexpected adopt result: ${ result } "
; ;
esac
done
state = $( echo " $state " | jq \
--arg step "complete" \
--arg ts " $( date -u +%Y-%m-%dT%H:%M:%SZ) " \
'.step = $step | .timestamp = $ts' )
write_onboard_state " $target_name " " $state "
2026-02-13 12:41:44 +03:00
current_step = "complete"
fi
# ── Step 5: Done ──
if [ " $current_step " = "complete" ] ; then
echo "" >& 2
echo "=== Onboarding complete! ===" >& 2
echo "" >& 2
2026-05-27 03:32:04 +01:00
if [ " $strategy " = "reset" ] ; then
echo "The new subrepo now has josh-filtered history." >& 2
echo "Developers should re-clone or reset their local copies:" >& 2
echo " git fetch origin && git reset --hard origin/main" >& 2
echo "" >& 2
echo "To migrate open PRs from the archived repo:" >& 2
echo " josh-sync migrate-pr ${ target_name } # interactive picker " >& 2
echo " josh-sync migrate-pr ${ target_name } --all # migrate all " >& 2
echo " josh-sync migrate-pr ${ target_name } 5 8 12 # specific PRs " >& 2
else
echo "The subrepo keeps its existing history and now has a josh-sync adoption merge." >& 2
echo "Developers can keep their clones; they only need to fast-forward active branches:" >& 2
echo " git fetch origin && git checkout main && git merge --ff-only origin/main" >& 2
fi
2026-02-13 12:41:44 +03:00
fi
}
# ─── Migrate One PR ──────────────────────────────────────────────
2026-02-13 20:51:22 +03:00
# Fetches the PR's branch from the archived repo, computes a local diff,
# and applies it to the new subrepo with --3way for resilience.
2026-02-13 12:41:44 +03:00
# Usage: migrate_one_pr <pr_number>
#
# Expects: JOSH_SYNC_TARGET_NAME, SUBREPO_API, SUBREPO_TOKEN, BOT_NAME, BOT_EMAIL loaded
migrate_one_pr( ) {
local pr_number = " $1 "
local target_name = " $JOSH_SYNC_TARGET_NAME "
2026-05-27 03:32:04 +01:00
local onboard_state strategy archived_api
2026-02-13 12:41:44 +03:00
onboard_state = $( read_onboard_state " $target_name " )
2026-05-27 03:32:04 +01:00
strategy = $( echo " $onboard_state " | jq -r '.strategy // "reset"' )
if [ " $strategy " = "adopt" ] ; then
die " Target ' ${ target_name } ' was onboarded with the adopt strategy, which does not record an archived repo. migrate-pr only applies to the reset strategy. "
fi
2026-02-13 12:41:44 +03:00
archived_api = $( echo " $onboard_state " | jq -r '.archived_api' )
if [ -z " $archived_api " ] || [ " $archived_api " = "null" ] ; then
2026-05-27 03:32:04 +01:00
die " No archived repo info found for ' ${ target_name } '. Run 'josh-sync onboard ${ target_name } --mode=reset' first. "
2026-02-13 12:41:44 +03:00
fi
local already_migrated
already_migrated = $( echo " $onboard_state " | jq -r \
--argjson num " $pr_number " '.migrated_prs // [] | map(select(.old_number == $num)) | length' )
if [ " $already_migrated " -gt 0 ] ; then
log "INFO" " PR # ${ pr_number } already migrated — skipping "
return 0
fi
local archived_token = " $SUBREPO_TOKEN "
local pr_json title base head body
pr_json = $( get_pr " $archived_api " " $archived_token " " $pr_number " ) \
|| die " Failed to fetch PR # ${ pr_number } from archived repo "
title = $( echo " $pr_json " | jq -r '.title' )
base = $( echo " $pr_json " | jq -r '.base.ref' )
head = $( echo " $pr_json " | jq -r '.head.ref' )
body = $( echo " $pr_json " | jq -r '.body // ""' )
log "INFO" " Migrating PR # ${ pr_number } : \" ${ title } \" ( ${ base } <- ${ head } ) "
local original_dir
original_dir = $( pwd )
local work_dir
work_dir = $( mktemp -d)
# shellcheck disable=SC2064 # Intentional early expansion
trap " cd ' $original_dir ' 2>/dev/null; rm -rf ' $work_dir ' " RETURN
git clone " $( subrepo_auth_url) " --branch " $base " --single-branch \
" ${ work_dir } /subrepo " 2>& 1 || die " Failed to clone new subrepo (branch: ${ base } ) "
cd " ${ work_dir } /subrepo " || exit
git config user.name " $BOT_NAME "
git config user.email " $BOT_EMAIL "
2026-02-13 20:51:22 +03:00
local archived_url archived_clone_url
archived_url = $( echo " $onboard_state " | jq -r '.archived_url' )
if [ " ${ SUBREPO_AUTH :- https } " = "ssh" ] ; then
archived_clone_url = " $archived_url "
else
# shellcheck disable=SC2001
archived_clone_url = $( echo " $archived_url " | sed " s|https://|https:// ${ BOT_USER } : ${ SUBREPO_TOKEN } @| " )
fi
git remote add archived " $archived_clone_url "
git fetch archived " $head " " $base " 2>& 1 \
|| die "Failed to fetch branches from archived repo"
2026-02-13 12:41:44 +03:00
git checkout -B " $head " >& 2
2026-02-13 20:51:22 +03:00
local diff
diff = $( git diff " archived/ ${ base } ..archived/ ${ head } " )
if [ -z " $diff " ] ; then
log "WARN" " Empty diff for PR # ${ pr_number } — skipping "
return 1
fi
if echo " $diff " | git apply --3way 2>& 1; then
2026-02-13 12:41:44 +03:00
git add -A
git commit -m " ${ title }
Migrated from archived repo PR #${pr_number}" >&2
git push " $( subrepo_auth_url) " " $head " >& 2 \
|| die " Failed to push branch ${ head } "
local new_number
new_number = $( create_pr_number " $SUBREPO_API " " $SUBREPO_TOKEN " \
" $base " " $head " " $title " " $body " )
log "INFO" " Migrated PR # ${ pr_number } -> # ${ new_number } : \" ${ title } \" "
cd " $original_dir " || true
onboard_state = $( read_onboard_state " $target_name " )
onboard_state = $( echo " $onboard_state " | jq \
--argjson old " $pr_number " \
--argjson new_num " ${ new_number } " \
--arg title " $title " \
'.migrated_prs += [{"old_number":$old, "new_number":$new_num, "title":$title}]' )
write_onboard_state " $target_name " " $onboard_state "
else
2026-02-13 20:51:22 +03:00
log "ERROR" " Could not apply changes for PR # ${ pr_number } even with 3-way merge "
log "ERROR" " Manual migration needed: branch ' ${ head } ' from archived repo "
2026-02-13 12:41:44 +03:00
return 1
fi
}