#!/usr/bin/env bash set -euo pipefail # ============================================================================= # phase10_local_repo_cutover.sh — Re-point local repos from GitHub to Gitea # Depends on: Phase 8 complete (Gitea publicly reachable) + Phase 4 migrated # # For each discovered local repo under /Users/s/development: # 1. Rename origin -> github (if needed) # 2. Ensure repo exists on Gitea (create if missing) # 3. Add/update origin to point at Gitea # 4. Push all branches and tags to Gitea origin # 5. Ensure every local branch tracks origin/ (Gitea) # # Discovery is based on local git remotes: # - repo root is a direct child of PHASE10_LOCAL_ROOT (default /Users/s/development) # - repo has origin/github pointing to github.com/${GITHUB_USERNAME}/ # - duplicate clones are deduped by repo slug # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/lib/common.sh" source "${SCRIPT_DIR}/lib/phase10_common.sh" load_env require_vars GITEA_ADMIN_TOKEN GITEA_ADMIN_USER GITEA_ORG_NAME GITEA_DOMAIN GITEA_INTERNAL_URL GITHUB_USERNAME phase_header 10 "Local Repo Remote Cutover" LOCAL_REPO_ROOT="${PHASE10_LOCAL_ROOT:-/Users/s/development}" EXPECTED_REPO_COUNT="${PHASE10_EXPECTED_REPO_COUNT:-3}" INCLUDE_PATHS=() DRY_RUN=false FORCE_WITH_LEASE=false ASKPASS_SCRIPT="" PHASE10_GITEA_REPO_EXISTS=false PHASE10_REMOTE_BRANCHES="" PHASE10_REMOTE_TAGS="" PHASE10_LAST_CURL_ERROR="" PHASE10_LAST_HTTP_CODE="" PHASE10_HTTP_CONNECT_TIMEOUT="${PHASE10_HTTP_CONNECT_TIMEOUT:-15}" PHASE10_HTTP_LOW_SPEED_LIMIT="${PHASE10_HTTP_LOW_SPEED_LIMIT:-1}" PHASE10_HTTP_LOW_SPEED_TIME="${PHASE10_HTTP_LOW_SPEED_TIME:-30}" PHASE10_PUSH_TIMEOUT_SEC="${PHASE10_PUSH_TIMEOUT_SEC:-120}" PHASE10_LSREMOTE_TIMEOUT_SEC="${PHASE10_LSREMOTE_TIMEOUT_SEC:-45}" PHASE10_API_CONNECT_TIMEOUT_SEC="${PHASE10_API_CONNECT_TIMEOUT_SEC:-8}" PHASE10_API_MAX_TIME_SEC="${PHASE10_API_MAX_TIME_SEC:-20}" if [[ -n "${PHASE10_INCLUDE_PATHS:-}" ]]; then # Space-delimited list of extra repo roots to include in phase10 discovery. read -r -a INCLUDE_PATHS <<< "${PHASE10_INCLUDE_PATHS}" fi for arg in "$@"; do case "$arg" in --local-root=*) LOCAL_REPO_ROOT="${arg#*=}" ;; --expected-count=*) EXPECTED_REPO_COUNT="${arg#*=}" ;; --include-path=*) INCLUDE_PATHS+=("${arg#*=}") ;; --dry-run) DRY_RUN=true ;; --force-with-lease) FORCE_WITH_LEASE=true ;; --help|-h) cat < "$ASKPASS_SCRIPT" <<'EOF' #!/usr/bin/env sh case "$1" in *sername*) printf '%s\n' "$GITEA_GIT_USERNAME" ;; *assword*) printf '%s\n' "$GITEA_GIT_TOKEN" ;; *) printf '\n' ;; esac EOF chmod 700 "$ASKPASS_SCRIPT" } git_with_auth() { GIT_TERMINAL_PROMPT=0 \ GIT_ASKPASS="$ASKPASS_SCRIPT" \ GITEA_GIT_USERNAME="$GITEA_ADMIN_USER" \ GITEA_GIT_TOKEN="$GITEA_ADMIN_TOKEN" \ "$@" } run_with_timeout() { local timeout_sec="$1" shift if command -v perl >/dev/null 2>&1; then perl -e ' my $timeout = shift @ARGV; my $pid = fork(); if (!defined $pid) { exit 125; } if ($pid == 0) { setpgrp(0, 0); exec @ARGV; exit 125; } my $timed_out = 0; local $SIG{ALRM} = sub { $timed_out = 1; kill "TERM", -$pid; select(undef, undef, undef, 0.5); kill "KILL", -$pid; }; alarm $timeout; waitpid($pid, 0); alarm 0; if ($timed_out) { exit 124; } my $rc = $?; if ($rc == -1) { exit 125; } if ($rc & 127) { exit(128 + ($rc & 127)); } exit($rc >> 8); ' "$timeout_sec" "$@" else "$@" fi } git_with_auth_timed() { local timeout_sec="$1" shift run_with_timeout "$timeout_sec" \ env \ GIT_TERMINAL_PROMPT=0 \ GIT_ASKPASS="$ASKPASS_SCRIPT" \ GITEA_GIT_USERNAME="$GITEA_ADMIN_USER" \ GITEA_GIT_TOKEN="$GITEA_ADMIN_TOKEN" \ "$@" } ensure_github_remote() { local repo_path="$1" repo_name="$2" github_url="$3" local existing origin_existing has_bad_github has_bad_github=false if existing=$(git -C "$repo_path" remote get-url github 2>/dev/null); then if phase10_url_is_github_repo "$existing" "$GITHUB_USERNAME" "$repo_name"; then if [[ "$existing" != "$github_url" ]]; then if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would set github URL -> ${github_url}" else git -C "$repo_path" remote set-url github "$github_url" fi fi return 0 fi has_bad_github=true fi if origin_existing=$(git -C "$repo_path" remote get-url origin 2>/dev/null); then if phase10_url_is_github_repo "$origin_existing" "$GITHUB_USERNAME" "$repo_name"; then if [[ "$has_bad_github" == "true" ]]; then if [[ "$DRY_RUN" == "true" ]]; then log_warn "${repo_name}: would remove misconfigured 'github' remote and rebuild it from origin" else git -C "$repo_path" remote remove github log_warn "${repo_name}: removed misconfigured 'github' remote and rebuilt it from origin" fi fi if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would rename origin -> github" log_info "${repo_name}: would set github URL -> ${github_url}" else git -C "$repo_path" remote rename origin github git -C "$repo_path" remote set-url github "$github_url" log_success "${repo_name}: renamed origin -> github" fi return 0 fi fi if [[ "$has_bad_github" == "true" ]]; then log_error "${repo_name}: existing 'github' remote does not point to GitHub repo ${GITHUB_USERNAME}/${repo_name}" return 1 fi # Explicit include-path repos may have no GitHub remote yet. if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would add github remote -> ${github_url}" else git -C "$repo_path" remote add github "$github_url" log_success "${repo_name}: added github remote (${github_url})" fi return 0 } ensure_gitea_origin() { local repo_path="$1" repo_name="$2" gitea_url="$3" local existing if existing=$(git -C "$repo_path" remote get-url origin 2>/dev/null); then if phase10_url_is_gitea_repo "$existing" "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name"; then if [[ "$existing" != "$gitea_url" ]]; then if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would normalize origin URL -> ${gitea_url}" else git -C "$repo_path" remote set-url origin "$gitea_url" fi fi return 0 fi # origin exists but points somewhere else; force it to Gitea. if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would set origin URL -> ${gitea_url}" else git -C "$repo_path" remote set-url origin "$gitea_url" fi return 0 fi if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would add origin -> ${gitea_url}" else git -C "$repo_path" remote add origin "$gitea_url" fi return 0 } ensure_gitea_repo_exists() { local repo_name="$1" local create_payload create_response get_gitea_repo_http_code() { local target_repo="$1" local tmpfile errfile curl_code tmpfile=$(mktemp) errfile=$(mktemp) curl_code=$(curl \ -sS \ -o "$tmpfile" \ -w "%{http_code}" \ --connect-timeout "$PHASE10_API_CONNECT_TIMEOUT_SEC" \ --max-time "$PHASE10_API_MAX_TIME_SEC" \ -H "Authorization: token ${GITEA_ADMIN_TOKEN}" \ -H "Accept: application/json" \ "${GITEA_INTERNAL_URL}/api/v1/repos/${GITEA_ORG_NAME}/${target_repo}" 2>"$errfile") || { PHASE10_LAST_CURL_ERROR="$(tr '\n' ' ' < "$errfile" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" rm -f "$tmpfile" rm -f "$errfile" return 1 } rm -f "$tmpfile" rm -f "$errfile" PHASE10_LAST_CURL_ERROR="" PHASE10_LAST_HTTP_CODE="$curl_code" return 0 } create_gitea_repo() { local payload="$1" local tmpfile errfile curl_code tmpfile=$(mktemp) errfile=$(mktemp) curl_code=$(curl \ -sS \ -o "$tmpfile" \ -w "%{http_code}" \ --connect-timeout "$PHASE10_API_CONNECT_TIMEOUT_SEC" \ --max-time "$PHASE10_API_MAX_TIME_SEC" \ -X POST \ -H "Authorization: token ${GITEA_ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$payload" \ "${GITEA_INTERNAL_URL}/api/v1/orgs/${GITEA_ORG_NAME}/repos" 2>"$errfile") || { PHASE10_LAST_CURL_ERROR="$(tr '\n' ' ' < "$errfile" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" rm -f "$tmpfile" rm -f "$errfile" return 1 } create_response="$(cat "$tmpfile")" rm -f "$tmpfile" rm -f "$errfile" PHASE10_LAST_CURL_ERROR="" PHASE10_LAST_HTTP_CODE="$curl_code" return 0 } PHASE10_GITEA_REPO_EXISTS=false PHASE10_LAST_CURL_ERROR="" PHASE10_LAST_HTTP_CODE="" if ! get_gitea_repo_http_code "$repo_name"; then log_error "${repo_name}: failed to query Gitea API for repo existence" if [[ -n "$PHASE10_LAST_CURL_ERROR" ]]; then log_error "${repo_name}: curl error: ${PHASE10_LAST_CURL_ERROR}" fi return 1 fi if [[ "$PHASE10_LAST_HTTP_CODE" == "200" ]]; then PHASE10_GITEA_REPO_EXISTS=true if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: Gitea repo already exists (${GITEA_ORG_NAME}/${repo_name})" fi return 0 fi if [[ "$PHASE10_LAST_HTTP_CODE" != "404" ]]; then log_error "${repo_name}: unexpected Gitea API status while checking repo (${PHASE10_LAST_HTTP_CODE})" return 1 fi create_payload=$(jq -n \ --arg name "$repo_name" \ '{name: $name, auto_init: false}') if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would create missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}" return 0 fi log_info "${repo_name}: creating missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}" PHASE10_LAST_HTTP_CODE="" if ! create_gitea_repo "$create_payload"; then log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name} (network/API call failed)" if [[ -n "$PHASE10_LAST_CURL_ERROR" ]]; then log_error "${repo_name}: curl error: ${PHASE10_LAST_CURL_ERROR}" fi return 1 fi if [[ "$PHASE10_LAST_HTTP_CODE" == "201" ]]; then log_success "${repo_name}: created missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}" return 0 fi # If another process created the repo concurrently, treat it as success. if [[ "$PHASE10_LAST_HTTP_CODE" == "409" ]]; then log_warn "${repo_name}: Gitea repo already exists (HTTP 409), continuing" return 0 fi log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name} (HTTP ${PHASE10_LAST_HTTP_CODE})" if [[ -n "${create_response:-}" ]]; then log_error "${repo_name}: API response: ${create_response}" fi return 1 } count_items() { local list="$1" if [[ -z "$list" ]]; then printf '0' return fi printf '%s\n' "$list" | sed '/^$/d' | wc -l | tr -d '[:space:]' } list_contains() { local list="$1" needle="$2" [[ -n "$list" ]] && printf '%s\n' "$list" | grep -Fxq "$needle" } fetch_remote_refs() { local url="$1" local refs ref short rc PHASE10_REMOTE_BRANCHES="" PHASE10_REMOTE_TAGS="" refs="$(git_with_auth_timed "$PHASE10_LSREMOTE_TIMEOUT_SEC" \ git \ -c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \ -c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \ -c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \ ls-remote --heads --tags "$url" 2>/dev/null)" rc=$? if [[ "$rc" -ne 0 ]]; then return 1 fi [[ -n "$refs" ]] || return 0 while IFS= read -r line; do [[ -z "$line" ]] && continue ref="${line#*[[:space:]]}" ref="${ref#"${ref%%[![:space:]]*}"}" [[ -n "$ref" ]] || continue case "$ref" in refs/heads/*) short="${ref#refs/heads/}" PHASE10_REMOTE_BRANCHES="${PHASE10_REMOTE_BRANCHES}${short}"$'\n' ;; refs/tags/*) short="${ref#refs/tags/}" [[ "$short" == *"^{}" ]] && continue PHASE10_REMOTE_TAGS="${PHASE10_REMOTE_TAGS}${short}"$'\n' ;; esac done <<< "$refs" PHASE10_REMOTE_BRANCHES="$(printf '%s' "$PHASE10_REMOTE_BRANCHES" | sed '/^$/d' | LC_ALL=C sort -u)" PHASE10_REMOTE_TAGS="$(printf '%s' "$PHASE10_REMOTE_TAGS" | sed '/^$/d' | LC_ALL=C sort -u)" } print_diff_summary() { local repo_name="$1" kind="$2" local_list="$3" remote_list="$4" local missing_count extra_count item local missing_preview="" extra_preview="" local preview_limit=5 missing_count=0 while IFS= read -r item; do [[ -z "$item" ]] && continue if ! list_contains "$remote_list" "$item"; then missing_count=$((missing_count + 1)) if [[ "$missing_count" -le "$preview_limit" ]]; then missing_preview="${missing_preview}${item}, " fi fi done <<< "$local_list" extra_count=0 while IFS= read -r item; do [[ -z "$item" ]] && continue if ! list_contains "$local_list" "$item"; then extra_count=$((extra_count + 1)) if [[ "$extra_count" -le "$preview_limit" ]]; then extra_preview="${extra_preview}${item}, " fi fi done <<< "$remote_list" if [[ "$missing_count" -eq 0 ]] && [[ "$extra_count" -eq 0 ]]; then log_success "${repo_name}: local ${kind}s match Gitea" return 0 fi if [[ "$missing_count" -gt 0 ]]; then missing_preview="${missing_preview%, }" log_info "${repo_name}: ${missing_count} ${kind}(s) missing on Gitea" if [[ -n "$missing_preview" ]]; then log_info " missing ${kind} sample: ${missing_preview}" fi fi if [[ "$extra_count" -gt 0 ]]; then extra_preview="${extra_preview%, }" log_info "${repo_name}: ${extra_count} ${kind}(s) exist on Gitea but not locally" if [[ -n "$extra_preview" ]]; then log_info " remote-only ${kind} sample: ${extra_preview}" fi fi } dry_run_compare_local_and_remote() { local repo_path="$1" repo_name="$2" gitea_url="$3" local local_branches local_tags local local_branch_count local_tag_count remote_branch_count remote_tag_count local_branches="$(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads | LC_ALL=C sort -u)" local_tags="$(git -C "$repo_path" tag -l | LC_ALL=C sort -u)" local_branch_count="$(count_items "$local_branches")" local_tag_count="$(count_items "$local_tags")" log_info "${repo_name}: local state = ${local_branch_count} branch(es), ${local_tag_count} tag(s)" if [[ "$PHASE10_GITEA_REPO_EXISTS" != "true" ]]; then log_info "${repo_name}: remote state = repo missing (would be created)" if [[ "$local_branch_count" -gt 0 ]]; then log_info "${repo_name}: all local branches would be pushed to new Gitea repo" fi if [[ "$local_tag_count" -gt 0 ]]; then log_info "${repo_name}: all local tags would be pushed to new Gitea repo" fi return 0 fi log_info "${repo_name}: reading remote refs from Gitea (timeout ${PHASE10_LSREMOTE_TIMEOUT_SEC}s)" if ! fetch_remote_refs "$gitea_url"; then log_warn "${repo_name}: could not read Gitea refs via ls-remote; skipping diff" return 0 fi remote_branch_count="$(count_items "$PHASE10_REMOTE_BRANCHES")" remote_tag_count="$(count_items "$PHASE10_REMOTE_TAGS")" log_info "${repo_name}: remote Gitea state = ${remote_branch_count} branch(es), ${remote_tag_count} tag(s)" print_diff_summary "$repo_name" "branch" "$local_branches" "$PHASE10_REMOTE_BRANCHES" print_diff_summary "$repo_name" "tag" "$local_tags" "$PHASE10_REMOTE_TAGS" } push_all_refs_to_origin() { local repo_path="$1" repo_name="$2" local push_output push_args push_rc if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would push all branches to origin" log_info "${repo_name}: would push all tags to origin" return 0 fi push_args=(push --no-verify --all origin) if [[ "$FORCE_WITH_LEASE" == "true" ]]; then push_args=(push --no-verify --force-with-lease --all origin) fi log_info "${repo_name}: pushing branches to origin (timeout ${PHASE10_PUSH_TIMEOUT_SEC}s)" push_output="$(git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \ git \ -c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \ -c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \ -c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \ -C "$repo_path" "${push_args[@]}" 2>&1)" push_rc=$? if [[ "$push_rc" -ne 0 ]]; then if [[ "$push_rc" -eq 124 ]]; then log_error "${repo_name}: branch push timed out after ${PHASE10_PUSH_TIMEOUT_SEC}s" log_error "${repo_name}: check network reachability to ${GITEA_DOMAIN} and retry" return 1 fi if [[ "$push_output" == *"non-fast-forward"* ]] || [[ "$push_output" == *"[rejected]"* ]]; then log_error "${repo_name}: branch push rejected (non-fast-forward)" log_error "${repo_name}: run with --dry-run first to review diffs, then re-run with --force-with-lease if local should win" else log_error "${repo_name}: failed pushing branches to Gitea origin" fi printf '%s\n' "$push_output" >&2 return 1 fi push_args=(push --no-verify --tags origin) if [[ "$FORCE_WITH_LEASE" == "true" ]]; then push_args=(push --no-verify --force-with-lease --tags origin) fi log_info "${repo_name}: pushing tags to origin (timeout ${PHASE10_PUSH_TIMEOUT_SEC}s)" push_output="$(git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \ git \ -c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \ -c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \ -c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \ -C "$repo_path" "${push_args[@]}" 2>&1)" push_rc=$? if [[ "$push_rc" -ne 0 ]]; then if [[ "$push_rc" -eq 124 ]]; then log_error "${repo_name}: tag push timed out after ${PHASE10_PUSH_TIMEOUT_SEC}s" log_error "${repo_name}: check network reachability to ${GITEA_DOMAIN} and retry" return 1 fi if [[ "$push_output" == *"non-fast-forward"* ]] || [[ "$push_output" == *"[rejected]"* ]]; then log_error "${repo_name}: tag push rejected (non-fast-forward/conflict)" log_error "${repo_name}: re-run with --force-with-lease only if replacing remote tags is intended" else log_error "${repo_name}: failed pushing tags to Gitea origin" fi printf '%s\n' "$push_output" >&2 return 1 fi return 0 } retarget_tracking_to_origin() { local repo_path="$1" repo_name="$2" local branch upstream_remote upstream_short branch_count branch_count=0 while IFS= read -r branch; do [[ -z "$branch" ]] && continue branch_count=$((branch_count + 1)) if ! git -C "$repo_path" show-ref --verify --quiet "refs/remotes/origin/${branch}"; then # A local branch can exist without an origin ref if it never got pushed. if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would create origin/${branch} by pushing local ${branch}" else if ! git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \ git \ -c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \ -c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \ -c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \ -C "$repo_path" push --no-verify origin "refs/heads/${branch}:refs/heads/${branch}" >/dev/null; then log_error "${repo_name}: could not create origin/${branch} while setting tracking" return 1 fi fi fi if [[ "$DRY_RUN" == "true" ]]; then log_info "${repo_name}: would set upstream ${branch} -> origin/${branch}" continue else if ! git -C "$repo_path" branch --set-upstream-to="origin/${branch}" "$branch" >/dev/null 2>&1; then log_error "${repo_name}: failed to set upstream for branch '${branch}' to origin/${branch}" return 1 fi upstream_remote=$(git -C "$repo_path" for-each-ref --format='%(upstream:remotename)' "refs/heads/${branch}") upstream_short=$(git -C "$repo_path" for-each-ref --format='%(upstream:short)' "refs/heads/${branch}") if [[ "$upstream_remote" != "origin" ]] || [[ "$upstream_short" != "origin/${branch}" ]]; then log_error "${repo_name}: branch '${branch}' upstream is '${upstream_short:-}' (expected origin/${branch})" return 1 fi fi done < <(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads) if [[ "$branch_count" -eq 0 ]]; then log_warn "${repo_name}: no local branches found" fi return 0 } if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" 0; then exit 1 fi for include_path in "${INCLUDE_PATHS[@]}"; do [[ -z "$include_path" ]] && continue if ! phase10_include_repo_path "$include_path" "$GITHUB_USERNAME"; then exit 1 fi done phase10_sort_repo_arrays if ! phase10_enforce_expected_count "$EXPECTED_REPO_COUNT" "$LOCAL_REPO_ROOT"; then exit 1 fi log_info "Discovered ${#PHASE10_REPO_NAMES[@]} local repos in ${LOCAL_REPO_ROOT}" for i in "${!PHASE10_REPO_NAMES[@]}"; do log_info " - ${PHASE10_REPO_NAMES[$i]} -> ${PHASE10_REPO_PATHS[$i]}" done setup_git_auth SUCCESS=0 FAILED=0 for i in "${!PHASE10_REPO_NAMES[@]}"; do repo_name="${PHASE10_REPO_NAMES[$i]}" repo_path="${PHASE10_REPO_PATHS[$i]}" github_url="${PHASE10_GITHUB_URLS[$i]}" gitea_url="$(phase10_canonical_gitea_url "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name")" log_info "--- Processing repo: ${repo_name} (${repo_path}) ---" if ! ensure_github_remote "$repo_path" "$repo_name" "$github_url"; then FAILED=$((FAILED + 1)) continue fi if ! ensure_gitea_repo_exists "$repo_name"; then FAILED=$((FAILED + 1)) continue fi if [[ "$DRY_RUN" == "true" ]]; then dry_run_compare_local_and_remote "$repo_path" "$repo_name" "$gitea_url" fi if ! ensure_gitea_origin "$repo_path" "$repo_name" "$gitea_url"; then log_error "${repo_name}: failed to set origin to ${gitea_url}" FAILED=$((FAILED + 1)) continue fi if ! push_all_refs_to_origin "$repo_path" "$repo_name"; then FAILED=$((FAILED + 1)) continue fi if ! retarget_tracking_to_origin "$repo_path" "$repo_name"; then FAILED=$((FAILED + 1)) continue fi if [[ "$DRY_RUN" == "true" ]]; then log_success "${repo_name}: dry-run plan complete" else log_success "${repo_name}: origin now points to Gitea and tracking updated" fi SUCCESS=$((SUCCESS + 1)) done printf '\n' TOTAL=${#PHASE10_REPO_NAMES[@]} log_info "Results: ${SUCCESS} succeeded, ${FAILED} failed (out of ${TOTAL})" if [[ "$DRY_RUN" == "true" ]]; then if [[ "$FAILED" -gt 0 ]]; then log_error "Phase 10 dry-run found ${FAILED} error(s); no changes were made" exit 1 fi log_success "Phase 10 dry-run complete — no changes were made" exit 0 fi if [[ "$FAILED" -gt 0 ]]; then log_error "Phase 10 failed for one or more repos" exit 1 fi log_success "Phase 10 complete — local repos now push/track via Gitea origin"