gabriel / muse public
pack_objects.py python
159 lines 5.3 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """muse pack-objects — build a MPackBundle and write to stdout.
2
3 Collects a set of commits (and all referenced snapshots and objects) into a
4 single msgpack MPackBundle suitable for transport to a remote. Efficient binary
5 encoding with raw bytes for object content (no base64 overhead).
6
7 Usage::
8
9 muse pack-objects <want_id>... [--have <id>...]
10
11 The ``--have`` IDs are commits the receiver already has. Objects reachable
12 exclusively from ``--have`` ancestors are pruned from the bundle.
13
14 Output: a MPackBundle msgpack binary written to stdout (pipe to a file or HTTP
15 request body).
16
17 Output contract
18 ---------------
19
20 - Exit 0: pack written to stdout.
21 - Exit 1: a wanted commit not found or HEAD has no commits.
22 - Exit 3: I/O error reading objects or snapshots from the local store.
23
24 Agent use — dry-run inspection
25 -------------------------------
26
27 Agents can inspect what *would* be packed without producing binary output::
28
29 muse pack-objects HEAD --dry-run
30 # → {
31 # "want": [...], "have": [...],
32 # "commits": 3, "snapshots": 3, "objects": 12, "object_bytes": 40960,
33 # "duration_ms": 4.2, "exit_code": 0
34 # }
35
36 ``object_bytes`` is the total uncompressed byte size of all object payloads in
37 the pack. Agents use it to decide whether to buffer the full bundle in memory
38 or stream it directly to the remote.
39 """
40
41 from __future__ import annotations
42
43 import argparse
44 import json
45 import logging
46 import sys
47
48 import msgpack
49
50 from muse.core.errors import ExitCode
51 from muse.core.pack import build_mpack
52 from muse.core.repo import require_repo
53 from muse.core.store import get_head_commit_id, read_current_branch
54 from muse.core.validation import validate_object_id
55 from muse.core.timing import start_timer
56
57 logger = logging.getLogger(__name__)
58
59
60 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
61 """Register the pack-objects subcommand."""
62 parser = subparsers.add_parser(
63 "pack-objects",
64 help="Build a MPackBundle from wanted commits and write to stdout.",
65 description=__doc__,
66 formatter_class=argparse.RawDescriptionHelpFormatter,
67 )
68 parser.add_argument(
69 "want",
70 nargs="+",
71 help="Commit IDs to pack. May be full hex IDs or 'HEAD'.",
72 )
73 parser.add_argument(
74 "--have",
75 action="append",
76 default=[],
77 dest="have",
78 metavar="COMMIT_ID",
79 help="Commits the receiver already has (pruned from pack). Repeat for multiple.",
80 )
81 parser.add_argument(
82 "--dry-run",
83 action="store_true",
84 dest="dry_run",
85 help=(
86 "Print pack summary as JSON instead of writing binary msgpack. "
87 "Includes elapsed(), exit_code, and object_bytes for agent pipelines."
88 ),
89 )
90 parser.set_defaults(func=run)
91
92
93 def run(args: argparse.Namespace) -> None:
94 """Build a MPackBundle from wanted commits and write to stdout.
95
96 Traverses the commit graph from each ``want`` ID, collecting all
97 commits, snapshots, and objects not already reachable from ``--have``
98 ancestors. The resulting binary bundle can be piped directly to
99 ``muse unpack-objects`` on the receiving side, or sent via
100 HTTP to a MuseHub endpoint.
101
102 Use ``--dry-run`` to receive a JSON summary instead of binary output —
103 safer for agent pipelines that just want to know the pack size before
104 committing to a transfer. The summary includes ``object_bytes`` (total
105 uncompressed payload size) so agents can choose buffer-vs-stream.
106 """
107 elapsed = start_timer()
108 want: list[str] = args.want
109 have: list[str] = args.have
110 dry_run: bool = args.dry_run
111
112 root = require_repo()
113
114 # Resolve "HEAD" → commit ID and validate all other IDs upfront so
115 # we fail loudly instead of silently producing empty packs.
116 resolved_wants: list[str] = []
117 for w in want:
118 if w.upper() == "HEAD":
119 branch = read_current_branch(root)
120 cid = get_head_commit_id(root, branch)
121 if cid is None:
122 print(json.dumps({"error": "HEAD has no commits"}), file=sys.stderr)
123 raise SystemExit(ExitCode.USER_ERROR)
124 resolved_wants.append(cid)
125 else:
126 try:
127 validate_object_id(w)
128 except ValueError as exc:
129 print(json.dumps({"error": f"Invalid want ID: {exc}"}), file=sys.stderr)
130 raise SystemExit(ExitCode.USER_ERROR)
131 resolved_wants.append(w)
132
133 for h in have:
134 try:
135 validate_object_id(h)
136 except ValueError as exc:
137 print(json.dumps({"error": f"Invalid --have ID: {exc}"}), file=sys.stderr)
138 raise SystemExit(ExitCode.USER_ERROR)
139
140 bundle = build_mpack(root, commit_ids=resolved_wants, have=have)
141
142 if dry_run:
143 object_bytes = sum(
144 len(obj["content"]) if isinstance(obj.get("content"), (bytes, bytearray)) else 0
145 for obj in bundle["objects"]
146 )
147 print(json.dumps({
148 "want": resolved_wants,
149 "have": have,
150 "commits": len(bundle["commits"]),
151 "snapshots": len(bundle["snapshots"]),
152 "objects": len(bundle["objects"]),
153 "object_bytes": object_bytes,
154 "duration_ms": elapsed(),
155 "exit_code": 0,
156 }))
157 return
158
159 sys.stdout.buffer.write(msgpack.packb(bundle, use_bin_type=True))
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 144 days ago