#!/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=() # 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//. # If 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" } # 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/. # 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="" if github_url=$(git -C "$dir" remote get-url github 2>/dev/null); then if ! phase10_url_is_github_repo "$github_url" "$github_owner"; then github_url="" fi fi if [[ -z "$github_url" ]] && github_url=$(git -C "$dir" remote get-url origin 2>/dev/null); then if ! phase10_url_is_github_repo "$github_url" "$github_owner"; then github_url="" fi fi [[ -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 }