gabriel / muse public
rev_parse.py python
245 lines 7.9 KB
Raw
sha256:18b983389ee1b55900fcd799bfbb496552d2e3ecded9d18cefbfef188947a12e chore: remove blob-debug test marker file Sonnet 4.6 1 day ago
1 """muse rev-parse — resolve a ref to a full commit ID.
2
3 Resolves a branch name, ``HEAD``, or an abbreviated SHA prefix to the full
4 64-character SHA-256 commit ID.
5
6 Output (JSON, default)::
7
8 {"ref": "main", "commit_id": "<sha256>", "duration_ms": 0.9, "exit_code": 0}
9
10 With ``--abbrev-ref``::
11
12 {"ref": "HEAD", "branch": "main", "duration_ms": 0.4, "exit_code": 0} # JSON
13 main # text
14
15 Output (--format text)::
16
17 <sha256>
18
19 Error output (all error paths, always JSON to stdout)::
20
21 {"ref": "...", "commit_id": null, "error": "not found",
22 "duration_ms": 0.3, "exit_code": 1}
23
24 Output contract
25 ---------------
26
27 - Exit 0: ref resolved successfully.
28 - Exit 1: ref not found, ambiguous, empty, or unknown --format value.
29 - All JSON (success and error) carries ``duration_ms`` and ``exit_code``.
30 - Errors always land on **stdout** as JSON — never on stderr — so agents
31 can parse failures without stderr redirection.
32
33 Agent use
34 ---------
35
36 Canonical "what branch am I on?" query::
37
38 muse rev-parse --abbrev-ref HEAD --json
39 # → {"ref": "HEAD", "branch": "main", "duration_ms": 0.4, "exit_code": 0}
40
41 Canonical "what is HEAD?" query::
42
43 muse rev-parse HEAD --format text
44 # → sha256:<64 hex chars>
45 """
46
47 import argparse
48 import json
49 import logging
50 import re
51 import sys
52
53 from muse.core.types import long_id
54 from muse.core.envelope import EnvelopeJson, make_envelope
55 from muse.core.errors import ExitCode
56 from muse.core.repo import require_repo
57 from muse.core.timing import start_timer
58 from muse.core.refs import (
59 get_head_commit_id,
60 read_current_branch,
61 )
62 from muse.core.commits import (
63 find_commits_by_prefix,
64 read_commit,
65 )
66
67 logger = logging.getLogger(__name__)
68
69 type _ErrorPayload = dict[str, str | int | float | None]
70
71 class _RevParseJson(EnvelopeJson, total=False):
72 """JSON output for normal ref resolution."""
73
74 ref: str
75 commit_id: str | None
76
77 class _RevParseAbbrevJson(EnvelopeJson):
78 """JSON output for --abbrev-ref mode."""
79
80 ref: str
81 branch: str
82
83 _SHA256_FULL_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
84 _SHA256_PREFIX_RE = re.compile(r"^sha256:[0-9a-f]{1,63}$")
85 _BARE_HEX_RE = re.compile(r"^[0-9a-f]+$", re.IGNORECASE)
86
87 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
88 """Register the rev-parse subcommand."""
89 parser = subparsers.add_parser(
90 "rev-parse",
91 help="Resolve branch/HEAD/SHA prefix → full commit_id.",
92 description=__doc__,
93 formatter_class=argparse.RawDescriptionHelpFormatter,
94 )
95 parser.add_argument(
96 "ref",
97 help="Ref to resolve: branch name, 'HEAD', or commit ID prefix.",
98 )
99 parser.add_argument(
100 "--abbrev-ref",
101 action="store_true",
102 dest="abbrev_ref",
103 help=(
104 "Resolve HEAD (or a commit_id) to the branch name rather than "
105 "the commit ID. Canonical agent query: "
106 "`muse rev-parse --abbrev-ref HEAD`."
107 ),
108 )
109 parser.add_argument(
110 "--json", "-j",
111 action="store_true",
112 dest="json_out",
113 help="Emit machine-readable JSON (default: plain text commit_id).",
114 )
115 parser.set_defaults(func=run)
116
117 def run(args: argparse.Namespace) -> None:
118 """Resolve a branch name, HEAD, or SHA prefix to a full commit ID.
119
120 Useful for canonicalising refs in scripts and agent pipelines before
121 passing them to other commands. With ``--abbrev-ref``: return the
122 symbolic branch name — the idiomatic "what branch am I on?" query.
123 Error responses always land on **stdout** as JSON, never on stderr.
124
125 Agent quickstart::
126
127 muse rev-parse HEAD --json
128 muse rev-parse main --json
129 muse rev-parse --abbrev-ref HEAD --json
130 muse rev-parse sha256:abc123 --json
131
132 JSON fields (normal mode)::
133
134 ref str Ref as passed by the caller
135 commit_id str|null Full sha256:<hex> commit ID
136
137 JSON fields (--abbrev-ref mode)::
138
139 ref str Ref as passed by the caller (usually "HEAD")
140 branch str Symbolic branch name
141
142 Exit codes::
143
144 0 Success.
145 1 Ref not found, ambiguous, empty, or unknown --format value.
146 """
147 json_out: bool = args.json_out
148 ref: str = args.ref
149 abbrev_ref: bool = args.abbrev_ref
150
151 elapsed = start_timer()
152
153 def _emit_error(payload: _ErrorPayload, code: int) -> None:
154 """Emit a structured JSON error to stdout and exit.
155
156 Always writes to stdout (never stderr) so agents can parse errors
157 without stderr redirection.
158 """
159 payload.update(make_envelope(elapsed, exit_code=code))
160 print(json.dumps(payload))
161 raise SystemExit(code)
162
163 if not ref:
164 _emit_error(
165 {"ref": ref, "commit_id": None, "error": "ref must not be empty"},
166 ExitCode.USER_ERROR,
167 )
168
169 root = require_repo()
170
171 # ── --abbrev-ref: resolve HEAD → branch name ─────────────────────────────
172 if abbrev_ref:
173 branch = read_current_branch(root)
174 if json_out:
175 print(json.dumps(_RevParseAbbrevJson(**make_envelope(elapsed), ref=ref, branch=branch)))
176 else:
177 print(branch)
178 return
179
180 # ── normal ref resolution ─────────────────────────────────────────────────
181 commit_id: str | None = None
182
183 if ref.upper() == "HEAD":
184 branch = read_current_branch(root)
185 commit_id = get_head_commit_id(root, branch)
186 if commit_id is None:
187 _emit_error(
188 {"ref": ref, "commit_id": None, "error": "HEAD has no commits"},
189 ExitCode.USER_ERROR,
190 )
191 else:
192 # Try as branch name first. validate_branch_name (called inside
193 # get_head_commit_id) raises ValueError for refs containing control
194 # characters, ANSI escapes, or other forbidden sequences — treat these
195 # as "not found" rather than letting the exception escape.
196 try:
197 candidate = get_head_commit_id(root, ref)
198 except ValueError:
199 candidate = None
200 if candidate is not None:
201 commit_id = candidate
202 else:
203 # Try as a canonical content-addressed ID.
204 if _SHA256_FULL_RE.match(ref):
205 record = read_commit(root, ref)
206 if record is not None:
207 commit_id = record.commit_id
208 elif _SHA256_PREFIX_RE.match(ref):
209 bare_prefix = long_id(ref, strip=True)
210 matches = find_commits_by_prefix(root, bare_prefix)
211 if len(matches) == 1:
212 commit_id = matches[0].commit_id
213 elif len(matches) > 1:
214 _emit_error(
215 {
216 "ref": ref,
217 "commit_id": None,
218 "error": "ambiguous",
219 "candidates": [m.commit_id for m in matches],
220 },
221 ExitCode.USER_ERROR,
222 )
223 elif _BARE_HEX_RE.match(ref):
224 _emit_error(
225 {
226 "ref": ref,
227 "commit_id": None,
228 "error": (
229 f"bare hex ID not accepted; "
230 f"use the canonical 'sha256:{ref.lower()}' form"
231 ),
232 },
233 ExitCode.USER_ERROR,
234 )
235
236 if commit_id is None:
237 _emit_error(
238 {"ref": ref, "commit_id": None, "error": "not found"},
239 ExitCode.USER_ERROR,
240 )
241
242 if json_out:
243 print(json.dumps(_RevParseJson(**make_envelope(elapsed), ref=ref, commit_id=commit_id)))
244 else:
245 print(commit_id)
File History 7 commits
sha256:18b983389ee1b55900fcd799bfbb496552d2e3ecded9d18cefbfef188947a12e chore: remove blob-debug test marker file Sonnet 4.6 1 day ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 8 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 10 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 24 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 30 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago