#!/usr/bin/env bash # scripts/sync_to_gl.sh - copy ~/acdl contents to ~/gl/acdl and push. # # Copies the ACDL source tree into the GitLab mirror at ~/gl/acdl. # Hidden files/dirs are NOT copied EXCEPT for .github (so GitLab CI # workflows stay current) and .gitignore. The terraform/ tree is # omitted entirely, and .gitignore patterns are honored. The # destination's existing .git directory is preserved untouched. # # After syncing, commits any changes on the current branch with a # timestamped message and pushes it to its upstream (origin/main). # # Run manually: # bash scripts/sync_to_gl.sh # sync + commit + push # bash scripts/sync_to_gl.sh -v # verbose (list copied files) # bash scripts/sync_to_gl.sh --no-push # sync + commit only, no push # bash scripts/sync_to_gl.sh --dry-run # show what would happen # SRC=~/acdl DST=~/gl/acdl bash scripts/sync_to_gl.sh set -euo pipefail SRC="${SRC:-$HOME/acdl}" DST="${DST:-$HOME/gl/acdl}" VERBOSE=0 NO_PUSH=0 DRY_RUN=0 for arg in "$@"; do case "$arg" in -v|--verbose) VERBOSE=1 ;; --no-push) NO_PUSH=1 ;; --dry-run) DRY_RUN=1 ;; -h|--help) sed -n '2,21p' "$0" exit 0 ;; *) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;; esac done fail() { echo "FAIL: $*" >&2; exit 1; } run() { if [ "$DRY_RUN" = "1" ]; then echo " [dry-run] $*" else "$@" fi } [ -d "$SRC" ] || fail "source not found: $SRC" [ -d "$DST" ] || fail "destination not found: $DST (create it first)" [ -d "$DST/.git" ] || fail "destination has no .git: $DST/.git (restore it first)" echo "=== sync_to_gl ===" echo "source: $SRC" echo "destination: $DST" [ "$NO_PUSH" = "1" ] && echo "mode: sync + commit (no push)" [ "$DRY_RUN" = "1" ] && echo "mode: dry-run (no changes made)" echo "" # Sanity: refuse if DST is not inside ~/gl or is the same as SRC. case "$DST" in "$HOME"/gl/*) : ;; *) fail "destination must live under ~/gl (got $DST)" ;; esac [ "$SRC" != "$DST" ] || fail "source and destination are identical" # --- sync (rsync) ----------------------------------------------------------- # Build rsync exclude list: every hidden entry in SRC except .github # and .gitignore. EXCLUDES=() for hidden in "$SRC"/.*; do name="$(basename "$hidden")" case "$name" in .|...) continue ;; .github|.gitignore) continue ;; # keep esac EXCLUDES+=("--exclude=/$name") done # Never touch the destination's .git. "protect" makes rsync skip it # entirely (neither transfer nor delete) even under --delete; this is # stronger than --exclude, which --delete-excluded would wipe out. # Drop it from the transfer set too. EXCLUDES+=("--exclude=/.git") # Omit the terraform/ tree entirely. EXCLUDES+=("--exclude=/terraform") # rsync filters: protect .git, then honor per-directory .gitignore # via dir-merge (:-) semantics so patterns anchor like git does. FILTERS=( "--filter=P .git" "--filter=:- .gitignore" ) # Use --delete (prune extras in the synced tree) but NOT --delete-excluded: # that would wipe destination paths covered by our --exclude rules, which # is exactly what must NOT happen for .git. RSYNC_ARGS=(-a --delete) [ "$VERBOSE" = "1" ] && RSYNC_ARGS+=(-v) echo "rsync excludes: ${EXCLUDES[*]}" echo "rsync filters: ${FILTERS[*]}" echo "" if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] rsync would run:" printf ' %q ' rsync "${RSYNC_ARGS[@]}" "${FILTERS[@]}" "${EXCLUDES[@]}" "$SRC/" "$DST/"; echo else rsync "${RSYNC_ARGS[@]}" "${FILTERS[@]}" "${EXCLUDES[@]}" "$SRC/" "$DST/" echo "rsync: OK" fi echo "" # --- git commit + push ------------------------------------------------------ cd "$DST" # Refuse to run inside a merge/rebase/conflict state. git rev-parse --is-inside-work-tree >/dev/null git_dir_state() { local f for f in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG; do [ -e ".git/$f" ] && return 1 done [ -d ".git/rebase-merge" -o -d ".git/rebase-apply" ] && return 1 return 0 } git_dir_state || fail "destination .git is mid-operation (merge/rebase/etc); resolve it then re-run" branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" [ -n "$branch" ] || fail "HEAD is detached; checkout a branch first (got $(git rev-parse --short HEAD))" # Stage everything in the working tree (including deletions). run git add -A # Commit only if there is something staged. if git diff --cached --quiet; then echo "git: no changes to commit on branch '$branch'" else ts="$(date -u +%Y-%m-%d\ %H:%M\ UTC)" msg="chore: sync from source mirror $ts" echo "git: committing on branch '$branch'" [ "$VERBOSE" = "1" ] && git diff --cached --stat run git commit -m "$msg" fi # Push (current branch to its upstream) unless suppressed. if [ "$NO_PUSH" = "1" ]; then echo "git: --no-push set, skipping push" PUSHED=0 else upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)" if [ -z "$upstream" ]; then fail "no upstream configured for branch '$branch'; set one with: git -C $DST branch --set-upstream-to=origin/$branch $branch" fi if [ "$DRY_RUN" = "1" ]; then echo " [dry-run] git push to $upstream" else echo "git: pushing '$branch' to $upstream" git push echo "git: push OK" fi fi echo "" echo "=== sync_to_gl OK ===" echo "copied $SRC -> $DST" [ "$DRY_RUN" = "1" ] && echo "(dry-run: nothing actually written or pushed)" [ "$NO_PUSH" = "1" ] && echo "(no-push: changes committed but not pushed)" exit 0