|
|
@@ -1,12 +1,12 @@
|
|
|
#!/usr/bin/env bash
|
|
|
# svn_git_sync — sync current SVN working copy into a Git mirror
|
|
|
# Usage:
|
|
|
-# svn_git_sync init [mirror_name]
|
|
|
+# svn_git_sync init [--history=N] [mirror_name]
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
show_usage() {
|
|
|
- echo "Usage: $0 init [mirror_name]" >&2
|
|
|
+ echo "Usage: $0 init [--history=N] [mirror_name]" >&2
|
|
|
exit 1
|
|
|
}
|
|
|
|
|
|
@@ -14,8 +14,39 @@ 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
|
|
|
@@ -30,7 +61,7 @@ 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="${1:-$(pwd -P)}"
|
|
|
+raw_name="${mirror_arg:-$(pwd -P)}"
|
|
|
BASE_NAME="$(printf "%s" "$raw_name" | sed 's#^[\\/]*##; s#[\\/]#_#g')"
|
|
|
DEST="${MIRROR_ROOT}/${BASE_NAME}"
|
|
|
|
|
|
@@ -38,12 +69,6 @@ echo "Exporting SVN working copy to $DEST (versioned files only)..."
|
|
|
rm -rf -- "$DEST"
|
|
|
mkdir -p "$MIRROR_ROOT"
|
|
|
|
|
|
-# Optionally compute total items upfront for a rough percentage
|
|
|
-total_items=""
|
|
|
-if total=$(svn list -R . 2>/dev/null | wc -l || true); then
|
|
|
- total_items="$total"
|
|
|
-fi
|
|
|
-
|
|
|
start_ts="$(date +%s.%N)"
|
|
|
elapsed() {
|
|
|
local now
|
|
|
@@ -51,68 +76,134 @@ elapsed() {
|
|
|
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}'
|
|
|
}
|
|
|
|
|
|
-# Run export in background so we can emit our own progress based on files created.
|
|
|
-svn export --quiet . "$DEST" &
|
|
|
-export_pid=$!
|
|
|
-
|
|
|
-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
|
|
|
+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
|
|
|
|
|
|
- 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)}')
|
|
|
+ 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
|
|
|
- printf "[%ss] Exported %d items%s\n" "$elapsed_val" "$current" "$pct"
|
|
|
- last_reported="$current"
|
|
|
+
|
|
|
+ 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
|
|
|
+}
|
|
|
|
|
|
- sleep 1
|
|
|
-done
|
|
|
+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"
|
|
|
|
|
|
-wait "$export_pid"
|
|
|
+ echo "[$(elapsed) s] Syncing working tree to repo content..."
|
|
|
+ GIT_QUIET=1 git checkout -q -- .
|
|
|
+}
|
|
|
|
|
|
-final_count=$(find "$DEST" -mindepth 1 -print | wc -l 2>/dev/null || echo 0)
|
|
|
-printf "[%ss] Exported %d items (done)\n" "$(elapsed)" "$final_count"
|
|
|
+stage_and_commit() {
|
|
|
+ local msg="$1"
|
|
|
+ echo "[$(elapsed) s] Staging files..."
|
|
|
+ git add -A
|
|
|
|
|
|
-cd "$DEST"
|
|
|
+ echo "[$(elapsed) s] Creating commit: $msg"
|
|
|
+ GIT_QUIET=1 git commit -q -m "$msg" || true
|
|
|
+}
|
|
|
|
|
|
-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
|
|
|
+# 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
|
|
|
|
|
|
-echo "[$(elapsed) s] Staging files..."
|
|
|
-git add -A
|
|
|
-
|
|
|
-echo "[$(elapsed) s] Creating initial commit..."
|
|
|
-GIT_QUIET=1 git commit -q -m "Import SVN snapshot" || true
|
|
|
+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
|
|
|
|
|
|
-# Reuse the repo for the source working tree by linking .git back to the new repo.
|
|
|
-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
|
|
|
+# 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
|
|
|
-printf "gitdir: %s/.git\n" "$DEST_ABS" > .git
|
|
|
-echo "[$(elapsed)s] Linked .git to $DEST_ABS/.git"
|
|
|
+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" "$STAGE_DIR"
|
|
|
+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 --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
|
|
|
|
|
|
-# Normalize working tree to match repo filters (e.g., CRLF handling).
|
|
|
-echo "[$(elapsed) s] Syncing working tree to repo content..."
|
|
|
-GIT_QUIET=1 git checkout -q -- .
|
|
|
+link_git_to_source
|