#!/usr/bin/env bash # svn_git_sync — sync current SVN working copy into a Git mirror # Usage: # svn_git_sync init [--history=N] [mirror_name] set -euo pipefail show_usage() { echo "Usage: $0 init [--history=N] [mirror_name]" >&2 exit 1 } subcommand="${1:-}" [[ -n "$subcommand" ]] || show_usage shift history_count=0 mirror_arg="" parse_opts() { while [[ $# -gt 0 ]]; do case "$1" in --history=*) history_count="${1#*=}" shift ;; --history) history_count="${2:-}" shift 2 ;; -h|--help) show_usage ;; *) mirror_arg="$1" shift break ;; esac done if [[ -n "$mirror_arg" && $# -gt 0 ]]; then echo "Unexpected extra arguments: $*" >&2 show_usage fi } case "$subcommand" in init) parse_opts "$@" ;; -h|--help|help) show_usage ;; *) echo "Unknown command: $subcommand" >&2 show_usage ;; esac SRC_DIR="$(pwd)" MIRROR_ROOT="${GIT_SVN_MIRROR_ROOT:-$HOME/git_svn_mirror}" # Derive a collision-resistant mirror name from the full path (unless overridden). raw_name="${mirror_arg:-$(pwd -P)}" BASE_NAME="$(printf "%s" "$raw_name" | sed 's#^[\\/]*##; s#[\\/]#_#g')" DEST="${MIRROR_ROOT}/${BASE_NAME}" echo "Exporting SVN working copy to $DEST (versioned files only)..." rm -rf -- "$DEST" mkdir -p "$MIRROR_ROOT" start_ts="$(date +%s.%N)" elapsed() { local now now="${1:-$(date +%s.%N)}" awk -v s="$start_ts" -v n="$now" 'BEGIN{split(s,sa,".");split(n,na,".");printf "%.1f", (na[1]-sa[1]) + (na[2]-sa[2])/1e9}' } simple_export() { # Optionally compute total items upfront for a rough percentage local total_items="" if total=$(svn list -R . 2>/dev/null | wc -l || true); then total_items="$total" fi svn export --quiet . "$DEST" & export_pid=$! local last_reported=-1 while kill -0 "$export_pid" 2>/dev/null; do if [[ -d "$DEST" ]]; then current=$(find "$DEST" -mindepth 1 -print | wc -l) else current=0 fi if [[ "$current" -ne "$last_reported" ]]; then elapsed_val="$(elapsed)" pct="" if [[ -n "$total_items" && "$total_items" -gt 0 ]]; then pct=$(awk -v c="$current" -v t="$total_items" 'BEGIN{printf(" (%.1f%%)", (c*100)/t)}') fi printf "[%ss] Exported %d items%s\n" "$elapsed_val" "$current" "$pct" last_reported="$current" fi sleep 1 done wait "$export_pid" final_count=$(find "$DEST" -mindepth 1 -print | wc -l 2>/dev/null || echo 0) printf "[%ss] Exported %d items (done)\n" "$(elapsed)" "$final_count" } init_git_repo() { echo "[$(elapsed) s] Initializing git repository..." GIT_QUIET=1 git init >/dev/null git config init.defaultBranch trunk || true git config core.fileMode false || true git config core.autocrlf true || true git config core.safecrlf false || true # Seed ignores for common noise (pyc/cache) if [[ ! -e .gitignore ]]; then echo "[$(elapsed) s] Adding default ignores (.pyc/__pycache__)..." printf "*.pyc\n__pycache__/\n" > .gitignore fi } link_git_to_source() { echo "[$(elapsed) s] Linking git metadata to source working tree..." DEST_ABS="$(pwd -P)" cd "$SRC_DIR" if [[ -e .git || -L .git ]]; then echo "[$(elapsed)s] Replacing existing .git in $SRC_DIR..." rm -rf -- .git fi printf "gitdir: %s/.git\n" "$DEST_ABS" > .git echo "[$(elapsed)s] Linked .git to $DEST_ABS/.git" echo "[$(elapsed) s] Syncing working tree to repo content..." GIT_QUIET=1 git checkout -q -- . } stage_and_commit() { local msg="$1" echo "[$(elapsed) s] Staging files..." git add -A echo "[$(elapsed) s] Creating commit: $msg" GIT_QUIET=1 git commit -q -m "$msg" || true } # Validate history_count is numeric (default 0) if [[ -z "${history_count:-}" ]]; then history_count=0 elif ! [[ "$history_count" =~ ^[0-9]+$ ]]; then echo "--history must be a non-negative integer" >&2 exit 1 fi if (( history_count == 0 )); then simple_export cd "$DEST" init_git_repo stage_and_commit "Import SVN snapshot" link_git_to_source exit 0 fi # History mode: pull last N revisions into Git. head_rev="$(svn info --show-item revision 2>/dev/null || svn info | awk -F': ' '/^Revision:/ {print $2; exit}')" if [[ -z "$head_rev" ]]; then echo "Unable to determine HEAD revision" >&2 exit 1 fi start_rev=$(( head_rev - history_count + 1 )) if (( start_rev < 1 )); then start_rev=1; fi echo "Including last $history_count revisions (r${start_rev}..r${head_rev})..." STAGE_DIR="$DEST/.stage" mkdir -p "$DEST" cd "$DEST" init_git_repo for rev in $(seq "$start_rev" "$head_rev"); do echo "[$(elapsed) s] Exporting r${rev}..." rm -rf -- "$STAGE_DIR" mkdir -p "$STAGE_DIR" svn export --force --quiet -r "$rev" "$SRC_DIR" "$STAGE_DIR" # Sync exported snapshot into repo working tree, preserving .git and .gitignore rsync -a --delete --exclude '.git' --exclude '.gitignore' "$STAGE_DIR"/ "$DEST"/ # Ensure ignores persist if [[ ! -e .gitignore ]]; then printf "*.pyc\n__pycache__/\n" > .gitignore fi if [[ "$rev" -eq "$start_rev" ]]; then stage_and_commit "Import SVN r${rev}" else stage_and_commit "Update to SVN r${rev}" fi done link_git_to_source