gabriel / muse public
migrate.py python
149 lines 5.1 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """``muse code migrate`` — commit-id-v2 DAG replay.
2
3 Rewrites every commit in the repository with the v2 commit ID formula,
4 which binds ``repo_id``, ``author``, and ``signer_public_key`` to the
5 content hash — closing the key-swap and cross-repo replay attack surfaces.
6
7 Also performs two cleanup passes in the same run:
8
9 - Bare base64 Ed25519 signatures are normalised to the ``ed25519:…`` prefix.
10 - Legacy object blobs stored without an algo subdirectory are moved to the
11 canonical ``sha256/`` layout.
12
13 Dry-run mode (``--dry-run``) is the default-safe entry point — it prints the
14 full id_map and counts but makes zero writes.
15
16 Usage::
17
18 muse code migrate --dry-run # inspect, zero writes (safe)
19 muse code migrate # execute the full migration
20 muse code migrate --json # machine-readable output
21
22 Output::
23
24 {
25 "commits_rewritten": <int>,
26 "blobs_migrated": <int>,
27 "id_map": {"sha256:<old>": "sha256:<new>", ...},
28 "dry_run": true | false
29 }
30
31 Exit codes
32 ----------
33 - 0: Migration completed (or dry-run completed) successfully.
34 - 1: Merge or rebase in progress — finish it first.
35 - 2: I/O error during migration.
36 """
37
38 import argparse
39 import json
40 import logging
41 import sys
42 import time
43
44 from muse.core.errors import ExitCode
45 from muse.core.migrate import migrate
46 from muse.core.repo import require_repo
47
48 logger = logging.getLogger(__name__)
49
50 def register(subparsers: argparse._SubParsersAction) -> None:
51 parser = subparsers.add_parser(
52 "migrate",
53 help="Rewrite the commit DAG with the v2 commit ID formula.",
54 description=__doc__,
55 formatter_class=argparse.RawDescriptionHelpFormatter,
56 )
57 parser.add_argument(
58 "--dry-run",
59 action="store_true",
60 default=False,
61 help="Print the id_map and counts but make no writes (default: False).",
62 )
63 parser.add_argument(
64 "--sign",
65 action="store_true",
66 default=False,
67 help="Sign unsigned commits (and re-sign invalidated ones) with the current identity.",
68 )
69 parser.add_argument(
70 "--force-resign",
71 dest="force_resign",
72 action="store_true",
73 default=False,
74 help="Re-sign every commit with the current identity, even already-signed ones. "
75 "Implies --sign. Use when the repo was signed with a different key.",
76 )
77 parser.add_argument(
78 "--json", "-j",
79 dest="json_out",
80 action="store_true",
81 default=False,
82 help="Emit machine-readable JSON.",
83 )
84 parser.set_defaults(func=run)
85
86 def run(args: argparse.Namespace) -> None:
87 """Execute the v2 commit ID migration (or dry-run)."""
88 t0 = time.monotonic()
89 json_out: bool = args.json_out
90 dry_run: bool = args.dry_run
91
92 root = require_repo()
93
94 signing_identity = None
95 if args.sign or args.force_resign:
96 from muse.cli.config import get_signing_identity
97 signing_identity = get_signing_identity(repo_root=root)
98
99 try:
100 result = migrate(root, dry_run=dry_run, signing_identity=signing_identity, force_resign=args.force_resign)
101 except RuntimeError as exc:
102 msg = str(exc)
103 if json_out:
104 print(json.dumps({"error": msg}))
105 else:
106 print(f"❌ {msg}", file=sys.stderr)
107 raise SystemExit(ExitCode.USER_ERROR)
108 except OSError as exc:
109 msg = f"I/O error during migration: {exc}"
110 if json_out:
111 print(json.dumps({"error": msg}))
112 else:
113 print(f"❌ {msg}", file=sys.stderr)
114 raise SystemExit(ExitCode.INTERNAL_ERROR)
115
116 elapsed_ms = round((time.monotonic() - t0) * 1000)
117
118 if json_out:
119 print(json.dumps({
120 "commits_rewritten": result.commits_rewritten,
121 "blobs_migrated": result.blobs_migrated,
122 "legacy_dirs_removed": result.legacy_dirs_removed,
123 "commits_relocated": result.commits_relocated,
124 "snapshots_relocated": result.snapshots_relocated,
125 "refs_updated": result.refs_updated,
126 "remote_refs_updated": result.remote_refs_updated,
127 "repo_id_updated": result.repo_id_updated,
128 "branch_fields_renamed": result.branch_fields_renamed,
129 "signatures_normalised": result.signatures_normalised,
130 "format_versions_bumped": result.format_versions_bumped,
131 "reflogs_updated": result.reflogs_updated,
132 "commits_signed": result.commits_signed,
133 "unsigned_commits_skipped": result.unsigned_commits_skipped,
134 "id_map": result.id_map,
135 "dry_run": result.dry_run,
136 "duration_ms": elapsed_ms,
137 }))
138 return
139
140 prefix = "[dry-run] " if dry_run else ""
141 print(f"{prefix}commits rewritten: {result.commits_rewritten}")
142 print(f"{prefix}blobs migrated: {result.blobs_migrated}")
143 print(f"{prefix}legacy dirs removed: {result.legacy_dirs_removed}")
144 if result.id_map:
145 print(f"\n{prefix}ID map ({len(result.id_map)} changed):")
146 for old, new in result.id_map.items():
147 print(f" {old[:20]}… → {new[:20]}…")
148 else:
149 print(f"\n{prefix}No commits needed rewriting.")
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago