#!/usr/bin/env bash
# git-mirror.sh — incremental file-level mirror of a bare git repo to a remote
# host over plain ssh+tar, for remotes that have no git and no usable rsync.
# Git object/pack files are immutable once written, so each run sends only
# files the remote lacks plus the small mutable metadata (refs, HEAD,
# packed-refs), then prunes files gc deleted locally. New files go over
# before anything is pruned, so the remote is always a valid clone-able repo.
set -uo pipefail
export LC_ALL=C  # both file lists must collate identically for comm

# ---- config ----
SRC=/path/to/local/repo.git            # local bare repo to mirror
REMOTE_HOST=user@your-server           # ssh destination
DEST_PARENT=/path/on/remote/backups    # parent dir on the remote
DEST="$DEST_PARENT/repo.git"
# ----------------

SSH="ssh -o BatchMode=yes -o ConnectTimeout=25 -o ServerAliveInterval=10"
TS=$(date -Iseconds)

log() { printf '%s git-mirror: %s\n' "$TS" "$*" >&2; }

cd "$SRC" || { log "source repo unreachable"; exit 1; }

# git requires refs/ and objects/ to exist even when empty, and find below
# skips empty dirs — recreate the bare skeleton every run (idempotent)
$SSH "$REMOTE_HOST" "mkdir -p '$DEST'/{refs/heads,refs/tags,objects/pack,objects/info,info,branches,hooks}" \
  || { log "remote unreachable, skipped"; exit 1; }

LOCAL=$(find . -type f | LC_ALL=C sort)
REMOTE_LIST=$($SSH "$REMOTE_HOST" "cd '$DEST' 2>/dev/null && find . -type f" | LC_ALL=C sort || true)

# send: files the remote lacks + mutable metadata every run
NEW=$(comm -23 <(printf '%s\n' "$LOCAL") <(printf '%s\n' "$REMOTE_LIST"))
MUT=$(find . -type f \( -path './refs/*' -o -name 'packed-refs' -o -name 'HEAD' \
      -o -path './info/*' -o -path './objects/info/*' \) 2>/dev/null)
SEND=$(printf '%s\n%s\n' "$NEW" "$MUT" | sed '/^$/d' | LC_ALL=C sort -u)
NSEND=$(printf '%s' "$SEND" | grep -c . || true)

if [ "${NSEND:-0}" -gt 0 ]; then
  printf '%s\n' "$SEND" | tar -cf - --files-from=- 2>/dev/null \
    | $SSH "$REMOTE_HOST" "tar -xf - -C '$DEST'" \
    || { log "tar transfer failed"; exit 1; }
fi

# prune what local gc removed, AFTER sending, so the remote never drops
# below a valid repo
GONE=$(comm -13 <(printf '%s\n' "$LOCAL") <(printf '%s\n' "$REMOTE_LIST"))
NPRUNE=$(printf '%s' "$GONE" | grep -c . || true)
if [ "${NPRUNE:-0}" -gt 0 ]; then
  printf '%s\n' "$GONE" | sed 's#^\./##;/^$/d' \
    | $SSH "$REMOTE_HOST" "cd '$DEST' && tr '\n' '\0' | xargs -0 -r rm -f" || true
fi

# freshness marker outside the mirror dir so pruning never touches it
SHA=$(git rev-parse HEAD 2>/dev/null || echo unknown)
$SSH "$REMOTE_HOST" "printf '%s %s\n' '$TS' '$SHA' > '$DEST_PARENT/.last-mirror'" || true

log "mirror updated: sent=${NSEND:-0} pruned=${NPRUNE:-0} head=$SHA"
# src: ezra, contact: ezkru69@mail.com
