cgcardona / muse public
show.py python
144 lines 5.1 KB
6d8ca4ac feat: god-tier MIDI dimension expansion + full supercharge architecture Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """muse show — inspect a commit: metadata, diff, and files."""
2 from __future__ import annotations
3
4 import json
5 import logging
6 import pathlib
7
8 import typer
9
10 from muse.core.errors import ExitCode
11 from muse.core.repo import require_repo
12 from muse.core.store import get_commit_snapshot_manifest, read_commit, resolve_commit_ref
13 from muse.domain import DomainOp
14
15 logger = logging.getLogger(__name__)
16
17 app = typer.Typer()
18
19
20 def _read_branch(root: pathlib.Path) -> str:
21 head_ref = (root / ".muse" / "HEAD").read_text().strip()
22 return head_ref.removeprefix("refs/heads/").strip()
23
24
25 def _read_repo_id(root: pathlib.Path) -> str:
26 return str(json.loads((root / ".muse" / "repo.json").read_text())["repo_id"])
27
28
29 def _format_op(op: DomainOp) -> list[str]:
30 """Return one or more display lines for a single domain op.
31
32 Each branch checks ``op["op"]`` directly so mypy can narrow the
33 TypedDict union to the specific subtype before accessing its fields.
34 """
35 if op["op"] == "insert":
36 return [f" A {op['address']}"]
37 if op["op"] == "delete":
38 return [f" D {op['address']}"]
39 if op["op"] == "replace":
40 return [f" M {op['address']}"]
41 if op["op"] == "move":
42 return [f" R {op['address']} ({op['from_position']} → {op['to_position']})"]
43 if op["op"] == "mutate":
44 fields = ", ".join(
45 f"{k}: {v['old']}→{v['new']}" for k, v in op.get("fields", {}).items()
46 )
47 return [f" ~ {op['address']} ({fields or op.get('old_summary', '')}→{op.get('new_summary', '')})"]
48 # op["op"] == "patch" — the only remaining variant.
49 lines = [f" M {op['address']}"]
50 if op["child_summary"]:
51 lines.append(f" └─ {op['child_summary']}")
52 return lines
53
54
55 @app.callback(invoke_without_command=True)
56 def show(
57 ctx: typer.Context,
58 ref: str | None = typer.Argument(None, help="Commit ID or branch (default: HEAD)."),
59 stat: bool = typer.Option(True, "--stat/--no-stat", help="Show file change summary."),
60 json_out: bool = typer.Option(False, "--json", help="Output as JSON."),
61 ) -> None:
62 """Inspect a commit: metadata, diff, and files."""
63 root = require_repo()
64 repo_id = _read_repo_id(root)
65 branch = _read_branch(root)
66
67 commit = resolve_commit_ref(root, repo_id, branch, ref)
68 if commit is None:
69 typer.echo(f"❌ Commit '{ref}' not found.")
70 raise typer.Exit(code=ExitCode.USER_ERROR)
71
72 if json_out:
73 import json as json_mod
74 commit_data = commit.to_dict()
75 if stat:
76 cur = get_commit_snapshot_manifest(root, commit.commit_id) or {}
77 par: dict[str, str] = (
78 get_commit_snapshot_manifest(root, commit.parent_commit_id) or {}
79 if commit.parent_commit_id else {}
80 )
81 stats = {
82 "files_added": sorted(set(cur) - set(par)),
83 "files_removed": sorted(set(par) - set(cur)),
84 "files_modified": sorted(
85 p for p in set(cur) & set(par) if cur[p] != par[p]
86 ),
87 }
88 typer.echo(json_mod.dumps({**commit_data, **stats}, indent=2, default=str))
89 else:
90 typer.echo(json_mod.dumps(commit_data, indent=2, default=str))
91 return
92
93 typer.echo(f"commit {commit.commit_id}")
94 if commit.parent_commit_id:
95 typer.echo(f"Parent: {commit.parent_commit_id[:8]}")
96 if commit.parent2_commit_id:
97 typer.echo(f"Parent: {commit.parent2_commit_id[:8]} (merge)")
98 if commit.author:
99 typer.echo(f"Author: {commit.author}")
100 typer.echo(f"Date: {commit.committed_at}")
101 if commit.metadata:
102 for k, v in sorted(commit.metadata.items()):
103 typer.echo(f" {k}: {v}")
104 typer.echo(f"\n {commit.message}\n")
105
106 if not stat:
107 return
108
109 # Prefer the structured delta stored on the commit.
110 # It carries rich note-level detail and is faster (no blob reloading).
111 if commit.structured_delta is not None:
112 delta = commit.structured_delta
113 if not delta["ops"]:
114 typer.echo(" (no changes)")
115 return
116 lines: list[str] = []
117 for op in delta["ops"]:
118 lines.extend(_format_op(op))
119 for line in lines:
120 typer.echo(line)
121 typer.echo(f"\n {delta['summary']}")
122 return
123
124 # Fallback for initial commits or pre-Phase-1 commits: compute file-level
125 # diff from snapshot manifests directly.
126 current = get_commit_snapshot_manifest(root, commit.commit_id) or {}
127 parent: dict[str, str] = {}
128 if commit.parent_commit_id:
129 parent = get_commit_snapshot_manifest(root, commit.parent_commit_id) or {}
130
131 added = sorted(set(current) - set(parent))
132 removed = sorted(set(parent) - set(current))
133 modified = sorted(p for p in set(current) & set(parent) if current[p] != parent[p])
134
135 for p in added:
136 typer.echo(f" A {p}")
137 for p in removed:
138 typer.echo(f" D {p}")
139 for p in modified:
140 typer.echo(f" M {p}")
141
142 total = len(added) + len(removed) + len(modified)
143 if total:
144 typer.echo(f"\n {total} file(s) changed")