Files
gitea-migration/lib/phase10_common.sh

328 lines
9.9 KiB
Bash

#!/usr/bin/env bash
# =============================================================================
# lib/phase10_common.sh — Shared helpers for phase 10 local repo cutover
# =============================================================================
# Shared discovery results (parallel arrays; bash 3.2 compatible).
PHASE10_REPO_NAMES=()
PHASE10_REPO_PATHS=()
PHASE10_GITHUB_URLS=()
PHASE10_DUPLICATES=()
phase10_repo_index_by_name() {
local repo_name="$1"
local i
for i in "${!PHASE10_REPO_NAMES[@]}"; do
if [[ "${PHASE10_REPO_NAMES[$i]}" == "$repo_name" ]]; then
printf '%s' "$i"
return 0
fi
done
printf '%s' "-1"
}
# Parse common git remote URL formats into: host|owner|repo
# Supports:
# - https://host/owner/repo(.git)
# - ssh://git@host/owner/repo(.git)
# - git@host:owner/repo(.git)
phase10_parse_git_url() {
local url="$1"
local rest host path owner repo
if [[ "$url" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
rest="${url#*://}"
# Drop optional userinfo component.
rest="${rest#*@}"
host="${rest%%/*}"
path="${rest#*/}"
elif [[ "$url" == *@*:* ]]; then
rest="${url#*@}"
host="${rest%%:*}"
path="${rest#*:}"
else
return 1
fi
path="${path#/}"
path="${path%.git}"
owner="${path%%/*}"
repo="${path#*/}"
repo="${repo%%/*}"
if [[ -z "$host" ]] || [[ -z "$owner" ]] || [[ -z "$repo" ]] || [[ "$owner" == "$path" ]]; then
return 1
fi
printf '%s|%s|%s\n' "$host" "$owner" "$repo"
}
phase10_host_matches() {
local host="$1" expected="$2"
[[ "$host" == "$expected" ]] || [[ "$host" == "${expected}:"* ]]
}
# Return 0 when URL matches github.com/<owner>/<repo>.
# If <repo> is omitted, only owner is checked.
phase10_url_is_github_repo() {
local url="$1" owner_expected="$2" repo_expected="${3:-}"
local parsed host owner repo
parsed=$(phase10_parse_git_url "$url" 2>/dev/null) || return 1
IFS='|' read -r host owner repo <<< "$parsed"
phase10_host_matches "$host" "github.com" || return 1
[[ "$owner" == "$owner_expected" ]] || return 1
if [[ -n "$repo_expected" ]] && [[ "$repo" != "$repo_expected" ]]; then
return 1
fi
return 0
}
phase10_url_is_gitea_repo() {
local url="$1" domain="$2" org="$3" repo_expected="$4"
local parsed host owner repo
parsed=$(phase10_parse_git_url "$url" 2>/dev/null) || return 1
IFS='|' read -r host owner repo <<< "$parsed"
phase10_host_matches "$host" "$domain" || return 1
[[ "$owner" == "$org" ]] || return 1
[[ "$repo" == "$repo_expected" ]] || return 1
}
phase10_canonical_github_url() {
local owner="$1" repo="$2"
printf 'https://github.com/%s/%s.git' "$owner" "$repo"
}
phase10_canonical_gitea_url() {
local domain="$1" org="$2" repo="$3"
printf 'https://%s/%s/%s.git' "$domain" "$org" "$repo"
}
# Resolve which local remote currently represents GitHub for this repo path.
# Prefers "github" remote, then "origin".
phase10_find_github_remote_url() {
local repo_path="$1" github_owner="$2"
local github_url=""
if github_url=$(git -C "$repo_path" remote get-url github 2>/dev/null); then
if phase10_url_is_github_repo "$github_url" "$github_owner"; then
printf '%s' "$github_url"
return 0
fi
fi
if github_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null); then
if phase10_url_is_github_repo "$github_url" "$github_owner"; then
printf '%s' "$github_url"
return 0
fi
fi
return 1
}
# Add or update a discovered repo entry.
# If repo already exists and path differs, explicit path wins.
phase10_upsert_repo_entry() {
local repo_name="$1" repo_path="$2" github_url="$3"
local idx existing_path
idx="$(phase10_repo_index_by_name "$repo_name")"
if [[ "$idx" -ge 0 ]]; then
existing_path="${PHASE10_REPO_PATHS[$idx]}"
if [[ "$existing_path" != "$repo_path" ]]; then
PHASE10_REPO_PATHS[idx]="$repo_path"
PHASE10_GITHUB_URLS[idx]="$github_url"
log_info "${repo_name}: using explicit include path ${repo_path} (replacing ${existing_path})"
fi
return 0
fi
PHASE10_REPO_NAMES+=("$repo_name")
PHASE10_REPO_PATHS+=("$repo_path")
PHASE10_GITHUB_URLS+=("$github_url")
return 0
}
# Add one explicitly included repo path into discovery arrays.
# Validates path is a git toplevel and maps to github.com/<owner>/repo.
phase10_include_repo_path() {
local include_path="$1" github_owner="$2"
local abs_path top github_url parsed host owner repo canonical
if [[ ! -d "$include_path" ]]; then
log_error "Include path not found: ${include_path}"
return 1
fi
abs_path="$(cd "$include_path" && pwd)"
if ! git -C "$abs_path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
log_error "Include path is not a git repo: ${abs_path}"
return 1
fi
top="$(git -C "$abs_path" rev-parse --show-toplevel 2>/dev/null || true)"
if [[ "$top" != "$abs_path" ]]; then
log_error "Include path must be repo root (git toplevel): ${abs_path}"
return 1
fi
github_url="$(phase10_find_github_remote_url "$abs_path" "$github_owner" 2>/dev/null || true)"
if [[ -z "$github_url" ]]; then
# Explicit include-path may point to a local repo with no GitHub remote yet.
# In that case, derive the repo slug from folder name and assume GitHub URL.
repo="$(basename "$abs_path")"
canonical="$(phase10_canonical_github_url "$github_owner" "$repo")"
log_warn "Include path has no GitHub remote; assuming ${canonical}"
else
parsed=$(phase10_parse_git_url "$github_url" 2>/dev/null) || {
log_error "Could not parse GitHub remote URL for include path: ${abs_path}"
return 1
}
IFS='|' read -r host owner repo <<< "$parsed"
canonical="$(phase10_canonical_github_url "$owner" "$repo")"
fi
phase10_upsert_repo_entry "$repo" "$abs_path" "$canonical"
return 0
}
phase10_enforce_expected_count() {
local expected_count="$1" root="$2"
local i
if [[ "$expected_count" -gt 0 ]] && [[ "${#PHASE10_REPO_NAMES[@]}" -ne "$expected_count" ]]; then
log_error "Expected ${expected_count} local repos under ${root}; found ${#PHASE10_REPO_NAMES[@]}"
for i in "${!PHASE10_REPO_NAMES[@]}"; do
log_error " - ${PHASE10_REPO_NAMES[$i]} -> ${PHASE10_REPO_PATHS[$i]}"
done
return 1
fi
return 0
}
# Stable in-place sort by repo name (keeps arrays aligned).
phase10_sort_repo_arrays() {
local i j tmp
for ((i = 0; i < ${#PHASE10_REPO_NAMES[@]}; i++)); do
for ((j = i + 1; j < ${#PHASE10_REPO_NAMES[@]}; j++)); do
if [[ "${PHASE10_REPO_NAMES[$i]}" > "${PHASE10_REPO_NAMES[$j]}" ]]; then
tmp="${PHASE10_REPO_NAMES[$i]}"
PHASE10_REPO_NAMES[i]="${PHASE10_REPO_NAMES[j]}"
PHASE10_REPO_NAMES[j]="$tmp"
tmp="${PHASE10_REPO_PATHS[i]}"
PHASE10_REPO_PATHS[i]="${PHASE10_REPO_PATHS[j]}"
PHASE10_REPO_PATHS[j]="$tmp"
tmp="${PHASE10_GITHUB_URLS[i]}"
PHASE10_GITHUB_URLS[i]="${PHASE10_GITHUB_URLS[j]}"
PHASE10_GITHUB_URLS[j]="$tmp"
fi
done
done
}
# Discover local repos under root that map to github.com/<github_owner>.
# Discovery rules:
# - Only direct children of root are considered.
# - Excludes exclude_path (typically this toolkit repo).
# - Accepts a repo if either "github" or "origin" points at GitHub owner.
# - Deduplicates by repo slug, preferring directory basename == slug.
#
# Args:
# $1 root dir (e.g., /Users/s/development)
# $2 github owner (from GITHUB_USERNAME)
# $3 exclude absolute path (optional; pass "" for none)
# $4 expected count (0 = don't enforce)
phase10_discover_local_repos() {
local root="$1"
local github_owner="$2"
local exclude_path="${3:-}"
local expected_count="${4:-0}"
PHASE10_REPO_NAMES=()
PHASE10_REPO_PATHS=()
PHASE10_GITHUB_URLS=()
PHASE10_DUPLICATES=()
if [[ ! -d "$root" ]]; then
log_error "Local repo root not found: ${root}"
return 1
fi
local dir top github_url parsed host owner repo canonical
local i idx existing existing_base new_base duplicate
for dir in "$root"/*; do
[[ -d "$dir" ]] || continue
if [[ -n "$exclude_path" ]] && [[ "$dir" == "$exclude_path" ]]; then
continue
fi
if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
continue
fi
top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || true)
[[ "$top" == "$dir" ]] || continue
github_url="$(phase10_find_github_remote_url "$dir" "$github_owner" 2>/dev/null || true)"
[[ -n "$github_url" ]] || continue
parsed=$(phase10_parse_git_url "$github_url" 2>/dev/null) || continue
IFS='|' read -r host owner repo <<< "$parsed"
canonical=$(phase10_canonical_github_url "$owner" "$repo")
idx=-1
for i in "${!PHASE10_REPO_NAMES[@]}"; do
if [[ "${PHASE10_REPO_NAMES[$i]}" == "$repo" ]]; then
idx="$i"
break
fi
done
if [[ "$idx" -ge 0 ]]; then
existing="${PHASE10_REPO_PATHS[$idx]}"
existing_base="$(basename "$existing")"
new_base="$(basename "$dir")"
if [[ "$new_base" == "$repo" ]] && [[ "$existing_base" != "$repo" ]]; then
PHASE10_REPO_PATHS[idx]="$dir"
PHASE10_GITHUB_URLS[idx]="$canonical"
PHASE10_DUPLICATES+=("${repo}: preferred ${dir} over ${existing}")
else
PHASE10_DUPLICATES+=("${repo}: ignored duplicate ${dir} (using ${existing})")
fi
continue
fi
PHASE10_REPO_NAMES+=("$repo")
PHASE10_REPO_PATHS+=("$dir")
PHASE10_GITHUB_URLS+=("$canonical")
done
phase10_sort_repo_arrays
for duplicate in "${PHASE10_DUPLICATES[@]}"; do
log_info "$duplicate"
done
if [[ "${#PHASE10_REPO_NAMES[@]}" -eq 0 ]]; then
log_error "No local GitHub repos found under ${root} for owner '${github_owner}'"
return 1
fi
if [[ "$expected_count" -gt 0 ]] && [[ "${#PHASE10_REPO_NAMES[@]}" -ne "$expected_count" ]]; then
log_error "Expected ${expected_count} local repos under ${root}; found ${#PHASE10_REPO_NAMES[@]}"
for i in "${!PHASE10_REPO_NAMES[@]}"; do
log_error " - ${PHASE10_REPO_NAMES[$i]} -> ${PHASE10_REPO_PATHS[$i]}"
done
return 1
fi
return 0
}