"#"
This commit is contained in:
335
lib/adopt.sh
Normal file
335
lib/adopt.sh
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/adopt.sh — Non-destructive adoption of existing subrepo history
|
||||
#
|
||||
# Provides:
|
||||
# adopt_flow() — Import → wait for merge → adoption merge per branch
|
||||
# adopt_branch() — Create the adoption merge and push it fast-forward only
|
||||
#
|
||||
# Adopt state is stored on the josh-sync-state branch at <target>/adopt.json.
|
||||
# Steps: start → importing → waiting-for-merge → adopting → complete
|
||||
#
|
||||
# Requires: lib/core.sh, lib/config.sh, lib/auth.sh, lib/state.sh, lib/sync.sh sourced
|
||||
|
||||
# ─── Adopt State Helpers ─────────────────────────────────────────
|
||||
|
||||
read_adopt_state() {
|
||||
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" 2>/dev/null || echo '{}'
|
||||
}
|
||||
|
||||
write_adopt_state() {
|
||||
local target_name="${1:-$JOSH_SYNC_TARGET_NAME}"
|
||||
local state_json="$2"
|
||||
local key="${target_name}/adopt"
|
||||
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 "adopt: update ${target_name}"
|
||||
git push origin "HEAD:${STATE_BRANCH}" || log "WARN" "Failed to push adopt state"
|
||||
fi
|
||||
)
|
||||
|
||||
git worktree remove "$tmp_dir" 2>/dev/null || rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
# ─── Adopt Branch ────────────────────────────────────────────────
|
||||
# Establishes shared ancestry without rewriting the existing subrepo branch.
|
||||
#
|
||||
# Preconditions:
|
||||
# - The subrepo branch and josh-filtered monorepo branch must have identical trees.
|
||||
#
|
||||
# Resulting commit:
|
||||
# - parent 1: josh-filtered HEAD, so josh can follow first-parent history
|
||||
# - parent 2: existing subrepo HEAD, so old subrepo history stays reachable
|
||||
# - tree: josh-filtered tree
|
||||
#
|
||||
# Returns: adopted | already-adopted | tree-mismatch | push-rejected | missing-branch
|
||||
|
||||
adopt_branch() {
|
||||
local mono_branch="$SYNC_BRANCH_MONO"
|
||||
local subrepo_branch="$SYNC_BRANCH_SUBREPO"
|
||||
local work_dir
|
||||
work_dir=$(mktemp -d)
|
||||
# shellcheck disable=SC2064 # Intentional early expansion — work_dir is local
|
||||
trap "rm -rf '$work_dir'" EXIT
|
||||
|
||||
log "INFO" "=== Adopt: subrepo/${subrepo_branch} ↔ mono/${mono_branch} ==="
|
||||
|
||||
local remote_subrepo_sha
|
||||
remote_subrepo_sha=$(git ls-remote "$(subrepo_auth_url)" "refs/heads/${subrepo_branch}" | awk '{print $1}') \
|
||||
|| die "Failed to reach subrepo (check SSH key / auth)"
|
||||
if [ -z "$remote_subrepo_sha" ]; then
|
||||
log "ERROR" "Subrepo branch ${subrepo_branch} does not exist"
|
||||
echo "missing-branch"
|
||||
return
|
||||
fi
|
||||
|
||||
git clone "$(subrepo_auth_url)" \
|
||||
--branch "$subrepo_branch" --single-branch \
|
||||
"${work_dir}/subrepo" || die "Failed to clone subrepo"
|
||||
|
||||
cd "${work_dir}/subrepo" || exit
|
||||
git config user.name "$BOT_NAME"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
|
||||
local subrepo_head subrepo_tree
|
||||
subrepo_head=$(git rev-parse HEAD)
|
||||
# shellcheck disable=SC1083 # {tree} is git syntax, not shell brace expansion
|
||||
subrepo_tree=$(git rev-parse "HEAD^{tree}")
|
||||
log "INFO" "Subrepo HEAD: ${subrepo_head:0:12}"
|
||||
|
||||
git remote add josh-filtered "$(josh_auth_url)"
|
||||
git fetch josh-filtered "$mono_branch" || die "Failed to fetch from josh-proxy"
|
||||
|
||||
local josh_head josh_tree
|
||||
josh_head=$(git rev-parse "josh-filtered/${mono_branch}")
|
||||
# shellcheck disable=SC1083
|
||||
josh_tree=$(git rev-parse "josh-filtered/${mono_branch}^{tree}")
|
||||
log "INFO" "Josh-filtered HEAD: ${josh_head:0:12}"
|
||||
|
||||
if [ "$subrepo_tree" != "$josh_tree" ]; then
|
||||
log "ERROR" "Tree mismatch: merge the import PR and ensure subrepo/${subrepo_branch} matches mono/${mono_branch} before adopting"
|
||||
echo "tree-mismatch"
|
||||
return
|
||||
fi
|
||||
|
||||
if git merge-base --is-ancestor "$josh_head" "$subrepo_head" \
|
||||
&& git merge-base --is-ancestor "$subrepo_head" "$josh_head"; then
|
||||
log "INFO" "Subrepo and josh-filtered histories already point to the same commit"
|
||||
echo "already-adopted"
|
||||
return
|
||||
fi
|
||||
|
||||
if git merge-base --is-ancestor "$josh_head" "$subrepo_head"; then
|
||||
log "INFO" "Josh-filtered HEAD is already an ancestor of subrepo HEAD"
|
||||
echo "already-adopted"
|
||||
return
|
||||
fi
|
||||
|
||||
local merge_commit
|
||||
merge_commit=$(git commit-tree "$josh_tree" \
|
||||
-p "$josh_head" \
|
||||
-p "$subrepo_head" \
|
||||
-m "Adopt ${JOSH_SYNC_TARGET_NAME} for josh-sync
|
||||
|
||||
${BOT_TRAILER}: adopt/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)")
|
||||
|
||||
git reset --hard "$merge_commit" >&2
|
||||
log "INFO" "Created adoption merge: ${merge_commit:0:12}"
|
||||
|
||||
if git push "$(subrepo_auth_url)" "HEAD:refs/heads/${subrepo_branch}"; then
|
||||
log "INFO" "Adoption complete: pushed fast-forward merge to subrepo/${subrepo_branch}"
|
||||
echo "adopted"
|
||||
else
|
||||
log "WARN" "Fast-forward push rejected — subrepo changed during adoption"
|
||||
echo "push-rejected"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Adopt Flow ──────────────────────────────────────────────────
|
||||
# Interactive orchestrator with checkpoint/resume.
|
||||
# Usage: adopt_flow <target_json> <restart>
|
||||
|
||||
adopt_flow() {
|
||||
local target_json="$1"
|
||||
local restart="${2:-false}"
|
||||
local target_name="$JOSH_SYNC_TARGET_NAME"
|
||||
|
||||
local adopt_state current_step
|
||||
adopt_state=$(read_adopt_state "$target_name")
|
||||
current_step=$(echo "$adopt_state" | jq -r '.step // "start"')
|
||||
|
||||
if [ "$restart" = true ]; then
|
||||
log "INFO" "Restarting adopt from scratch"
|
||||
current_step="start"
|
||||
adopt_state='{}'
|
||||
fi
|
||||
|
||||
log "INFO" "Adopt step: ${current_step}"
|
||||
|
||||
if [ "$current_step" = "start" ]; then
|
||||
echo "" >&2
|
||||
echo "=== Adopting ${target_name} ===" >&2
|
||||
echo "" >&2
|
||||
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
|
||||
|
||||
adopt_state=$(jq -n \
|
||||
--arg step "importing" \
|
||||
--arg mode "adopt" \
|
||||
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'{step:$step, mode:$mode, import_prs:{}, adopted_branches:[], timestamp:$ts}')
|
||||
write_adopt_state "$target_name" "$adopt_state"
|
||||
current_step="importing"
|
||||
fi
|
||||
|
||||
if [ "$current_step" = "importing" ]; then
|
||||
echo "" >&2
|
||||
log "INFO" "Step 1: Importing current subrepo content into monorepo..."
|
||||
|
||||
local branches import_prs
|
||||
branches=$(echo "$target_json" | jq -r '.branches | keys[]')
|
||||
import_prs=$(echo "$adopt_state" | jq -r '.import_prs // {}')
|
||||
|
||||
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"
|
||||
|
||||
local result
|
||||
result=$(initial_import)
|
||||
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')
|
||||
|
||||
[ -n "$pr_number" ] || die "Could not find import PR number for ${branch}; cannot safely continue adoption"
|
||||
import_prs=$(echo "$import_prs" | jq --arg b "$branch" --arg n "$pr_number" '. + {($b): ($n | tonumber)}')
|
||||
log "INFO" "Import PR for ${branch}: #${pr_number}"
|
||||
fi
|
||||
|
||||
adopt_state=$(echo "$adopt_state" | jq --argjson prs "$import_prs" '.import_prs = $prs')
|
||||
write_adopt_state "$target_name" "$adopt_state"
|
||||
done
|
||||
|
||||
adopt_state=$(echo "$adopt_state" | jq \
|
||||
--arg step "waiting-for-merge" \
|
||||
--argjson prs "$import_prs" \
|
||||
'.step = $step | .import_prs = $prs')
|
||||
write_adopt_state "$target_name" "$adopt_state"
|
||||
current_step="waiting-for-merge"
|
||||
fi
|
||||
|
||||
if [ "$current_step" = "waiting-for-merge" ]; then
|
||||
echo "" >&2
|
||||
log "INFO" "Step 2: Waiting for import PR(s) to be merged..."
|
||||
|
||||
local import_prs pr_count
|
||||
import_prs=$(echo "$adopt_state" | jq -r '.import_prs')
|
||||
pr_count=$(echo "$import_prs" | jq 'length')
|
||||
|
||||
if [ "$pr_count" -gt 0 ]; then
|
||||
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
|
||||
local pr_number pr_json merged
|
||||
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 adopt ${target_name}' after merging."
|
||||
fi
|
||||
else
|
||||
log "INFO" "No import PRs recorded — subfolder already matched subrepo"
|
||||
fi
|
||||
|
||||
adopt_state=$(echo "$adopt_state" | jq '.step = "adopting"')
|
||||
write_adopt_state "$target_name" "$adopt_state"
|
||||
current_step="adopting"
|
||||
fi
|
||||
|
||||
if [ "$current_step" = "adopting" ]; then
|
||||
echo "" >&2
|
||||
log "INFO" "Step 3: Creating adoption merge commit(s)..."
|
||||
|
||||
local branches already_adopted
|
||||
branches=$(echo "$target_json" | jq -r '.branches | keys[]')
|
||||
already_adopted=$(echo "$adopt_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)
|
||||
adopt_state=$(echo "$adopt_state" | jq --arg b "$branch" '.adopted_branches += [$b]')
|
||||
write_adopt_state "$target_name" "$adopt_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 adopt ${target_name}' to retry."
|
||||
;;
|
||||
missing-branch)
|
||||
die "Subrepo branch ${mapped} does not exist."
|
||||
;;
|
||||
*)
|
||||
die "Unexpected adopt result: ${result}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
adopt_state=$(echo "$adopt_state" | jq \
|
||||
--arg step "complete" \
|
||||
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'.step = $step | .timestamp = $ts')
|
||||
write_adopt_state "$target_name" "$adopt_state"
|
||||
current_step="complete"
|
||||
fi
|
||||
|
||||
if [ "$current_step" = "complete" ]; then
|
||||
echo "" >&2
|
||||
echo "=== Adoption complete! ===" >&2
|
||||
echo "" >&2
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user