gabriel / muse public
migrate.py python
185 lines 6.9 KB
Raw
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 8 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 2 commits
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 8 days ago
sha256:869ab660acd6d8d0faceb4b5a29cc5e0e304660f38a9fc9aa84285bad3318cb7 docs: update muse code migrate docstring to reflect all mig… Sonnet 4.6 minor 22 days ago