"""muse plumbing read-commit — emit full commit metadata as JSON. Reads a commit record by its SHA-256 ID and emits the complete JSON representation including provenance fields, CRDT annotations, and the structured delta. Equivalent to ``git cat-file commit`` but producing the Muse JSON schema directly. Output:: { "format_version": 5, "commit_id": "", "repo_id": "", "branch": "main", "snapshot_id": "", "message": "Add verse melody", "committed_at": "2026-03-18T12:00:00+00:00", "parent_commit_id": " | null", "parent2_commit_id": null, "author": "gabriel", "agent_id": "", "model_id": "", "sem_ver_bump": "none", "breaking_changes": [], "reviewed_by": [], "test_runs": 0, ... } Plumbing contract ----------------- - Exit 0: commit found and printed. - Exit 1: commit not found, ambiguous prefix, or invalid commit ID format. Agent use --------- Fetch only the fields you need to keep agent context small:: muse plumbing read-commit --fields commit_id,branch,message,committed_at muse plumbing read-commit --fields agent_id,model_id,format_version """ from __future__ import annotations import argparse import json import logging import sys from muse.core.errors import ExitCode from muse.core.repo import require_repo from muse.core.store import CommitDict, find_commits_by_prefix, read_commit from muse.core.validation import sanitize_display, validate_object_id logger = logging.getLogger(__name__) _FORMAT_CHOICES = ("json", "text") # All fields exposed by CommitRecord.to_dict() — used to validate --fields input. _ALL_FIELDS: frozenset[str] = frozenset(CommitDict.__annotations__.keys()) def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the read-commit subcommand.""" parser = subparsers.add_parser( "read-commit", help="Emit full commit metadata as JSON.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "commit_id", help="Full or abbreviated SHA-256 commit ID.", ) parser.add_argument( "--format", "-f", dest="fmt", default="json", metavar="FORMAT", help="Output format: json (default) or text.", ) parser.add_argument( "--json", action="store_const", const="json", dest="fmt", help="Shorthand for --format json.", ) parser.add_argument( "--fields", default=None, metavar="FIELD,…", dest="fields", help=( "Comma-separated list of CommitDict fields to include in JSON output. " "Reduces response size for agent pipelines. " "Example: --fields commit_id,branch,message,committed_at" ), ) parser.set_defaults(func=run) def run(args: argparse.Namespace) -> None: """Emit full commit metadata as JSON (default) or a compact text summary. Accepts a full 64-character commit ID or a unique prefix. The JSON output schema matches ``CommitRecord.to_dict()`` and is stable across Muse versions (use ``format_version`` to detect schema changes). Use ``--fields`` to request only the fields you need — essential for agents that must keep their context window small. Text format (``--format text``):: """ fmt: str = args.fmt commit_id: str = args.commit_id fields_raw: str | None = args.fields if fmt not in _FORMAT_CHOICES: print( json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"}), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) # Parse and validate --fields before touching the store. requested_fields: frozenset[str] | None = None if fields_raw is not None: if fmt == "text": print( json.dumps({"error": "--fields is only valid with --format json"}), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) parts = {f.strip() for f in fields_raw.split(",") if f.strip()} unknown = parts - _ALL_FIELDS if unknown: print( json.dumps({ "error": f"Unknown field(s): {', '.join(sorted(unknown))}. " f"Valid fields: {', '.join(sorted(_ALL_FIELDS))}", }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) requested_fields = frozenset(parts) root = require_repo() record = None if len(commit_id) == 64: try: validate_object_id(commit_id) except ValueError as exc: print(json.dumps({"error": f"Invalid commit ID: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) record = read_commit(root, commit_id) else: matches = find_commits_by_prefix(root, commit_id) if len(matches) == 1: record = matches[0] elif len(matches) > 1: print( json.dumps({ "error": "ambiguous prefix", "candidates": [m.commit_id for m in matches], }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) if record is None: print(json.dumps({"error": f"Commit not found: {commit_id}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) if fmt == "text": msg = sanitize_display((record.message or "").replace("\n", " ")) print( f"{record.commit_id[:12]} {sanitize_display(record.branch)} " f"{sanitize_display(record.author or '')} " f"{record.committed_at.isoformat()} {msg}" ) return record_data = record.to_dict() if requested_fields is not None: print(json.dumps( {k: v for k, v in record_data.items() if k in requested_fields}, indent=2, default=str, )) else: print(json.dumps(record_data, indent=2, default=str))