gabriel / muse public
show.py python
98 lines 3.4 KB
77f04a8f feat: eliminate all Any/object/ignore — strict TypedDicts at every boundary Gabriel Cardona <gabriel@tellurstori.com> 7d 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
14 logger = logging.getLogger(__name__)
15
16 app = typer.Typer()
17
18
19 def _read_branch(root: pathlib.Path) -> str:
20 head_ref = (root / ".muse" / "HEAD").read_text().strip()
21 return head_ref.removeprefix("refs/heads/").strip()
22
23
24 def _read_repo_id(root: pathlib.Path) -> str:
25 return str(json.loads((root / ".muse" / "repo.json").read_text())["repo_id"])
26
27
28 @app.callback(invoke_without_command=True)
29 def show(
30 ctx: typer.Context,
31 ref: str | None = typer.Argument(None, help="Commit ID or branch (default: HEAD)."),
32 stat: bool = typer.Option(True, "--stat/--no-stat", help="Show file change summary."),
33 json_out: bool = typer.Option(False, "--json", help="Output as JSON."),
34 ) -> None:
35 """Inspect a commit: metadata, diff, and files."""
36 root = require_repo()
37 repo_id = _read_repo_id(root)
38 branch = _read_branch(root)
39
40 commit = resolve_commit_ref(root, repo_id, branch, ref)
41 if commit is None:
42 typer.echo(f"❌ Commit '{ref}' not found.")
43 raise typer.Exit(code=ExitCode.USER_ERROR)
44
45 if json_out:
46 import json as json_mod
47 commit_data = commit.to_dict()
48 if stat:
49 cur = get_commit_snapshot_manifest(root, commit.commit_id) or {}
50 par: dict[str, str] = (
51 get_commit_snapshot_manifest(root, commit.parent_commit_id) or {}
52 if commit.parent_commit_id else {}
53 )
54 stats = {
55 "files_added": sorted(set(cur) - set(par)),
56 "files_removed": sorted(set(par) - set(cur)),
57 "files_modified": sorted(
58 p for p in set(cur) & set(par) if cur[p] != par[p]
59 ),
60 }
61 typer.echo(json_mod.dumps({**commit_data, **stats}, indent=2, default=str))
62 else:
63 typer.echo(json_mod.dumps(commit_data, indent=2, default=str))
64 return
65
66 typer.echo(f"commit {commit.commit_id}")
67 if commit.parent_commit_id:
68 typer.echo(f"Parent: {commit.parent_commit_id[:8]}")
69 if commit.parent2_commit_id:
70 typer.echo(f"Parent: {commit.parent2_commit_id[:8]} (merge)")
71 if commit.author:
72 typer.echo(f"Author: {commit.author}")
73 typer.echo(f"Date: {commit.committed_at}")
74 if commit.metadata:
75 for k, v in sorted(commit.metadata.items()):
76 typer.echo(f" {k}: {v}")
77 typer.echo(f"\n {commit.message}\n")
78
79 if stat:
80 current = get_commit_snapshot_manifest(root, commit.commit_id) or {}
81 parent: dict[str, str] = {}
82 if commit.parent_commit_id:
83 parent = get_commit_snapshot_manifest(root, commit.parent_commit_id) or {}
84
85 added = sorted(set(current) - set(parent))
86 removed = sorted(set(parent) - set(current))
87 modified = sorted(p for p in set(current) & set(parent) if current[p] != parent[p])
88
89 for p in added:
90 typer.echo(f" + {p}")
91 for p in removed:
92 typer.echo(f" - {p}")
93 for p in modified:
94 typer.echo(f" M {p}")
95
96 total = len(added) + len(removed) + len(modified)
97 if total:
98 typer.echo(f"\n {total} file(s) changed")