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