gabriel / muse public
migrate.py python
185 lines 6.9 KB
Raw
sha256:e8214e0062ef8ef0af999937df2731655b6082781fc22bc563f58db2f42b1de9 Merge 'bump/muse-v0.2.1' into 'dev' — proposal: chore: bump… Human 9 days ago
1 """``muse code migrate`` — unified object store migration + v2 commit ID replay.
2
3 Brings a repository fully up to date with the current on-disk format.
4 Safe to run on an already-migrated repo — all passes are idempotent.
5
6 Passes performed (in order)
7 ----------------------------
8 1. **Blob relocation** — legacy blobs stored without an algo subdirectory
9 are moved to the canonical ``.muse/objects/sha256/`` layout.
10
11 2. **Snapshot relocation** — msgpack files in ``.muse/snapshots/sha256/``
12 are moved into the unified ``.muse/objects/sha256/`` store and the
13 legacy ``.muse/snapshots/`` directory is removed.
14
15 3. **Commit relocation** — msgpack files in ``.muse/commits/sha256/`` are
16 moved into the unified ``.muse/objects/sha256/`` store and the legacy
17 ``.muse/commits/`` directory is removed.
18
19 4. **v2 commit ID rewrite** — rewrites commits whose IDs were produced by
20 the v1 formula (which did not bind ``repo_id``, ``author``, or
21 ``signer_public_key``). All branch refs, remote tracking refs, and
22 reflogs are updated to the new IDs.
23
24 5. **Signature normalisation** — bare base64 Ed25519 signatures are
25 normalised to the ``ed25519:…`` prefix.
26
27 Dry-run mode (``--dry-run``) is the default-safe entry point — it prints the
28 full id_map and counts but makes zero writes.
29
30 Usage::
31
32 muse code migrate --dry-run # inspect, zero writes (safe)
33 muse code migrate # execute the full migration
34 muse code migrate --sign # also sign unsigned commits
35 muse code migrate --json # machine-readable output
36
37 Output::
38
39 {
40 "commits_rewritten": <int>,
41 "blobs_migrated": <int>,
42 "blobs_rewritten": <int>,
43 "snapshots_relocated": <int>,
44 "snapshots_rewritten": <int>,
45 "commits_relocated": <int>,
46 "commits_unified": <int>,
47 "legacy_dirs_removed": <int>,
48 "refs_updated": <int>,
49 "remote_refs_updated": <int>,
50 "signatures_normalised":<int>,
51 "commits_signed": <int>,
52 "id_map": {"sha256:<old>": "sha256:<new>", ...},
53 "dry_run": true | false
54 }
55
56 Exit codes
57 ----------
58 - 0: Migration completed (or dry-run completed) successfully.
59 - 1: Merge or rebase in progress — finish it first.
60 - 2: I/O error during migration.
61 """
62
63 import argparse
64 import json
65 import logging
66 import sys
67 import time
68
69 from muse.core.errors import ExitCode
70 from muse.core.migrate import migrate
71 from muse.core.repo import require_repo
72
73 logger = logging.getLogger(__name__)
74
75 def register(subparsers: argparse._SubParsersAction) -> None:
76 parser = subparsers.add_parser(
77 "migrate",
78 help="Rewrite the commit DAG with the v2 commit ID formula.",
79 description=__doc__,
80 formatter_class=argparse.RawDescriptionHelpFormatter,
81 )
82 parser.add_argument(
83 "--dry-run",
84 action="store_true",
85 default=False,
86 help="Print the id_map and counts but make no writes (default: False).",
87 )
88 parser.add_argument(
89 "--sign",
90 action="store_true",
91 default=False,
92 help="Sign unsigned commits (and re-sign invalidated ones) with the current identity.",
93 )
94 parser.add_argument(
95 "--force-resign",
96 dest="force_resign",
97 action="store_true",
98 default=False,
99 help="Re-sign every commit with the current identity, even already-signed ones. "
100 "Implies --sign. Use when the repo was signed with a different key.",
101 )
102 parser.add_argument(
103 "--json", "-j",
104 dest="json_out",
105 action="store_true",
106 default=False,
107 help="Emit machine-readable JSON.",
108 )
109 parser.set_defaults(func=run)
110
111 def run(args: argparse.Namespace) -> None:
112 """Execute the v2 commit ID migration (or dry-run)."""
113 t0 = time.monotonic()
114 json_out: bool = args.json_out
115 dry_run: bool = args.dry_run
116
117 root = require_repo()
118
119 signing_identity = None
120 if args.sign or args.force_resign:
121 from muse.cli.config import get_signing_identity, list_remotes
122 signing_identity = get_signing_identity(repo_root=root)
123 if signing_identity is None:
124 for remote in list_remotes(repo_root=root):
125 signing_identity = get_signing_identity(repo_root=root, remote_url=remote["url"])
126 if signing_identity is not None:
127 break
128
129 try:
130 result = migrate(root, dry_run=dry_run, signing_identity=signing_identity, force_resign=args.force_resign)
131 except (RuntimeError, ValueError) as exc:
132 msg = str(exc)
133 if json_out:
134 print(json.dumps({"error": msg}))
135 else:
136 print(f"❌ {msg}", file=sys.stderr)
137 raise SystemExit(ExitCode.USER_ERROR)
138 except OSError as exc:
139 msg = f"I/O error during migration: {exc}"
140 if json_out:
141 print(json.dumps({"error": msg}))
142 else:
143 print(f"❌ {msg}", file=sys.stderr)
144 raise SystemExit(ExitCode.INTERNAL_ERROR)
145
146 elapsed_ms = round((time.monotonic() - t0) * 1000)
147
148 if json_out:
149 print(json.dumps({
150 "commits_rewritten": result.commits_rewritten,
151 "blobs_migrated": result.blobs_migrated,
152 "legacy_dirs_removed": result.legacy_dirs_removed,
153 "commits_relocated": result.commits_relocated,
154 "snapshots_relocated": result.snapshots_relocated,
155 "refs_updated": result.refs_updated,
156 "remote_refs_updated": result.remote_refs_updated,
157 "repo_id_updated": result.repo_id_updated,
158 "branch_fields_renamed": result.branch_fields_renamed,
159 "signatures_normalised": result.signatures_normalised,
160 "format_versions_bumped": result.format_versions_bumped,
161 "reflogs_updated": result.reflogs_updated,
162 "commits_signed": result.commits_signed,
163 "unsigned_commits_skipped": result.unsigned_commits_skipped,
164 "blobs_rewritten": result.blobs_rewritten,
165 "snapshots_rewritten": result.snapshots_rewritten,
166 "commits_unified": result.commits_unified,
167 "id_map": result.id_map,
168 "dry_run": result.dry_run,
169 "duration_ms": elapsed_ms,
170 }))
171 return
172
173 prefix = "[dry-run] " if dry_run else ""
174 print(f"{prefix}commits rewritten: {result.commits_rewritten}")
175 print(f"{prefix}blobs migrated: {result.blobs_migrated}")
176 print(f"{prefix}legacy dirs removed: {result.legacy_dirs_removed}")
177 print(f"{prefix}blobs rewritten: {result.blobs_rewritten}")
178 print(f"{prefix}snapshots rewritten: {result.snapshots_rewritten}")
179 print(f"{prefix}commits unified: {result.commits_unified}")
180 if result.id_map:
181 print(f"\n{prefix}ID map ({len(result.id_map)} changed):")
182 for old, new in result.id_map.items():
183 print(f" {old} → {new}")
184 else:
185 print(f"\n{prefix}No commits needed rewriting.")
File History 4 commits
sha256:e8214e0062ef8ef0af999937df2731655b6082781fc22bc563f58db2f42b1de9 Merge 'bump/muse-v0.2.1' into 'dev' — proposal: chore: bump… Human 9 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 66 days ago
sha256:a317886dc0496c4af7b285b3e41c86c4c34ea2e79afc63b8829aadb1ada7903f chore: bump version to 0.2.0rc15 to match musehub#113 fix release Sonnet 4.6 patch 66 days ago
sha256:f3b726b50f0aee3622bba751e0a67aa7ae4cf75a798477dbce581940b6a9cf70 feat: migrate invariants cache to .muse/cache/invariants.ms… Sonnet 4.6 patch 134 days ago