Files

284 lines
7.7 KiB
Bash
Executable File

#!/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"