gabriel / muse public
read_commit.py python
194 lines 6.2 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days 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 Agent use
37 ---------
38
39 Fetch only the fields you need to keep agent context small::
40
41 muse plumbing read-commit <id> --fields commit_id,branch,message,committed_at
42 muse plumbing read-commit <id> --fields agent_id,model_id,format_version
43 """
44
45 from __future__ import annotations
46
47 import argparse
48 import json
49 import logging
50 import sys
51 from muse.core.errors import ExitCode
52 from muse.core.repo import require_repo
53 from muse.core.store import CommitDict, find_commits_by_prefix, read_commit
54 from muse.core.validation import sanitize_display, validate_object_id
55
56 logger = logging.getLogger(__name__)
57
58 _FORMAT_CHOICES = ("json", "text")
59
60 # All fields exposed by CommitRecord.to_dict() — used to validate --fields input.
61 _ALL_FIELDS: frozenset[str] = frozenset(CommitDict.__annotations__.keys())
62
63
64 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
65 """Register the read-commit subcommand."""
66 parser = subparsers.add_parser(
67 "read-commit",
68 help="Emit full commit metadata as JSON.",
69 description=__doc__,
70 formatter_class=argparse.RawDescriptionHelpFormatter,
71 )
72 parser.add_argument(
73 "commit_id",
74 help="Full or abbreviated SHA-256 commit ID.",
75 )
76 parser.add_argument(
77 "--format", "-f",
78 dest="fmt",
79 default="json",
80 metavar="FORMAT",
81 help="Output format: json (default) or text.",
82 )
83 parser.add_argument(
84 "--json", action="store_const", const="json", dest="fmt",
85 help="Shorthand for --format json.",
86 )
87 parser.add_argument(
88 "--fields",
89 default=None,
90 metavar="FIELD,…",
91 dest="fields",
92 help=(
93 "Comma-separated list of CommitDict fields to include in JSON output. "
94 "Reduces response size for agent pipelines. "
95 "Example: --fields commit_id,branch,message,committed_at"
96 ),
97 )
98 parser.set_defaults(func=run)
99
100
101 def run(args: argparse.Namespace) -> None:
102 """Emit full commit metadata as JSON (default) or a compact text summary.
103
104 Accepts a full 64-character commit ID or a unique prefix. The JSON output
105 schema matches ``CommitRecord.to_dict()`` and is stable across Muse
106 versions (use ``format_version`` to detect schema changes).
107
108 Use ``--fields`` to request only the fields you need — essential for
109 agents that must keep their context window small.
110
111 Text format (``--format text``)::
112
113 <commit_id[:12]> <branch> <author> <committed_at> <message>
114 """
115 fmt: str = args.fmt
116 commit_id: str = args.commit_id
117 fields_raw: str | None = args.fields
118
119 if fmt not in _FORMAT_CHOICES:
120 print(
121 json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"}),
122 file=sys.stderr,
123 )
124 raise SystemExit(ExitCode.USER_ERROR)
125
126 # Parse and validate --fields before touching the store.
127 requested_fields: frozenset[str] | None = None
128 if fields_raw is not None:
129 if fmt == "text":
130 print(
131 json.dumps({"error": "--fields is only valid with --format json"}),
132 file=sys.stderr,
133 )
134 raise SystemExit(ExitCode.USER_ERROR)
135 parts = {f.strip() for f in fields_raw.split(",") if f.strip()}
136 unknown = parts - _ALL_FIELDS
137 if unknown:
138 print(
139 json.dumps({
140 "error": f"Unknown field(s): {', '.join(sorted(unknown))}. "
141 f"Valid fields: {', '.join(sorted(_ALL_FIELDS))}",
142 }),
143 file=sys.stderr,
144 )
145 raise SystemExit(ExitCode.USER_ERROR)
146 requested_fields = frozenset(parts)
147
148 root = require_repo()
149
150 record = None
151
152 if len(commit_id) == 64:
153 try:
154 validate_object_id(commit_id)
155 except ValueError as exc:
156 print(json.dumps({"error": f"Invalid commit ID: {exc}"}), file=sys.stderr)
157 raise SystemExit(ExitCode.USER_ERROR)
158 record = read_commit(root, commit_id)
159 else:
160 matches = find_commits_by_prefix(root, commit_id)
161 if len(matches) == 1:
162 record = matches[0]
163 elif len(matches) > 1:
164 print(
165 json.dumps({
166 "error": "ambiguous prefix",
167 "candidates": [m.commit_id for m in matches],
168 }),
169 file=sys.stderr,
170 )
171 raise SystemExit(ExitCode.USER_ERROR)
172
173 if record is None:
174 print(json.dumps({"error": f"Commit not found: {commit_id}"}), file=sys.stderr)
175 raise SystemExit(ExitCode.USER_ERROR)
176
177 if fmt == "text":
178 msg = sanitize_display((record.message or "").replace("\n", " "))
179 print(
180 f"{record.commit_id[:12]} {sanitize_display(record.branch)} "
181 f"{sanitize_display(record.author or '')} "
182 f"{record.committed_at.isoformat()} {msg}"
183 )
184 return
185
186 record_data = record.to_dict()
187 if requested_fields is not None:
188 print(json.dumps(
189 {k: v for k, v in record_data.items() if k in requested_fields},
190 indent=2,
191 default=str,
192 ))
193 else:
194 print(json.dumps(record_data, indent=2, default=str))
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago