733 lines
24 KiB
Bash
Executable File
733 lines
24 KiB
Bash
Executable File
#!/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/<branch> (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}/<repo>
|
|
# - 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 <<EOF
|
|
Usage: $(basename "$0") [options]
|
|
|
|
Options:
|
|
--local-root=PATH Root folder containing local repos (default: /Users/s/development)
|
|
--expected-count=N Require exactly N discovered repos (default: 3, 0 disables)
|
|
--include-path=PATH Explicit repo root to include (repeatable)
|
|
--dry-run Print planned actions only (no mutations)
|
|
--force-with-lease Use force-with-lease when pushing branches/tags to Gitea
|
|
--help Show this help
|
|
EOF
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown argument: $arg"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if ! [[ "$EXPECTED_REPO_COUNT" =~ ^[0-9]+$ ]]; then
|
|
log_error "--expected-count must be a non-negative integer"
|
|
exit 1
|
|
fi
|
|
for timeout_var in PHASE10_HTTP_CONNECT_TIMEOUT PHASE10_HTTP_LOW_SPEED_LIMIT PHASE10_HTTP_LOW_SPEED_TIME \
|
|
PHASE10_PUSH_TIMEOUT_SEC PHASE10_LSREMOTE_TIMEOUT_SEC \
|
|
PHASE10_API_CONNECT_TIMEOUT_SEC PHASE10_API_MAX_TIME_SEC; do
|
|
if ! [[ "${!timeout_var}" =~ ^[0-9]+$ ]] || [[ "${!timeout_var}" -le 0 ]]; then
|
|
log_error "${timeout_var} must be a positive integer"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
cleanup() {
|
|
if [[ -n "$ASKPASS_SCRIPT" ]]; then
|
|
rm -f "$ASKPASS_SCRIPT"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
setup_git_auth() {
|
|
ASKPASS_SCRIPT=$(mktemp)
|
|
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:-<none>}' (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"
|