diff --git a/sync_repos.sh b/sync_repos.sh new file mode 100755 index 0000000..2beba17 --- /dev/null +++ b/sync_repos.sh @@ -0,0 +1,149 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(pwd)" +TIMESTAMP="$(date +%Y%m%d_%H%M%S)" +REPORT_TXT="$ROOT_DIR/sync_report_$TIMESTAMP.txt" +REPORT_JSON="$ROOT_DIR/sync_report_$TIMESTAMP.json" + +DRY_RUN=false +TIMEOUT=60 + +# Parse args +for arg in "$@"; do + case $arg in + --dry-run) DRY_RUN=true ;; + --timeout=*) TIMEOUT="${arg#*=}" ;; + esac +done + +log() { + echo -e "$1" + echo -e "$1" >> "$REPORT_TXT" +} + +json_escape() { + echo "$1" | sed 's/"/\\"/g' +} + +run_cmd() { + if $DRY_RUN; then + log "🟔 [DRY-RUN] $*" + return 0 + fi + + timeout "$TIMEOUT" bash -c "$*" >/dev/null 2>&1 +} + +echo "Repo Sync Report - $(date)" > "$REPORT_TXT" +echo "[" > "$REPORT_JSON" + +FIRST=true +SUCCESS=0 +FAIL=0 +SKIP=0 + +for dir in */; do + REPO_PATH="$ROOT_DIR/$dir" + REPO_NAME=$(basename "$dir") + + STATUS="unknown" + MESSAGE="" + + log "\nšŸ”¹ Processing: $REPO_NAME" + + cd "$REPO_PATH" || { STATUS="fail"; MESSAGE="cannot cd"; ((FAIL++)); continue; } + + if [ ! -d ".git" ]; then + STATUS="skip"; MESSAGE="not a git repo"; ((SKIP++)) + log "āš ļø Not a git repo" + continue + fi + + if ! git remote get-url origin &>/dev/null; then + STATUS="skip"; MESSAGE="no origin"; ((SKIP++)) + log "āš ļø No origin" + continue + fi + + # Detect default branch + DEFAULT_BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}') + DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} + + if ! git show-ref --verify --quiet "refs/heads/$DEFAULT_BRANCH"; then + STATUS="skip"; MESSAGE="no local $DEFAULT_BRANCH"; ((SKIP++)) + log "āš ļø Missing local branch $DEFAULT_BRANCH" + continue + fi + + # Check clean working tree + if ! git diff --quiet || ! git diff --cached --quiet; then + STATUS="skip"; MESSAGE="uncommitted changes"; ((SKIP++)) + log "āš ļø Uncommitted changes" + continue + fi + + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + + if [ "$CURRENT_BRANCH" = "HEAD" ]; then + STATUS="skip"; MESSAGE="detached HEAD"; ((SKIP++)) + log "āš ļø Detached HEAD" + continue + fi + + # Fetch + if ! run_cmd "git fetch origin --prune"; then + STATUS="fail"; MESSAGE="fetch failed"; ((FAIL++)) + log "āŒ Fetch failed" + continue + fi + + # Checkout default branch + if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then + if ! run_cmd "git checkout $DEFAULT_BRANCH"; then + STATUS="fail"; MESSAGE="checkout failed"; ((FAIL++)) + log "āŒ Checkout failed" + continue + fi + fi + + # Rebase safely + if run_cmd "git rebase origin/$DEFAULT_BRANCH"; then + STATUS="success"; MESSAGE="rebased" + log "āœ… Rebased onto origin/$DEFAULT_BRANCH" + ((SUCCESS++)) + else + git rebase --abort &>/dev/null || true + STATUS="fail"; MESSAGE="rebase conflict" + log "āŒ Rebase conflict (aborted)" + ((FAIL++)) + fi + + # Return to original branch + if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then + run_cmd "git checkout $CURRENT_BRANCH" + fi + + # JSON logging + ESC_MSG=$(json_escape "$MESSAGE") + if [ "$FIRST" = true ]; then + FIRST=false + else + echo "," >> "$REPORT_JSON" + fi + + echo " {\"repo\":\"$REPO_NAME\",\"status\":\"$STATUS\",\"message\":\"$ESC_MSG\"}" >> "$REPORT_JSON" + +done + +echo "]" >> "$REPORT_JSON" + +log "\n======================================" +log "āœ… Success: $SUCCESS" +log "āŒ Failed: $FAIL" +log "āš ļø Skipped: $SKIP" +log "šŸ“„ TXT Report: $REPORT_TXT" +log "šŸ“„ JSON Report: $REPORT_JSON" + +echo -e "\nDone."