#!/usr/bin/env bash set -euo pipefail DRY_RUN=0 FORCE_SWITCH=0 INIT_MISSING=0 ROOT_DIR="." ERRORS=0 COUNT_REPOS=0 COUNT_SUBMODULES=0 COUNT_FIXED=0 COUNT_SKIPPED=0 COUNT_ALREADY_OK=0 COUNT_OTHER_BRANCH=0 COUNT_INITIALIZED=0 COUNT_DIRTY=0 log() { printf '%s\n' "$*" } warn() { printf 'WARN: %s\n' "$*" >&2 } err() { printf 'ERROR: %s\n' "$*" >&2 } on_error() { local exit_code=$? err "Abbruch in Zeile $1 mit Exit-Code $exit_code" exit "$exit_code" } trap 'on_error $LINENO' ERR run_cmd() { if [[ "$DRY_RUN" -eq 1 ]]; then printf '[dry-run] ' printf '%q ' "$@" printf '\n' return 0 fi "$@" } is_dirty_repo() { local repo_dir="$1" [[ -n "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]] } get_default_branch() { local repo_dir="$1" local head_ref default_branch head_ref="$(git -C "$repo_dir" symbolic-ref -q refs/remotes/origin/HEAD 2>/dev/null || true)" if [[ -n "$head_ref" ]]; then default_branch="${head_ref#refs/remotes/origin/}" printf '%s\n' "$default_branch" return 0 fi git -C "$repo_dir" fetch origin >/dev/null 2>&1 || true head_ref="$(git -C "$repo_dir" symbolic-ref -q refs/remotes/origin/HEAD 2>/dev/null || true)" if [[ -n "$head_ref" ]]; then default_branch="${head_ref#refs/remotes/origin/}" printf '%s\n' "$default_branch" return 0 fi default_branch="$(git -C "$repo_dir" remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p' | head -n1 || true)" if [[ -n "$default_branch" ]]; then printf '%s\n' "$default_branch" return 0 fi return 1 } init_submodule_if_needed() { local repo_dir="$1" local sub_path="$2" local submodule_abs="$repo_dir/$sub_path" if [[ -d "$submodule_abs/.git" || -f "$submodule_abs/.git" ]]; then return 0 fi if [[ "$INIT_MISSING" -ne 1 ]]; then return 1 fi log " Initialisiere fehlendes Submodule: $sub_path" if run_cmd git -C "$repo_dir" submodule update --init -- "$sub_path"; then COUNT_INITIALIZED=$((COUNT_INITIALIZED + 1)) return 0 fi return 1 } checkout_target_branch() { local submodule_abs="$1" local resolved_branch="$2" run_cmd git -C "$submodule_abs" fetch origin "$resolved_branch" >/dev/null 2>&1 || true if git -C "$submodule_abs" show-ref --verify --quiet "refs/heads/$resolved_branch"; then log " Checke lokalen Branch '$resolved_branch' aus" run_cmd git -C "$submodule_abs" checkout "$resolved_branch" return 0 fi if git -C "$submodule_abs" show-ref --verify --quiet "refs/remotes/origin/$resolved_branch"; then log " Erzeuge Tracking-Branch '$resolved_branch' von origin/$resolved_branch" run_cmd git -C "$submodule_abs" checkout -b "$resolved_branch" --track "origin/$resolved_branch" return 0 fi return 1 } process_repo() { local repo_dir="$1" local gitmodules_file="$repo_dir/.gitmodules" [[ -f "$gitmodules_file" ]] || return 0 COUNT_REPOS=$((COUNT_REPOS + 1)) log "Prüfe Repository: $repo_dir" local entries=() while IFS= read -r line; do entries+=("$line") done < <(git -C "$repo_dir" config -f "$gitmodules_file" --get-regexp '^submodule\..*\.path$' 2>/dev/null || true) local entry key sub_path sub_name branch submodule_abs current_branch resolved_branch for entry in "${entries[@]}"; do COUNT_SUBMODULES=$((COUNT_SUBMODULES + 1)) key="${entry%% *}" sub_path="${entry#* }" sub_name="${key#submodule.}" sub_name="${sub_name%.path}" submodule_abs="$repo_dir/$sub_path" if [[ ! -e "$submodule_abs" ]]; then if [[ "$INIT_MISSING" -eq 1 ]]; then log " Submodule '$sub_name' ($sub_path): Pfad fehlt, versuche Initialisierung" else err "Submodule '$sub_name' ($sub_path): Pfad existiert nicht" ERRORS=1 COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) continue fi fi if ! init_submodule_if_needed "$repo_dir" "$sub_path"; then err "Submodule '$sub_name' ($sub_path): nicht initialisiert" ERRORS=1 COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) continue fi if is_dirty_repo "$submodule_abs"; then warn "Submodule '$sub_name' ($sub_path): uncommitted changes vorhanden, überspringe" COUNT_DIRTY=$((COUNT_DIRTY + 1)) COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) process_repo "$submodule_abs" continue fi branch="$(git -C "$repo_dir" config -f "$gitmodules_file" --get "submodule.${sub_name}.branch" 2>/dev/null || true)" if [[ -n "$branch" ]]; then resolved_branch="$branch" log " Submodule '$sub_name' ($sub_path): branch aus .gitmodules = $resolved_branch" else if resolved_branch="$(get_default_branch "$submodule_abs")"; then log " Submodule '$sub_name' ($sub_path): kein branch in .gitmodules, nehme Default-Branch = $resolved_branch" else err "Submodule '$sub_name' ($sub_path): weder branch in .gitmodules noch Default-Branch von origin ermittelbar" ERRORS=1 COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) process_repo "$submodule_abs" continue fi fi current_branch="$(git -C "$submodule_abs" symbolic-ref --short -q HEAD 2>/dev/null || true)" if [[ -n "$current_branch" ]]; then if [[ "$current_branch" == "$resolved_branch" ]]; then log " Bereits auf richtigem Branch: $current_branch" COUNT_ALREADY_OK=$((COUNT_ALREADY_OK + 1)) else log " Auf anderem Branch: $current_branch" if [[ "$FORCE_SWITCH" -eq 1 ]]; then log " --force-switch aktiv, wechsle auf '$resolved_branch'" if checkout_target_branch "$submodule_abs" "$resolved_branch"; then COUNT_FIXED=$((COUNT_FIXED + 1)) else err "Submodule '$sub_name' ($sub_path): Branch '$resolved_branch' existiert weder lokal noch als origin/$resolved_branch" ERRORS=1 COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) fi else log " Keine Änderung, da nur detached HEADs umgestellt werden" COUNT_OTHER_BRANCH=$((COUNT_OTHER_BRANCH + 1)) fi fi else log " Detached HEAD erkannt" if checkout_target_branch "$submodule_abs" "$resolved_branch"; then COUNT_FIXED=$((COUNT_FIXED + 1)) else err "Submodule '$sub_name' ($sub_path): Branch '$resolved_branch' existiert weder lokal noch als origin/$resolved_branch" ERRORS=1 COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) fi fi process_repo "$submodule_abs" done } usage() { cat <<'EOF' Usage: fix-submodule-branches-prod.sh [options] [root-dir] Optionen: -n, --dry-run Nur anzeigen, was getan würde --force-switch Auch von falschem Branch auf Ziel-Branch wechseln --init-missing Nicht initialisierte Submodule automatisch initen -h, --help Hilfe anzeigen EOF } while [[ $# -gt 0 ]]; do case "$1" in -n|--dry-run) DRY_RUN=1 ;; --force-switch) FORCE_SWITCH=1 ;; --init-missing) INIT_MISSING=1 ;; -h|--help) usage exit 0 ;; *) ROOT_DIR="$1" ;; esac shift done process_repo "$ROOT_DIR" log "" log "Zusammenfassung:" log " Repositories mit .gitmodules geprüft: $COUNT_REPOS" log " Submodule insgesamt: $COUNT_SUBMODULES" log " Detached/falsch Branch gefixt: $COUNT_FIXED" log " Bereits korrekt auf Branch: $COUNT_ALREADY_OK" log " Auf anderem Branch belassen: $COUNT_OTHER_BRANCH" log " Initialisierte Submodule: $COUNT_INITIALIZED" log " Dirty Submodule übersprungen: $COUNT_DIRTY" log " Übersprungen/Fehler: $COUNT_SKIPPED" if [[ "$ERRORS" -ne 0 ]]; then log "Status: Fertig mit Fehlern" exit 1 fi log "Status: Fertig ohne Fehler"