gabriel / muse public
muse-git-backup.sh bash
183 lines 6.5 KB
Raw
sha256:74edab09b21c5878acf84e50d3a5e64c20987ec6da01cb9a1a85c1a68be94ed3 chore: add muse-git-backup.sh — tested wrapper for the git-… Sonnet 5 patch 13 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
8 # ./muse-git-backup.sh replay-musehub
9 # ./muse-git-backup.sh github-push
10 # ./muse-git-backup.sh verify
11 # ./muse-git-backup.sh incremental-muse
12 # ./muse-git-backup.sh incremental-musehub
13 #
14 # Each subcommand is independent and safe to re-run. This script never
15 # touches ~/ecosystem/muse or ~/ecosystem/musehub — every operation targets
16 # the copies under /Users/gabriel/dev/copies 2/ and brand-new directories/
17 # GitHub repos this script creates fresh. Run as ./muse-git-backup.sh <cmd>,
18 # never sourced into your interactive shell — that's what makes `exit`
19 # inside this script safe (it only ends the script's own process).
20
21 set -euo pipefail
22
23 MUSE_COPY="/Users/gabriel/dev/copies 2/muse 03"
24 MUSEHUB_COPY="/Users/gabriel/dev/copies 2/musehub 03"
25 MUSE_GIT_BACKUP="/Users/gabriel/dev/copies 2/muse-git-backup"
26 MUSEHUB_GIT_BACKUP="/Users/gabriel/dev/copies 2/musehub-git-backup"
27 GH_MUSE_REPO="gabriel/muse-backup"
28 GH_MUSEHUB_REPO="gabriel/musehub-backup"
29
30 guard() {
31 case "$1" in
32 *ecosystem*) echo "❌ REFUSED: path contains 'ecosystem': $1"; exit 1 ;;
33 esac
34 }
35
36 cmd_preflight() {
37 test -d "$MUSE_COPY/.muse" && echo "muse copy: OK" || { echo "muse copy: MISSING .muse/ — STOP"; exit 1; }
38 test -d "$MUSEHUB_COPY/.muse" && echo "musehub copy: OK" || { echo "musehub copy: MISSING .muse/ — STOP"; exit 1; }
39
40 if [ -L "$MUSE_COPY" ]; then echo "STOP — muse copy is a symlink"; exit 1; fi
41 if [ -L "$MUSEHUB_COPY" ]; then echo "STOP — musehub copy is a symlink"; exit 1; fi
42 echo "muse copy: real directory, not a symlink"
43 echo "musehub copy: real directory, not a symlink"
44
45 echo "muse commit count:"
46 muse -C "$MUSE_COPY" rev-list --count HEAD --json
47 echo "musehub commit count:"
48 muse -C "$MUSEHUB_COPY" rev-list --count HEAD --json
49 }
50
51 _replay_full_history() {
52 local muse_copy="$1" git_backup="$2" label="$3"
53 guard "$git_backup"
54
55 mkdir -p "$git_backup"
56 if [ ! -d "$git_backup/.git" ]; then
57 git -C "$git_backup" init -q
58 git -C "$git_backup" config user.email "[email protected]"
59 git -C "$git_backup" config user.name "gabriel"
60 git -C "$git_backup" commit --allow-empty -qm "init ($label git-bridge backup mirror)"
61 fi
62
63 local commits total n
64 commits=$(muse -C "$muse_copy" rev-list --reverse HEAD --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
65 if [ -n "${LIMIT:-}" ]; then
66 commits=$(echo "$commits" | head -n "$LIMIT")
67 echo "LIMIT=$LIMIT set — replaying only the first $LIMIT commits (dry run)"
68 echo "⚠️ Before running the full replay, delete $git_backup — replay always"
69 echo " starts from commit 1, so re-running without cleanup duplicates these."
70 fi
71 total=$(echo "$commits" | wc -l | tr -d ' ')
72 n=0
73 while IFS= read -r cid; do
74 n=$((n + 1))
75 echo "[$n/$total] exporting $cid"
76 (cd "$muse_copy" && muse bridge git-export \
77 --muse-ref "$cid" \
78 --git-dir "$git_backup" \
79 --git-branch muse-mirror \
80 --no-push \
81 --allow-empty \
82 --json)
83 done <<< "$commits"
84
85 echo "git history length:"
86 git -C "$git_backup" rev-list --count muse-mirror
87 }
88
89 cmd_replay_muse() {
90 _replay_full_history "$MUSE_COPY" "$MUSE_GIT_BACKUP" "muse"
91 }
92
93 cmd_replay_musehub() {
94 _replay_full_history "$MUSEHUB_COPY" "$MUSEHUB_GIT_BACKUP" "musehub"
95 }
96
97 cmd_github_push() {
98 gh auth status || { echo "Run 'gh auth login' first"; exit 1; }
99
100 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"
101 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"
102
103 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"
104 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"
105
106 git -C "$MUSE_GIT_BACKUP" push -u origin muse-mirror
107 git -C "$MUSEHUB_GIT_BACKUP" push -u origin muse-mirror
108 }
109
110 cmd_verify() {
111 rm -rf /tmp/verify-muse-backup
112 git clone "[email protected]:$GH_MUSE_REPO.git" /tmp/verify-muse-backup
113 git -C /tmp/verify-muse-backup checkout muse-mirror
114 echo "--- muse diff (expect no output) ---"
115 diff -rq --exclude=.git --exclude=.muse "$MUSE_COPY" /tmp/verify-muse-backup || true
116
117 rm -rf /tmp/verify-musehub-backup
118 git clone "[email protected]:$GH_MUSEHUB_REPO.git" /tmp/verify-musehub-backup
119 git -C /tmp/verify-musehub-backup checkout muse-mirror
120 echo "--- musehub diff (expect no output) ---"
121 diff -rq --exclude=.git --exclude=.muse "$MUSEHUB_COPY" /tmp/verify-musehub-backup || true
122 }
123
124 _incremental() {
125 local muse_copy="$1" git_backup="$2"
126 local last_bridged new_commits
127
128 last_bridged=$(python3 -c "
129 import tomllib
130 with open('$muse_copy/.muse/git-bridge.toml', 'rb') as f:
131 state = tomllib.load(f)
132 print(state['last_export']['muse_commit_id'])
133 ")
134 echo "Last bridged commit: $last_bridged"
135
136 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']))")
137
138 if [ -z "$new_commits" ]; then
139 echo "Nothing new since the last backup — done."
140 return 0
141 fi
142
143 while IFS= read -r cid; do
144 echo "exporting $cid"
145 (cd "$muse_copy" && muse bridge git-export \
146 --muse-ref "$cid" \
147 --git-dir "$git_backup" \
148 --git-branch muse-mirror \
149 --no-push \
150 --allow-empty \
151 --json)
152 done <<< "$new_commits"
153
154 git -C "$git_backup" push origin muse-mirror
155 }
156
157 cmd_incremental_muse() {
158 _incremental "$MUSE_COPY" "$MUSE_GIT_BACKUP"
159 }
160
161 cmd_incremental_musehub() {
162 _incremental "$MUSEHUB_COPY" "$MUSEHUB_GIT_BACKUP"
163 }
164
165 SUBCOMMAND="${1:-}"
166 LIMIT=""
167 if [ "${2:-}" = "--limit" ]; then
168 LIMIT="${3:?--limit requires a number}"
169 fi
170
171 case "$SUBCOMMAND" in
172 preflight) cmd_preflight ;;
173 replay-muse) cmd_replay_muse ;;
174 replay-musehub) cmd_replay_musehub ;;
175 github-push) cmd_github_push ;;
176 verify) cmd_verify ;;
177 incremental-muse) cmd_incremental_muse ;;
178 incremental-musehub) cmd_incremental_musehub ;;
179 *)
180 echo "Usage: $0 {preflight|replay-muse|replay-musehub|github-push|verify|incremental-muse|incremental-musehub} [--limit N]"
181 exit 1
182 ;;
183 esac
File History 1 commit
sha256:74edab09b21c5878acf84e50d3a5e64c20987ec6da01cb9a1a85c1a68be94ed3 chore: add muse-git-backup.sh — tested wrapper for the git-… Sonnet 5 patch 13 days ago