"""muse plumbing pack-objects — build a PackBundle and write to stdout. Collects a set of commits (and all referenced snapshots and objects) into a single msgpack PackBundle suitable for transport to a remote. Analogous to ``git pack-objects`` using a binary packfile format — efficient binary encoding with raw bytes for object content (no base64 overhead). Usage:: muse plumbing pack-objects ... [--have ...] The ``--have`` IDs are commits the receiver already has. Objects reachable exclusively from ``--have`` ancestors are pruned from the bundle. Output: a PackBundle msgpack binary written to stdout (pipe to a file or HTTP request body). Plumbing contract ----------------- - Exit 0: pack written to stdout. - Exit 1: a wanted commit not found or HEAD has no commits. - Exit 3: I/O error reading objects or snapshots from the local store. Agent use — dry-run inspection ------------------------------- Agents can inspect what *would* be packed without producing binary output:: muse plumbing pack-objects HEAD --dry-run # → {"want": [...], "have": [...], "commits": 3, "snapshots": 3, "objects": 12} """ from __future__ import annotations import argparse import json import logging import sys import msgpack from muse.core.errors import ExitCode from muse.core.pack import build_pack from muse.core.repo import require_repo from muse.core.store import get_head_commit_id, read_current_branch from muse.core.validation import validate_object_id logger = logging.getLogger(__name__) def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the pack-objects subcommand.""" parser = subparsers.add_parser( "pack-objects", help="Build a PackBundle JSON from wanted commits and write to stdout.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "want", nargs="+", help="Commit IDs to pack. May be full hex IDs or 'HEAD'.", ) parser.add_argument( "--have", action="append", default=[], dest="have", metavar="COMMIT_ID", help="Commits the receiver already has (pruned from pack). Repeat for multiple.", ) parser.add_argument( "--dry-run", action="store_true", dest="dry_run", help=( "Print pack summary as JSON instead of writing binary msgpack. " "Agents can use this to inspect pack contents without piping binary." ), ) parser.set_defaults(func=run) def run(args: argparse.Namespace) -> None: """Build a PackBundle JSON from wanted commits and write to stdout. Traverses the commit graph from each ``want`` ID, collecting all commits, snapshots, and objects not already reachable from ``--have`` ancestors. The resulting binary bundle can be piped directly to ``muse plumbing unpack-objects`` on the receiving side, or sent via HTTP to a MuseHub endpoint. Use ``--dry-run`` to receive a JSON summary instead of binary output — safer for agent pipelines that just want to know the pack size. """ want: list[str] = args.want have: list[str] = args.have dry_run: bool = args.dry_run root = require_repo() # Resolve "HEAD" → commit ID and validate all other IDs upfront so # we fail loudly instead of silently producing empty packs. resolved_wants: list[str] = [] for w in want: if w.upper() == "HEAD": branch = read_current_branch(root) cid = get_head_commit_id(root, branch) if cid is None: print(json.dumps({"error": "HEAD has no commits"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) resolved_wants.append(cid) else: try: validate_object_id(w) except ValueError as exc: print(json.dumps({"error": f"Invalid want ID: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) resolved_wants.append(w) for h in have: try: validate_object_id(h) except ValueError as exc: print(json.dumps({"error": f"Invalid --have ID: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) bundle = build_pack(root, commit_ids=resolved_wants, have=have) if dry_run: print(json.dumps({ "want": resolved_wants, "have": have, "commits": len(bundle["commits"]), "snapshots": len(bundle["snapshots"]), "objects": len(bundle["objects"]), })) return sys.stdout.buffer.write(msgpack.packb(bundle, use_bin_type=True))