| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- #!/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 subject="${1:-}"
- local body="${2:-}"
- local allow_empty="${3:-0}"
- local display="${4:-$subject}"
- [[ -z "$subject" ]] && subject="SVN snapshot"
- git add -A
- if (( allow_empty )); then
- if [[ -n "$body" ]]; then
- GIT_QUIET=1 git commit -q --allow-empty -m "$subject" -m "$body"
- else
- GIT_QUIET=1 git commit -q --allow-empty -m "$subject"
- fi
- else
- if [[ -n "$body" ]]; then
- GIT_QUIET=1 git commit -q -m "$subject" -m "$body"
- else
- GIT_QUIET=1 git commit -q -m "$subject"
- fi
- fi
- }
- # 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.
- SRC_URL="$(svn info --show-item url 2>/dev/null || svn info | awk -F': ' '/^URL:/ {print $2; exit}')"
- if [[ -z "$SRC_URL" ]]; then
- echo "Unable to determine SVN URL" >&2
- exit 1
- fi
- REPO_ROOT="$(svn info --show-item repos-root-url 2>/dev/null || svn info | awk -F': ' '/^Repository Root:/ {print $2; exit}')"
- mapfile -t REV_LIST < <(svn log -q -r HEAD:1 --limit "$history_count" "$SRC_URL" | awk '/^r[0-9]+/ {sub(/^r/,"",$1); print $1}' | tac)
- if (( ${#REV_LIST[@]} == 0 )); then
- echo "No SVN revisions found for path $SRC_URL" >&2
- exit 1
- fi
- start_rev="${REV_LIST[0]}"
- head_rev="${REV_LIST[-1]}"
- echo "Including last ${#REV_LIST[@]} revisions affecting this path (r${start_rev}..r${head_rev})..."
- WC_DIR="$(mktemp -d "${MIRROR_ROOT}/.svn_git_sync_wc.XXXXXX")"
- mkdir -p "$DEST"
- sync_wc_to_dest() {
- rsync -a --delete --exclude '.svn' --exclude '.git' "$WC_DIR"/ "$DEST"/
- if [[ ! -e "$DEST/.gitignore" ]]; then
- printf "*.pyc\n__pycache__/\n" > "$DEST/.gitignore"
- fi
- }
- checkout_with_progress() {
- local rev="$1"
- echo "[$(elapsed)s] Checking out r${rev}..."
- rm -rf -- "$WC_DIR"
- mkdir -p "$WC_DIR"
- svn checkout -q -r "$rev" "$SRC_URL" "$WC_DIR" &
- local co_pid=$!
- local last_report=-1
- while kill -0 "$co_pid" 2>/dev/null; do
- local current=0
- if [[ -d "$WC_DIR" ]]; then
- current=$(find "$WC_DIR" -path "$WC_DIR/.svn" -prune -o -print | wc -l)
- fi
- if [[ "$current" -ne "$last_report" ]]; then
- echo "[$(elapsed)s] Checked out $current items..."
- last_report="$current"
- fi
- sleep 1
- done
- wait "$co_pid"
- local final_count=0
- if [[ -d "$WC_DIR" ]]; then
- final_count=$(find "$WC_DIR" -path "$WC_DIR/.svn" -prune -o -print | wc -l)
- fi
- echo "[$(elapsed)s] Checked out $final_count items (done)"
- }
- checkout_with_progress "$start_rev"
- sync_wc_to_dest
- cd "$DEST"
- init_git_repo
- build_commit_msg() {
- local rev="$1"
- local svn_log author msg_body commit_date
- svn_log="$(svn log -r "$rev" -l 1 "$SRC_URL")"
- author="$(printf "%s\n" "$svn_log" | awk 'NR==2{split($0,a,"|"); gsub(/^ +| +$/,"",a[2]); print a[2]}')"
- commit_date="$(printf "%s\n" "$svn_log" | awk 'NR==2{split($0,a,"|"); gsub(/^ +| +$/,"",a[3]); split(a[3],b," "); print b[1] " " b[2]}')"
- msg_body="$(printf "%s\n" "$svn_log" | sed '1,2d;/^---/,$d')"
- if { [[ -z "$author" ]] || [[ -z "$msg_body" ]]; } && [[ -n "$REPO_ROOT" ]]; then
- svn_log="$(svn log -r "$rev" -l 1 "$REPO_ROOT" 2>/dev/null || true)"
- author="$(printf "%s\n" "$svn_log" | awk 'NR==2{split($0,a,"|"); gsub(/^ +| +$/,"",a[2]); print a[2]}')"
- commit_date="$(printf "%s\n" "$svn_log" | awk 'NR==2{split($0,a,"|"); gsub(/^ +| +$/,"",a[3]); split(a[3],b," "); print b[1] " " b[2]}')"
- msg_body="$(printf "%s\n" "$svn_log" | sed '1,2d;/^---/,$d')"
- fi
- msg_body="$(printf "%s" "$msg_body" | perl -0777 -pe 's/\A\s+//; s/\s+\z//; s/\n{2,}/\n\n/g')"
- COMMIT_BODY="$msg_body"
- COMMIT_BODY_FIRST="$(printf "%s" "$msg_body" | sed -n '1p')"
- COMMIT_SUBJECT="r${rev}"
- [[ -n "$author" ]] && COMMIT_SUBJECT+=" by $author"
- COMMIT_DATE="$commit_date"
- COMMIT_PROGRESS=""
- [[ -n "$COMMIT_DATE" ]] && COMMIT_PROGRESS+="[$COMMIT_DATE] "
- COMMIT_PROGRESS+="$COMMIT_SUBJECT"
- [[ -n "$COMMIT_BODY_FIRST" ]] && COMMIT_PROGRESS+=" — $COMMIT_BODY_FIRST"
- }
- build_commit_msg "$start_rev"
- echo "[$(elapsed)s] Copy: $COMMIT_PROGRESS"
- stage_and_commit "$COMMIT_SUBJECT" "$COMMIT_BODY" 0 "$COMMIT_PROGRESS"
- if (( ${#REV_LIST[@]} > 1 )); then
- for rev in "${REV_LIST[@]:1}"; do
- svn update -q -r "$rev" "$WC_DIR"
- sync_wc_to_dest
- build_commit_msg "$rev"
- echo "[$(elapsed)s] Copy: $COMMIT_PROGRESS"
- stage_and_commit "$COMMIT_SUBJECT" "$COMMIT_BODY" 1 "$COMMIT_PROGRESS"
- done
- fi
- rm -rf -- "$WC_DIR"
- link_git_to_source
|