mirror of
https://github.com/X11Libre/xserver.git
synced 2026-03-24 18:54:38 +00:00
pull submodules if existing Signed-off-by: Enrico Weigelt, metux IT consult <info@metux.net>
269 lines
9.1 KiB
Bash
Executable File
269 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# git-smart-checkout: clone/reuse a repo and force-checkout a given ref (branch/tag/commit),
|
|
# with mirror URLs, minimal transfer, and automatic retries.
|
|
|
|
# Robustness
|
|
set -euo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
# ---- Logging helpers --------------------------------------------------------
|
|
log_info() { printf '[INFO] %s\n' "$*" >&2; }
|
|
log_warn() { printf '[WARN] %s\n' "$*" >&2; }
|
|
log_error() { printf '[ERROR] %s\n' "$*" >&2; }
|
|
|
|
# ---- Usage ------------------------------------------------------------------
|
|
usage() {
|
|
cat >&2 <<'USAGE'
|
|
Usage:
|
|
git-smart-checkout.sh -n <local-dir> -r <ref|commit> -u <url> [-u <url2> ...] [options]
|
|
|
|
Required:
|
|
-n, --name <dir> local target directory (repo / path).
|
|
-r, --ref <ref> branch, tag or Commit-SHA.
|
|
-u, --url <url> repo URL (multiple for mirror fallbacks)
|
|
|
|
Options:
|
|
--retries <N> amount of retries (default: 3).
|
|
--sleep <SEC> wait seconds between retries (default: 3).
|
|
--no-clean no hard working dir cleanup after checkout
|
|
--full-if-needed if commit not reachable with shallow fetch, retry again without --depth
|
|
--keep-remote dont change remote config (otherwise set 'origin' to first URL).
|
|
--depth <N> shallow depth for fetch (default: 1). only useful for branch/tag
|
|
--no-partial disable partial clone/filter (otherwise load blobs on-demand).
|
|
--no-tags dont fetch tags by default (default).
|
|
--allow-tags fetch tags (overwrites --no-tags).
|
|
|
|
Examples:
|
|
git-smart-checkout.sh -n myrepo -r main -u https://github.com/org/proj.git -u https://gitmirror/org/proj.git
|
|
git-smart-checkout.sh -n myrepo -r v2.1.0 -u ssh://git@example.com/proj.git
|
|
git-smart-checkout.sh -n myrepo -r 1a2b3c4d5e --full-if-needed -u https://github.com/org/proj.git
|
|
USAGE
|
|
exit 2
|
|
}
|
|
|
|
# ---- Defaults ---------------------------------------------------------------
|
|
NAME=""
|
|
REF=""
|
|
URLS=()
|
|
RETRIES=3
|
|
SLEEP_SEC=3
|
|
CLEAN=1
|
|
FULL_IF_NEEDED=0
|
|
KEEP_REMOTE=0
|
|
DEPTH=1
|
|
USE_PARTIAL=1
|
|
FETCH_TAGS=0 # 0 => --no-tags, 1 => allow tags
|
|
|
|
# ---- Parse args -------------------------------------------------------------
|
|
if [[ $# -eq 0 ]]; then usage; fi
|
|
|
|
while (( "$#" )); do
|
|
case "$1" in
|
|
-n|--name) NAME="${2:-}"; shift 2 ;;
|
|
-r|--ref) REF="${2:-}"; shift 2 ;;
|
|
-u|--url) URLS+=("${2:-}"); shift 2 ;;
|
|
--retries) RETRIES="${2:-}"; shift 2 ;;
|
|
--sleep) SLEEP_SEC="${2:-}"; shift 2 ;;
|
|
--no-clean) CLEAN=0; shift ;;
|
|
--full-if-needed) FULL_IF_NEEDED=1; shift ;;
|
|
--keep-remote) KEEP_REMOTE=1; shift ;;
|
|
--depth) DEPTH="${2:-}"; shift 2 ;;
|
|
--no-partial) USE_PARTIAL=0; shift ;;
|
|
--no-tags) FETCH_TAGS=0; shift ;;
|
|
--allow-tags) FETCH_TAGS=1; shift ;;
|
|
-h|--help) usage ;;
|
|
*) log_error "Unknown option: $1"; usage ;;
|
|
esac
|
|
done
|
|
|
|
# ---- Validate args ----------------------------------------------------------
|
|
if [[ -z "$NAME" ]]; then log_error "Missing --name"; usage; fi
|
|
if [[ -z "$REF" ]]; then log_error "Missing --ref"; usage; fi
|
|
if [[ ${#URLS[@]} -eq 0 ]]; then log_error "At least one --url is required"; usage; fi
|
|
if ! command -v git >/dev/null 2>&1; then log_error "'git' not found"; exit 127; fi
|
|
|
|
# ---- Helpers ----------------------------------------------------------------
|
|
is_git_dir() {
|
|
[[ -d "$1/.git" ]] || git -C "$1" rev-parse --git-dir >/dev/null 2>&1
|
|
}
|
|
|
|
is_sha_like() {
|
|
[[ "$1" =~ ^[0-9a-fA-F]{7,40}$ ]]
|
|
}
|
|
|
|
sleep_backoff() {
|
|
local attempt="$1"
|
|
local base="$SLEEP_SEC"
|
|
local delay=$(( base * attempt ))
|
|
[[ $delay -lt 1 ]] && delay=1
|
|
sleep "$delay"
|
|
}
|
|
|
|
git_retry() {
|
|
# git_retry <desc> <cmd...>
|
|
local desc="$1"; shift
|
|
local attempt=1
|
|
local rc=0
|
|
while :; do
|
|
if "$@"; then
|
|
return 0
|
|
fi
|
|
rc=$?
|
|
if (( attempt >= RETRIES )); then
|
|
log_error "$desc: failed after $RETRIES tries (rc=$rc)"
|
|
return "$rc"
|
|
fi
|
|
log_warn "$desc: try $attempt failed (rc=$rc). retrying ..."
|
|
sleep_backoff "$attempt"
|
|
attempt=$(( attempt + 1 ))
|
|
done
|
|
}
|
|
|
|
# Compose common fetch flags
|
|
fetch_flags_common=()
|
|
(( USE_PARTIAL )) && fetch_flags_common+=( "--filter=blob:none" )
|
|
(( FETCH_TAGS == 0 )) && fetch_flags_common+=( "--no-tags" )
|
|
fetch_flags_common+=( "--prune" "--prune-tags" "--update-head-ok" "--force" )
|
|
|
|
# With or without depth
|
|
fetch_flags_shallow=( "${fetch_flags_common[@]}" "--depth=$DEPTH" )
|
|
fetch_flags_full=( "${fetch_flags_common[@]}" )
|
|
|
|
# ---- Clone or reuse existing ------------------------------------------------
|
|
if [[ -d "$NAME" ]]; then
|
|
if is_git_dir "$NAME"; then
|
|
log_info "detected existing git repository: $NAME"
|
|
if (( KEEP_REMOTE == 0 )); then
|
|
log_info "setting 'origin' to first URL: ${URLS[0]}"
|
|
if git -C "$NAME" remote get-url origin >/dev/null 2>&1; then
|
|
git -C "$NAME" remote set-url origin "${URLS[0]}"
|
|
else
|
|
git -C "$NAME" remote add origin "${URLS[0]}"
|
|
fi
|
|
else
|
|
log_info "--keep-remote: remote config unchanged."
|
|
fi
|
|
else
|
|
log_error "directory '$NAME' exists, but not a git repository."
|
|
exit 1
|
|
fi
|
|
else
|
|
# fresh clone: minimal & no-checkout
|
|
mkdir -p "$(dirname -- "$NAME")"
|
|
clone_ok=0
|
|
for u in "${URLS[@]}"; do
|
|
log_info "Clone von $u -> $NAME (minimaler Transfer, no-checkout)"
|
|
clone_cmd=( git clone "--recurse-submodules" "--no-checkout" "--origin=origin" "$u" "$NAME" )
|
|
(( USE_PARTIAL )) && clone_cmd+=( "--filter=blob:none" )
|
|
(( FETCH_TAGS == 0 )) && clone_cmd+=( "--no-tags" )
|
|
if git_retry "clone $u" "${clone_cmd[@]}"; then
|
|
clone_ok=1
|
|
break
|
|
fi
|
|
done
|
|
if (( clone_ok == 0 )); then
|
|
log_error "clone failed on all URLs."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ---- Prepare partial clone config (for init/clone without --filter at fetch time) ----
|
|
if (( USE_PARTIAL )); then
|
|
# Best effort: ensure repo knows it's a partial clone (helps späteres lazy-fetch)
|
|
git -C "$NAME" config --local remote.origin.promisor true || true
|
|
git -C "$NAME" config --local remote.origin.partialclonefilter "blob:none" || true
|
|
git -C "$NAME" config --local extensions.partialClone "origin" || true
|
|
fi
|
|
|
|
# ---- Fetch target ref with minimal transfer ---------------------------------
|
|
# We'll try URLs in order; for each, attempt specific refspecs.
|
|
# Strategy:
|
|
# - If SHA-like: try shallow fetch of the object; fallback to full if enabled.
|
|
# - Else (name): try heads/<REF>, tags/<REF>, and plain <REF> shallow; fallback to full if needed.
|
|
enter_repo() { cd "$NAME"; } # for readability
|
|
enter_repo
|
|
|
|
fetch_from_url_for_ref() {
|
|
# fetch_from_url_for_ref <url> <ref> <shallow_or_full>
|
|
local url="$1" ref="$2" mode="$3" ; shift 3
|
|
local -a flags=()
|
|
if [[ "$mode" == "shallow" ]]; then
|
|
flags=( "${fetch_flags_shallow[@]}" )
|
|
else
|
|
flags=( "${fetch_flags_full[@]}" )
|
|
fi
|
|
|
|
if is_sha_like "$ref"; then
|
|
# Try fetching the specific commit object
|
|
# FETCH_HEAD will point to the fetched object on success.
|
|
git fetch "${flags[@]}" "$url" "$ref"
|
|
return $?
|
|
else
|
|
# Try specific namespaces to avoid extra refs
|
|
local try=(
|
|
"refs/heads/$ref:refs/tmp/$ref"
|
|
"refs/tags/$ref:refs/tmp/$ref"
|
|
"$ref:refs/tmp/$ref"
|
|
)
|
|
local t
|
|
for t in "${try[@]}"; do
|
|
if git fetch "${flags[@]}" "$url" "$t"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
fetch_ok=0
|
|
for u in "${URLS[@]}"; do
|
|
# 1) Try shallow
|
|
if git_retry "fetch url: $u $REF" fetch_from_url_for_ref "$u" "$REF" "shallow"; then
|
|
fetch_ok=1
|
|
break
|
|
fi
|
|
# 2) Optional fallback to full (still filtered blobs)
|
|
if (( FULL_IF_NEEDED )); then
|
|
log_warn "shallow fetch insufficient. trying without --depth from $u ..."
|
|
if git_retry "fetch url: $u $REF" fetch_from_url_for_ref "$u" "$REF" "full"; then
|
|
fetch_ok=1
|
|
break
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if (( fetch_ok == 0 )); then
|
|
log_error "failed fetching reference '$REF' on all URLs."
|
|
exit 1
|
|
fi
|
|
|
|
# Resolve fetched commit
|
|
TARGET_COMMIT="$(git rev-parse --verify --quiet FETCH_HEAD || true)"
|
|
if [[ -z "$TARGET_COMMIT" ]]; then
|
|
log_error "couldnt resolve FETCH_HEAD."
|
|
exit 1
|
|
fi
|
|
log_info "fetched: $TARGET_COMMIT"
|
|
|
|
# ---- Forced checkout --------------------------------------------------------
|
|
# We checkout detached at the exact commit to be fully deterministic.
|
|
# (No implicit branch tracking; avoids surprises if remote refs move.)
|
|
if (( CLEAN )); then
|
|
log_info "cleaning working dir (reset --hard, clean -fdx)"
|
|
git reset --hard
|
|
git clean -fdx
|
|
fi
|
|
|
|
log_info "forced checkout (detached) auf $TARGET_COMMIT"
|
|
# Ensure no stray temp ref prevents checkout
|
|
git update-ref -d "refs/tmp/$REF" >/dev/null 2>&1 || true
|
|
git checkout --force --detach "$TARGET_COMMIT"
|
|
git submodule update --init
|
|
|
|
# Small summary
|
|
log_info "working dir:"
|
|
git --no-pager log -1 --oneline
|
|
git --no-pager status --short --branch
|
|
|
|
log_info "Done."
|