tenseleyflow/ndotfiles / 2e64e0d

Browse files

add this script

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
2e64e0d089149501a109dcd6d48183c849be1286
Parents
b993ac7
Tree
4d778ff

1 changed file

StatusFile+-
A Script/sync-repos 231 0
Script/sync-reposadded
@@ -0,0 +1,231 @@
1
+#!/usr/bin/env bash
2
+#
3
+# sync-repos - Sync all GitHub organizations and their repos locally
4
+#
5
+# Clones new repos and pulls updates (ff-only) for existing ones.
6
+# Config: ~/.config/sync-repos/orgs.conf
7
+#
8
+
9
+set -uo pipefail
10
+
11
+CONFIG_FILE="${SYNC_REPOS_CONFIG:-$HOME/.config/sync-repos/orgs.conf}"
12
+GITHUB_ORGS_DIR="${GITHUB_ORGS_DIR:-$HOME/GithubOrgs}"
13
+CLONE_TIMEOUT="${CLONE_TIMEOUT:-300}"  # seconds (5 min for large repos)
14
+
15
+# Colors
16
+RED='\033[0;31m'
17
+GREEN='\033[0;32m'
18
+YELLOW='\033[0;33m'
19
+BLUE='\033[0;34m'
20
+BOLD='\033[1m'
21
+NC='\033[0m'
22
+
23
+# Counters
24
+total_cloned=0
25
+total_updated=0
26
+total_skipped=0
27
+total_failed=0
28
+declare -a failed_repos=()
29
+
30
+log_info()    { echo -e "${BLUE}→${NC} $*"; }
31
+log_success() { echo -e "${GREEN}✓${NC} $*"; }
32
+log_warn()    { echo -e "${YELLOW}⚠${NC} $*"; }
33
+log_error()   { echo -e "${RED}✗${NC} $*"; }
34
+log_header()  { echo -e "\n${BOLD}${BLUE}══════════════════════════════════════${NC}"; echo -e "${BOLD}  $*${NC}"; echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}"; }
35
+
36
+check_requirements() {
37
+    if ! command -v gh &> /dev/null; then
38
+        log_error "gh CLI is not installed. Install from: https://cli.github.com/"
39
+        exit 1
40
+    fi
41
+
42
+    if ! gh auth status &> /dev/null; then
43
+        log_error "Not authenticated with GitHub. Run: gh auth login"
44
+        exit 1
45
+    fi
46
+
47
+    if [[ ! -f "$CONFIG_FILE" ]]; then
48
+        log_error "Config file not found: $CONFIG_FILE"
49
+        exit 1
50
+    fi
51
+}
52
+
53
+sync_repo() {
54
+    local github_org="$1"
55
+    local repo="$2"
56
+    local org_path="$3"
57
+    local repo_path="${org_path}/${repo}"
58
+
59
+    if [[ -d "$repo_path" ]]; then
60
+        # Repo exists - try to pull
61
+        if [[ -d "$repo_path/.git" ]]; then
62
+            cd "$repo_path"
63
+
64
+            # Check for uncommitted changes
65
+            if ! git diff-index --quiet HEAD -- 2>/dev/null; then
66
+                log_warn "$repo (uncommitted changes, skipping)"
67
+                ((total_skipped++)) || true
68
+                return 0
69
+            fi
70
+
71
+            # Try fast-forward pull
72
+            if output=$(timeout 30 git pull --ff-only 2>&1); then
73
+                if [[ "$output" == *"Already up to date"* ]]; then
74
+                    log_success "$repo (up to date)"
75
+                else
76
+                    log_success "$repo (updated)"
77
+                    ((total_updated++)) || true
78
+                fi
79
+            else
80
+                log_warn "$repo (can't fast-forward, skipping)"
81
+                ((total_skipped++)) || true
82
+            fi
83
+        else
84
+            log_warn "$repo (exists but not a git repo)"
85
+            ((total_skipped++)) || true
86
+        fi
87
+    else
88
+        # Clone new repo with timeout, showing progress
89
+        log_info "Cloning $repo..."
90
+        if timeout "$CLONE_TIMEOUT" gh repo clone "${github_org}/${repo}" "$repo_path" -- --progress 2>&1; then
91
+            if [[ -d "$repo_path/.git" ]]; then
92
+                log_success "$repo (cloned)"
93
+                ((total_cloned++)) || true
94
+            else
95
+                log_error "$repo (clone failed - no permission?)"
96
+                failed_repos+=("${github_org}/${repo}")
97
+                ((total_failed++)) || true
98
+            fi
99
+        else
100
+            exit_code=$?
101
+            # Check if it actually succeeded despite error
102
+            if [[ -d "$repo_path/.git" ]]; then
103
+                log_success "$repo (cloned)"
104
+                ((total_cloned++)) || true
105
+            elif [[ $exit_code -eq 124 ]]; then
106
+                log_error "$repo (timeout after ${CLONE_TIMEOUT}s)"
107
+                failed_repos+=("${github_org}/${repo}")
108
+                ((total_failed++)) || true
109
+            else
110
+                log_error "$repo (clone failed)"
111
+                failed_repos+=("${github_org}/${repo}")
112
+                ((total_failed++)) || true
113
+            fi
114
+        fi
115
+    fi
116
+}
117
+
118
+sync_org() {
119
+    local local_dir="$1"
120
+    local org_spec="$2"
121
+    local org_path="${GITHUB_ORGS_DIR}/${local_dir}"
122
+
123
+    # Parse org:repo1,repo2 format
124
+    local github_org specific_repos
125
+    if [[ "$org_spec" == *:* ]]; then
126
+        github_org="${org_spec%%:*}"
127
+        specific_repos="${org_spec#*:}"
128
+    else
129
+        github_org="$org_spec"
130
+        specific_repos=""
131
+    fi
132
+
133
+    log_header "$local_dir ← $github_org"
134
+
135
+    # Create org directory if needed
136
+    if [[ ! -d "$org_path" ]]; then
137
+        log_info "Creating directory: $org_path"
138
+        mkdir -p "$org_path"
139
+    fi
140
+
141
+    local repos
142
+    if [[ -n "$specific_repos" ]]; then
143
+        # Use specific repos from config
144
+        repos=$(echo "$specific_repos" | tr ',' '\n')
145
+        local repo_count
146
+        repo_count=$(echo "$repos" | wc -l)
147
+        log_info "Syncing $repo_count specific repositories"
148
+    else
149
+        # Fetch all repos from GitHub
150
+        log_info "Fetching repository list..."
151
+        if ! repos=$(gh repo list "$github_org" --limit 1000 --json name --jq '.[].name' 2>&1); then
152
+            log_error "Failed to list repos for $github_org: $repos"
153
+            return 1
154
+        fi
155
+
156
+        if [[ -z "$repos" ]]; then
157
+            log_warn "No repositories found for $github_org"
158
+            return 0
159
+        fi
160
+
161
+        local repo_count
162
+        repo_count=$(echo "$repos" | wc -l)
163
+        log_info "Found $repo_count repositories"
164
+    fi
165
+    echo ""
166
+
167
+    local cloned=0 updated=0 skipped=0 failed=0
168
+    local before_cloned=$total_cloned
169
+    local before_updated=$total_updated
170
+    local before_skipped=$total_skipped
171
+    local before_failed=$total_failed
172
+
173
+    while IFS= read -r repo; do
174
+        [[ -z "$repo" ]] && continue
175
+        sync_repo "$github_org" "$repo" "$org_path"
176
+    done <<< "$repos"
177
+
178
+    cloned=$((total_cloned - before_cloned))
179
+    updated=$((total_updated - before_updated))
180
+    skipped=$((total_skipped - before_skipped))
181
+    failed=$((total_failed - before_failed))
182
+
183
+    echo ""
184
+    echo "  Cloned: $cloned | Updated: $updated | Skipped: $skipped | Failed: $failed"
185
+}
186
+
187
+main() {
188
+    echo -e "${BOLD}${BLUE}"
189
+    echo "  ╔═══════════════════════════════════╗"
190
+    echo "  ║       GitHub Repos Sync           ║"
191
+    echo "  ╚═══════════════════════════════════╝"
192
+    echo -e "${NC}"
193
+
194
+    check_requirements
195
+
196
+    log_info "Config: $CONFIG_FILE"
197
+    log_info "Target: $GITHUB_ORGS_DIR"
198
+
199
+    # Create base directory if needed
200
+    mkdir -p "$GITHUB_ORGS_DIR"
201
+
202
+    # Read config and sync each org
203
+    while IFS='=' read -r local_dir org_spec || [[ -n "$local_dir" ]]; do
204
+        # Skip comments and empty lines
205
+        [[ -z "$local_dir" || "$local_dir" =~ ^[[:space:]]*# ]] && continue
206
+
207
+        # Trim whitespace
208
+        local_dir="${local_dir// /}"
209
+        org_spec="${org_spec// /}"
210
+
211
+        sync_org "$local_dir" "$org_spec"
212
+    done < "$CONFIG_FILE"
213
+
214
+    # Summary
215
+    log_header "Sync Complete"
216
+    echo ""
217
+    echo -e "  ${GREEN}Cloned:${NC}  $total_cloned"
218
+    echo -e "  ${BLUE}Updated:${NC} $total_updated"
219
+    echo -e "  ${YELLOW}Skipped:${NC} $total_skipped"
220
+    echo -e "  ${RED}Failed:${NC}  $total_failed"
221
+    echo ""
222
+
223
+    if [[ ${#failed_repos[@]} -gt 0 ]]; then
224
+        log_error "Failed repositories:"
225
+        for repo in "${failed_repos[@]}"; do
226
+            echo "    - $repo"
227
+        done
228
+    fi
229
+}
230
+
231
+main "$@"