gabriel / muse public
muse-git-backup.sh bash
280 lines 9.5 KB
Raw
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 days ago
1 #!/usr/bin/env bash
2 # muse-git-backup.sh — git-bridge backup mirror for muse/musehub -> GitHub
3 # See MuseHub staging issue #127 for full context.
4 #
5 # Usage:
6 # ./muse-git-backup.sh preflight
7 # ./muse-git-backup.sh replay-muse [--skip N] [--limit N]
8 # ./muse-git-backup.sh replay-musehub [--skip N] [--limit N]
9 # ./muse-git-backup.sh replay-all [--skip N] [--limit N] # muse + musehub in parallel
10 # ./muse-git-backup.sh github-push-muse
11 # ./muse-git-backup.sh github-push-musehub
12 # ./muse-git-backup.sh github-push # both
13 # ./muse-git-backup.sh verify-muse
14 # ./muse-git-backup.sh verify-musehub
15 # ./muse-git-backup.sh verify # both
16 # ./muse-git-backup.sh incremental-muse
17 # ./muse-git-backup.sh incremental-musehub
18 #
19 # replay-* is resumable by default: it inspects how many commits are already
20 # on the target mirror branch and skips that many automatically, so running
21 # `replay-muse --limit 20` as a dry run and then `replay-muse` again to do
22 # the rest continues where the dry run left off instead of duplicating it.
23 # Pass --skip explicitly to override the auto-detected count.
24 #
25 # Each subcommand is independent and safe to re-run. This script never
26 # touches ~/ecosystem/muse or ~/ecosystem/musehub — every operation targets
27 # the copies under /Users/gabriel/dev/copies 2/ and brand-new directories/
28 # GitHub repos this script creates fresh. Run as ./muse-git-backup.sh <cmd>,
29 # never sourced into your interactive shell — that's what makes `exit`
30 # inside this script safe (it only ends the script's own process).
31
32 set -euo pipefail
33
34 MUSE_COPY="/Users/gabriel/dev/copies 2/muse 04"
35 MUSEHUB_COPY="/Users/gabriel/dev/copies 2/musehub 04"
36 MUSE_GIT_BACKUP="/Users/gabriel/dev/copies 2/muse-git-backup"
37 MUSEHUB_GIT_BACKUP="/Users/gabriel/dev/copies 2/musehub-git-backup"
38 GH_MUSE_REPO="cgcardona/muse-backup"
39 GH_MUSEHUB_REPO="cgcardona/musehub-backup"
40
41 guard() {
42 case "$1" in
43 *ecosystem*) echo "❌ REFUSED: path contains 'ecosystem': $1"; exit 1 ;;
44 esac
45 }
46
47 cmd_preflight() {
48 guard "$MUSE_COPY"
49 guard "$MUSEHUB_COPY"
50
51 test -d "$MUSE_COPY/.muse" && echo "muse copy: OK" || { echo "muse copy: MISSING .muse/ — STOP"; exit 1; }
52 test -d "$MUSEHUB_COPY/.muse" && echo "musehub copy: OK" || { echo "musehub copy: MISSING .muse/ — STOP"; exit 1; }
53
54 if [ -L "$MUSE_COPY" ]; then echo "STOP — muse copy is a symlink"; exit 1; fi
55 if [ -L "$MUSEHUB_COPY" ]; then echo "STOP — musehub copy is a symlink"; exit 1; fi
56 echo "muse copy: real directory, not a symlink"
57 echo "musehub copy: real directory, not a symlink"
58
59 echo "muse commit count:"
60 muse -C "$MUSE_COPY" rev-list --count HEAD --json
61 echo "musehub commit count:"
62 muse -C "$MUSEHUB_COPY" rev-list --count HEAD --json
63 }
64
65 _replay_full_history() {
66 local muse_copy="$1" git_backup="$2" label="$3"
67 guard "$muse_copy"
68 guard "$git_backup"
69
70 mkdir -p "$git_backup"
71 local already_exported=0
72 if [ ! -d "$git_backup/.git" ]; then
73 git -C "$git_backup" init -q
74 git -C "$git_backup" config user.email "[email protected]"
75 git -C "$git_backup" config user.name "gabriel"
76 git -C "$git_backup" commit --allow-empty -qm "init ($label git-bridge backup mirror)"
77 else
78 already_exported=$(git -C "$git_backup" rev-list --count muse-mirror 2>/dev/null || echo 1)
79 already_exported=$((already_exported - 1))
80 [ "$already_exported" -lt 0 ] && already_exported=0
81 fi
82
83 local commits total n skip
84 commits=$(muse -C "$muse_copy" rev-list --reverse HEAD --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
85
86 skip="${SKIP:-$already_exported}"
87 if [ "$skip" -gt 0 ]; then
88 echo "Skipping first $skip commits already on $label-mirror"
89 commits=$(echo "$commits" | tail -n "+$((skip + 1))")
90 fi
91
92 if [ -z "$commits" ]; then
93 echo "Nothing to export — $label-mirror is already fully replayed."
94 return 0
95 fi
96
97 if [ -n "${LIMIT:-}" ]; then
98 # head -n via a pipe can SIGPIPE the writer on large input (head exits
99 # once it has its N lines, closing its read end while echo is still
100 # blocked writing the rest) -- a here-string avoids the pipe entirely.
101 # Only surfaced at real scale (1459 commits); tiny test fixtures never
102 # produced enough output to hit the pipe-buffer boundary.
103 commits=$(head -n "$LIMIT" <<< "$commits")
104 echo "LIMIT=$LIMIT set — replaying only $LIMIT commits from this point (dry run)"
105 fi
106
107 total=$(echo "$commits" | wc -l | tr -d ' ')
108 n=0
109 while IFS= read -r cid; do
110 n=$((n + 1))
111 echo "[$n/$total] exporting $cid"
112 (cd "$muse_copy" && muse bridge git-export \
113 --muse-ref "$cid" \
114 --git-dir "$git_backup" \
115 --git-branch muse-mirror \
116 --no-push \
117 --allow-empty \
118 --json)
119 done <<< "$commits"
120
121 echo "git history length:"
122 git -C "$git_backup" rev-list --count muse-mirror
123 }
124
125 cmd_replay_muse() {
126 _replay_full_history "$MUSE_COPY" "$MUSE_GIT_BACKUP" "muse"
127 }
128
129 cmd_replay_musehub() {
130 _replay_full_history "$MUSEHUB_COPY" "$MUSEHUB_GIT_BACKUP" "musehub"
131 }
132
133 cmd_replay_all() {
134 local log_muse log_musehub
135 log_muse=$(mktemp)
136 log_musehub=$(mktemp)
137 echo "Replaying muse and musehub in parallel — logs: $log_muse , $log_musehub"
138
139 ( _replay_full_history "$MUSE_COPY" "$MUSE_GIT_BACKUP" "muse" > "$log_muse" 2>&1 ) &
140 local pid_muse=$!
141 ( _replay_full_history "$MUSEHUB_COPY" "$MUSEHUB_GIT_BACKUP" "musehub" > "$log_musehub" 2>&1 ) &
142 local pid_musehub=$!
143
144 wait "$pid_muse" && echo "✅ muse replay finished" || echo "🛑 muse replay FAILED — see $log_muse"
145 wait "$pid_musehub" && echo "✅ musehub replay finished" || echo "🛑 musehub replay FAILED — see $log_musehub"
146
147 echo "--- muse log ---"
148 cat "$log_muse"
149 echo "--- musehub log ---"
150 cat "$log_musehub"
151 }
152
153 _github_push_one() {
154 local git_backup="$1" gh_repo="$2" description="$3"
155 guard "$git_backup"
156
157 gh auth status || { echo "Run 'gh auth login' first"; exit 1; }
158
159 # public -- muse and musehub are developed in public.
160 gh repo view "$gh_repo" >/dev/null 2>&1 || gh repo create "$gh_repo" --public --description "$description"
161
162 # https, not [email protected] SSH -- matches `gh auth status`'s reported
163 # git protocol, which uses gh's own credential helper (no SSH key setup
164 # required).
165 git -C "$git_backup" remote get-url origin >/dev/null 2>&1 || git -C "$git_backup" remote add origin "https://github.com/$gh_repo.git"
166
167 git -C "$git_backup" push -u origin muse-mirror
168 }
169
170 cmd_github_push_muse() {
171 _github_push_one "$MUSE_GIT_BACKUP" "$GH_MUSE_REPO" "Git-bridge backup mirror of the muse Muse repo"
172 }
173
174 cmd_github_push_musehub() {
175 _github_push_one "$MUSEHUB_GIT_BACKUP" "$GH_MUSEHUB_REPO" "Git-bridge backup mirror of the musehub Muse repo"
176 }
177
178 cmd_github_push() {
179 cmd_github_push_muse
180 cmd_github_push_musehub
181 }
182
183 _verify_one() {
184 local muse_copy="$1" gh_repo="$2" clone_dir="$3" label="$4"
185 guard "$muse_copy"
186
187 rm -rf "$clone_dir"
188 git clone "https://github.com/$gh_repo.git" "$clone_dir"
189 git -C "$clone_dir" checkout muse-mirror
190 echo "--- $label diff (expect no output) ---"
191 diff -rq --exclude=.git --exclude=.muse "$muse_copy" "$clone_dir" || true
192 }
193
194 cmd_verify_muse() {
195 _verify_one "$MUSE_COPY" "$GH_MUSE_REPO" /tmp/verify-muse-backup muse
196 }
197
198 cmd_verify_musehub() {
199 _verify_one "$MUSEHUB_COPY" "$GH_MUSEHUB_REPO" /tmp/verify-musehub-backup musehub
200 }
201
202 cmd_verify() {
203 cmd_verify_muse
204 cmd_verify_musehub
205 }
206
207 _incremental() {
208 local muse_copy="$1" git_backup="$2"
209 local last_bridged new_commits
210 guard "$muse_copy"
211 guard "$git_backup"
212
213 last_bridged=$(python3 -c "
214 import tomllib
215 with open('$muse_copy/.muse/git-bridge.toml', 'rb') as f:
216 state = tomllib.load(f)
217 print(state['last_export']['muse_commit_id'])
218 ")
219 echo "Last bridged commit: $last_bridged"
220
221 new_commits=$(muse -C "$muse_copy" rev-list "$last_bridged..HEAD" --reverse --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
222
223 if [ -z "$new_commits" ]; then
224 echo "Nothing new since the last backup — done."
225 return 0
226 fi
227
228 while IFS= read -r cid; do
229 echo "exporting $cid"
230 (cd "$muse_copy" && muse bridge git-export \
231 --muse-ref "$cid" \
232 --git-dir "$git_backup" \
233 --git-branch muse-mirror \
234 --no-push \
235 --allow-empty \
236 --json)
237 done <<< "$new_commits"
238
239 git -C "$git_backup" push origin muse-mirror
240 }
241
242 cmd_incremental_muse() {
243 _incremental "$MUSE_COPY" "$MUSE_GIT_BACKUP"
244 }
245
246 cmd_incremental_musehub() {
247 _incremental "$MUSEHUB_COPY" "$MUSEHUB_GIT_BACKUP"
248 }
249
250 SUBCOMMAND="${1:-}"
251 shift || true
252
253 LIMIT=""
254 SKIP=""
255 while [ $# -gt 0 ]; do
256 case "$1" in
257 --limit) LIMIT="${2:?--limit requires a number}"; shift 2 ;;
258 --skip) SKIP="${2:?--skip requires a number}"; shift 2 ;;
259 *) echo "Unknown flag: $1"; exit 1 ;;
260 esac
261 done
262
263 case "$SUBCOMMAND" in
264 preflight) cmd_preflight ;;
265 replay-muse) cmd_replay_muse ;;
266 replay-musehub) cmd_replay_musehub ;;
267 replay-all) cmd_replay_all ;;
268 github-push-muse) cmd_github_push_muse ;;
269 github-push-musehub) cmd_github_push_musehub ;;
270 github-push) cmd_github_push ;;
271 verify-muse) cmd_verify_muse ;;
272 verify-musehub) cmd_verify_musehub ;;
273 verify) cmd_verify ;;
274 incremental-muse) cmd_incremental_muse ;;
275 incremental-musehub) cmd_incremental_musehub ;;
276 *)
277 echo "Usage: $0 {preflight|replay-muse|replay-musehub|replay-all|github-push-muse|github-push-musehub|github-push|verify-muse|verify-musehub|verify|incremental-muse|incremental-musehub} [--skip N] [--limit N]"
278 exit 1
279 ;;
280 esac
File History 1 commit
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 days ago