"""``muse show`` — inspect a commit: metadata, delta, and provenance. Display the full details of any commit: author, timestamp, semantic-version impact, agent provenance, and a file/symbol change summary. Usage ----- Inspect HEAD:: muse show Inspect a specific commit or branch tip:: muse show Omit the file-change summary:: muse show --no-stat Omit the stored ``structured_delta`` blob from JSON output (smaller payload):: muse show --json --no-delta Exit codes:: 0 — commit found and displayed 1 — commit ref not found or other user error 3 — I/O error """ from __future__ import annotations import argparse import json import logging import pathlib import sys import textwrap from muse.core.errors import ExitCode from muse.core.repo import read_repo_id, require_repo from muse.core.store import ( get_commit_snapshot_manifest, get_head_commit_id, read_commit, read_current_branch, read_snapshot, resolve_commit_ref, ) from muse.core.validation import sanitize_display from muse.domain import DomainOp type _StrMap = dict[str, str] logger = logging.getLogger(__name__) def _format_op(op: DomainOp) -> list[str]: """Return one or more display lines for a single domain op. Each branch checks ``op["op"]`` directly so mypy can narrow the TypedDict union to the specific subtype before accessing its fields. """ if op["op"] == "insert": return [f" A {op['address']}"] if op["op"] == "delete": return [f" D {op['address']}"] if op["op"] == "replace": return [f" M {op['address']}"] if op["op"] == "move": return [f" R {op['address']} ({op['from_position']} → {op['to_position']})"] if op["op"] == "mutate": fields = ", ".join( f"{k}: {v['old']}→{v['new']}" for k, v in op.get("fields", {}).items() ) return [f" ~ {op['address']} ({fields or op.get('old_summary', '')}→{op.get('new_summary', '')})"] # op["op"] == "patch" — the only remaining variant. lines = [f" M {op['address']}"] if op["child_summary"]: lines.append(f" └─ {op['child_summary']}") return lines def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the ``muse show`` subcommand and its flags.""" parser = subparsers.add_parser( "show", help="Inspect a commit: metadata, delta, and provenance.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "ref", nargs="?", default=None, help="Commit ID or branch name (default: HEAD).", ) parser.add_argument( "--no-stat", dest="stat", action="store_false", default=True, help="Omit the file/symbol change summary from output.", ) parser.add_argument( "--no-delta", dest="include_delta", action="store_false", default=True, help=( "Exclude the ``structured_delta`` blob from JSON output. " "Produces a smaller payload for agents that only need commit metadata." ), ) parser.add_argument( "--format", "-f", default="text", dest="fmt", help="Output format: text (default) or json.", ) parser.add_argument( "--json", action="store_const", const="json", dest="fmt", help="Shorthand for --format json.", ) parser.set_defaults(func=run) def run(args: argparse.Namespace) -> None: """Inspect a commit: metadata, delta, and provenance. Agents should pass ``--json`` to receive a machine-readable result:: { "commit_id": "", "branch": "main", "message": "Add verse melody", "author": "gabriel", "agent_id": "", "model_id": "", "toolchain_id": "", "committed_at": "2026-03-21T12:00:00+00:00", "snapshot_id": "", "parent_commit_id": " | null", "parent2_commit_id": null, "sem_ver_bump": "minor", "breaking_changes": [], "metadata": {}, "files_added": ["new_track.mid"], "files_removed": [], "files_modified": ["tracks/bass.mid"], "structured_delta": { ... } } Pass ``--no-stat`` to omit ``files_added/removed/modified``. Pass ``--no-delta`` to omit ``structured_delta`` (smaller payload). """ ref: str | None = args.ref stat: bool = args.stat include_delta: bool = args.include_delta fmt: str = args.fmt if fmt not in ("text", "json"): print( f"❌ Unknown --format '{sanitize_display(fmt)}'. Choose text or json.", file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) root = require_repo() repo_id = read_repo_id(root) branch = read_current_branch(root) # Try branch-name resolution first: ``muse show feat/my-thing`` should # display the HEAD commit of that branch, not attempt a SHA prefix scan. if ref is not None and ref.upper() not in ("HEAD",): branch_head_id = get_head_commit_id(root, ref) if branch_head_id is not None: commit = read_commit(root, branch_head_id) else: commit = resolve_commit_ref(root, repo_id, branch, ref) else: commit = resolve_commit_ref(root, repo_id, branch, ref) if commit is None: print( f"❌ Commit '{sanitize_display(str(ref))}' not found.", file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) if fmt == "json": commit_data = commit.to_dict() if not include_delta: commit_data.pop("structured_delta", None) if stat: # Read current snapshot directly via snapshot_id (avoids re-reading # the commit we already have in memory). cur_snap = read_snapshot(root, commit.snapshot_id) cur = cur_snap.manifest if cur_snap is not None else {} par: _StrMap = {} if commit.parent_commit_id: par_manifest = get_commit_snapshot_manifest(root, commit.parent_commit_id) par = par_manifest if par_manifest is not None else {} stats = { "files_added": sorted(set(cur) - set(par)), "files_removed": sorted(set(par) - set(cur)), "files_modified": sorted( p for p in set(cur) & set(par) if cur[p] != par[p] ), } print(json.dumps({**commit_data, **stats}, indent=2, default=str)) else: print(json.dumps(commit_data, indent=2, default=str)) return # ── Text output ──────────────────────────────────────────────────────────── print(f"commit {commit.commit_id}") if commit.parent_commit_id: print(f"Parent: {commit.parent_commit_id[:8]}") if commit.parent2_commit_id: print(f"Parent: {commit.parent2_commit_id[:8]} (merge)") if commit.author: print(f"Author: {sanitize_display(commit.author)}") # Use ISO 8601 format (with T separator) for consistency with --json output. print(f"Date: {commit.committed_at.isoformat()}") if commit.sem_ver_bump and commit.sem_ver_bump != "none": print(f"SemVer: {commit.sem_ver_bump}") if commit.agent_id: print(f"Agent: {sanitize_display(commit.agent_id)}") if commit.metadata: for k, v in sorted(commit.metadata.items()): print(f" {sanitize_display(k)}: {sanitize_display(str(v))}") # Render the commit message with consistent 4-space indentation for every # line. Previously only the first line was indented; subsequent lines in # multiline messages started at column 0, breaking readability. raw_message = sanitize_display(commit.message) if commit.message else "" indented_message = textwrap.indent(raw_message, " ") if raw_message else "" print(f"\n{indented_message}\n") if not stat: return # Prefer the structured delta stored on the commit. # It carries rich note-level detail and is faster (no blob reloading). if commit.structured_delta is not None: delta = commit.structured_delta if not delta["ops"]: print(" (no changes)") return lines: list[str] = [] for op in delta["ops"]: lines.extend(_format_op(op)) for line in lines: print(line) print(f"\n {delta['summary']}") return # Fallback for initial commits or pre-structured-delta commits: compute # file-level diff from snapshot manifests directly. current = get_commit_snapshot_manifest(root, commit.commit_id) or {} parent: _StrMap = {} if commit.parent_commit_id: parent = get_commit_snapshot_manifest(root, commit.parent_commit_id) or {} added = sorted(set(current) - set(parent)) removed = sorted(set(parent) - set(current)) modified = sorted(p for p in set(current) & set(parent) if current[p] != parent[p]) for p in added: print(f" A {p}") for p in removed: print(f" D {p}") for p in modified: print(f" M {p}") total = len(added) + len(removed) + len(modified) if total: print(f"\n {total} file(s) changed")