cgcardona / muse public
diff.py python
103 lines 3.4 KB
e6786943 feat: upgrade to Python 3.14, drop from __future__ import annotations Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """muse diff — compare working tree against HEAD, or compare two commits."""
2
3 import json
4 import logging
5 import pathlib
6
7 import typer
8
9 from muse.core.errors import ExitCode
10 from muse.core.repo import require_repo
11 from muse.core.store import get_commit_snapshot_manifest, get_head_snapshot_manifest, resolve_commit_ref
12 from muse.domain import DomainOp, SnapshotManifest
13 from muse.plugins.registry import read_domain, resolve_plugin
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 _print_structured_delta(ops: list[DomainOp]) -> int:
30 """Print a structured delta op-by-op. Returns the number of ops printed.
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 for op in ops:
36 if op["op"] == "insert":
37 typer.echo(f"A {op['address']}")
38 elif op["op"] == "delete":
39 typer.echo(f"D {op['address']}")
40 elif op["op"] == "replace":
41 typer.echo(f"M {op['address']}")
42 elif op["op"] == "move":
43 typer.echo(
44 f"R {op['address']} ({op['from_position']} → {op['to_position']})"
45 )
46 elif op["op"] == "patch":
47 typer.echo(f"M {op['address']}")
48 if op["child_summary"]:
49 typer.echo(f" └─ {op['child_summary']}")
50 return len(ops)
51
52
53 @app.callback(invoke_without_command=True)
54 def diff(
55 ctx: typer.Context,
56 commit_a: str | None = typer.Argument(None, help="Base commit ID (default: HEAD)."),
57 commit_b: str | None = typer.Argument(None, help="Target commit ID (default: working tree)."),
58 stat: bool = typer.Option(False, "--stat", help="Show summary statistics only."),
59 ) -> None:
60 """Compare working tree against HEAD, or compare two commits."""
61 root = require_repo()
62 repo_id = _read_repo_id(root)
63 branch = _read_branch(root)
64 domain = read_domain(root)
65 plugin = resolve_plugin(root)
66
67 if commit_a is None:
68 base_snap = SnapshotManifest(
69 files=get_head_snapshot_manifest(root, repo_id, branch) or {},
70 domain=domain,
71 )
72 target_snap = plugin.snapshot(root / "muse-work")
73 elif commit_b is None:
74 base_snap = SnapshotManifest(
75 files=get_head_snapshot_manifest(root, repo_id, branch) or {},
76 domain=domain,
77 )
78 target_snap = SnapshotManifest(
79 files=get_commit_snapshot_manifest(root, commit_a) or {},
80 domain=domain,
81 )
82 else:
83 base_snap = SnapshotManifest(
84 files=get_commit_snapshot_manifest(root, commit_a) or {},
85 domain=domain,
86 )
87 target_snap = SnapshotManifest(
88 files=get_commit_snapshot_manifest(root, commit_b) or {},
89 domain=domain,
90 )
91
92 delta = plugin.diff(base_snap, target_snap, repo_root=root)
93
94 if stat:
95 typer.echo(delta["summary"] if delta["ops"] else "No differences.")
96 return
97
98 changed = _print_structured_delta(delta["ops"])
99
100 if changed == 0:
101 typer.echo("No differences.")
102 else:
103 typer.echo(f"\n{delta['summary']}")