gabriel / muse public
read_commit.py python
122 lines 3.6 KB
e8c4265e feat(hardening): final sweep — security, performance, API consistency, docs Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """muse plumbing read-commit — emit full commit metadata as JSON.
2
3 Reads a commit record by its SHA-256 ID and emits the complete JSON
4 representation including provenance fields, CRDT annotations, and the
5 structured delta. Equivalent to ``git cat-file commit`` but producing
6 the Muse JSON schema directly.
7
8 Output::
9
10 {
11 "format_version": 5,
12 "commit_id": "<sha256>",
13 "repo_id": "<uuid>",
14 "branch": "main",
15 "snapshot_id": "<sha256>",
16 "message": "Add verse melody",
17 "committed_at": "2026-03-18T12:00:00+00:00",
18 "parent_commit_id": "<sha256> | null",
19 "parent2_commit_id": null,
20 "author": "gabriel",
21 "agent_id": "",
22 "model_id": "",
23 "sem_ver_bump": "none",
24 "breaking_changes": [],
25 "reviewed_by": [],
26 "test_runs": 0,
27 ...
28 }
29
30 Plumbing contract
31 -----------------
32
33 - Exit 0: commit found and printed.
34 - Exit 1: commit not found, ambiguous prefix, or invalid commit ID format.
35 """
36
37 from __future__ import annotations
38
39 import json
40 import logging
41
42 import typer
43
44 from muse.core.errors import ExitCode
45 from muse.core.repo import require_repo
46 from muse.core.store import find_commits_by_prefix, read_commit
47 from muse.core.validation import validate_object_id
48
49 logger = logging.getLogger(__name__)
50
51 app = typer.Typer()
52
53
54 _FORMAT_CHOICES = ("json", "text")
55
56
57 @app.callback(invoke_without_command=True)
58 def read_commit_cmd(
59 ctx: typer.Context,
60 commit_id: str = typer.Argument(
61 ..., help="Full or abbreviated SHA-256 commit ID."
62 ),
63 fmt: str = typer.Option(
64 "json", "--format", "-f", help="Output format: json (default) or text."
65 ),
66 ) -> None:
67 """Emit full commit metadata as JSON (default) or a compact text summary.
68
69 Accepts a full 64-character commit ID or a unique prefix. The JSON output
70 schema matches ``CommitRecord.to_dict()`` and is stable across Muse
71 versions (use ``format_version`` to detect schema changes).
72
73 Text format (``--format text``)::
74
75 <commit_id> <branch> <author> <committed_at> <message>
76 """
77 if fmt not in _FORMAT_CHOICES:
78 typer.echo(
79 json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"})
80 )
81 raise typer.Exit(code=ExitCode.USER_ERROR)
82 root = require_repo()
83
84 record = None
85
86 if len(commit_id) == 64:
87 try:
88 validate_object_id(commit_id)
89 except ValueError as exc:
90 # JSON to stdout so scripts that parse this command's output can
91 # detect the error without switching to stderr.
92 typer.echo(json.dumps({"error": f"Invalid commit ID: {exc}"}))
93 raise typer.Exit(code=ExitCode.USER_ERROR)
94 record = read_commit(root, commit_id)
95 else:
96 matches = find_commits_by_prefix(root, commit_id)
97 if len(matches) == 1:
98 record = matches[0]
99 elif len(matches) > 1:
100 typer.echo(
101 json.dumps(
102 {
103 "error": "ambiguous prefix",
104 "candidates": [m.commit_id for m in matches],
105 }
106 )
107 )
108 raise typer.Exit(code=ExitCode.USER_ERROR)
109
110 if record is None:
111 typer.echo(json.dumps({"error": f"Commit not found: {commit_id}"}))
112 raise typer.Exit(code=ExitCode.USER_ERROR)
113
114 if fmt == "text":
115 msg = (record.message or "").replace("\n", " ")
116 typer.echo(
117 f"{record.commit_id[:12]} {record.branch} {record.author or ''} "
118 f"{record.committed_at.isoformat()} {msg}"
119 )
120 return
121
122 typer.echo(json.dumps(record.to_dict(), indent=2, default=str))