gabriel / muse public
describe.py python
178 lines 5.5 KB
Raw
sha256:649be9ce77a127cd847c4f798d60104dab7fe830cbc928762fafe3deadf38839 chore: point muse-git-backup.sh's MUSE_COPY at the fresh 'm… Sonnet 5 patch 13 days ago
1 """``muse describe`` — describe a commit relative to the nearest version tag.
2
3 Walks backward from a commit (default: HEAD) through the ancestry graph and
4 finds the nearest semantic version tag.
5
6 Without ``--pre`` only stable version tags are considered. With ``--pre``
7 pre-release tags (rc, beta, alpha) are also eligible.
8
9 When no eligible tag is found the command exits 1 with ``tag: null``,
10 ``distance: null``, and ``description: null`` in JSON output.
11
12 Usage::
13
14 muse describe # describe HEAD (stable tags only)
15 muse describe --pre # include pre-release tags
16 muse describe feat/audio # describe tip of a branch
17 muse describe sha256:<id> # describe a specific commit
18 muse describe --abbrev 12 # 12-char hex suffix in description
19 muse describe --first-parent # follow main-line ancestry only
20 muse describe --json # machine-readable output
21
22 JSON output schema::
23
24 {
25 "commit_id": "<sha256:hex>",
26 "tag": "<nearest version tag>" | null,
27 "distance": <int> | null,
28 "short_sha": "<abbrev hex chars>",
29 "description": "<describe string>" | null,
30 "exact": true | false,
31 "repo_id": "<sha256:...>",
32 "branch": "<current branch>",
33 "duration_ms": <float>,
34 "exit_code": 0 | 1
35 }
36
37 Exit codes::
38
39 0 — tag found and description produced
40 1 — no eligible version tags in the ancestry
41 2 — not a Muse repository
42 """
43
44 import argparse
45 import json
46 import logging
47 import sys
48 from typing import TypedDict
49
50 from muse.core.describe import describe_commit
51 from muse.core.errors import ExitCode
52 from muse.core.repo import read_repo_id, require_repo
53 from muse.core.refs import get_head_commit_id, read_current_branch
54 from muse.core.commits import resolve_commit_ref
55 from muse.core.envelope import EnvelopeJson, make_envelope
56 from muse.core.timing import start_timer
57
58 logger = logging.getLogger(__name__)
59
60
61 class _DescribeJson(EnvelopeJson):
62 """JSON output for ``muse describe --json``."""
63
64 commit_id: str
65 tag: str | None
66 distance: int | None
67 short_sha: str
68 description: str | None
69 exact: bool
70 repo_id: str
71 branch: str
72
73
74 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
75 """Register the ``muse describe`` subcommand."""
76 parser = subparsers.add_parser(
77 "describe",
78 help="Describe a commit relative to the nearest version tag.",
79 description=__doc__,
80 formatter_class=argparse.RawDescriptionHelpFormatter,
81 )
82 parser.add_argument(
83 "ref",
84 nargs="?",
85 default=None,
86 help="Branch name or commit ID to describe (default: HEAD).",
87 )
88 parser.add_argument(
89 "--pre",
90 action="store_true",
91 dest="include_pre",
92 help="Include pre-release version tags (rc, beta, alpha) as reference points.",
93 )
94 parser.add_argument(
95 "--first-parent",
96 action="store_true",
97 dest="first_parent",
98 help="Follow only the first-parent chain (skip merge second-parents).",
99 )
100 parser.add_argument(
101 "--abbrev",
102 type=int,
103 default=8,
104 dest="abbrev",
105 metavar="N",
106 help="Length of the hex suffix in the description string (default: 8).",
107 )
108 parser.add_argument(
109 "--json", "-j",
110 action="store_true",
111 dest="json_out",
112 help="Emit machine-readable JSON on stdout.",
113 )
114 parser.set_defaults(func=run)
115
116
117 def run(args: argparse.Namespace) -> None:
118 """Describe a commit relative to the nearest semantic version tag."""
119 elapsed = start_timer()
120 ref: str | None = args.ref
121 include_pre: bool = args.include_pre
122 first_parent: bool = args.first_parent
123 abbrev: int = args.abbrev
124 json_out: bool = args.json_out
125
126 if abbrev < 4 or abbrev > 64:
127 print(
128 f"❌ --abbrev must be between 4 and 64 (got {abbrev}).",
129 file=sys.stderr,
130 )
131 raise SystemExit(ExitCode.USER_ERROR)
132
133 root = require_repo()
134 repo_id = read_repo_id(root)
135 branch = read_current_branch(root)
136
137 if ref is None:
138 commit_id = get_head_commit_id(root, branch)
139 if commit_id is None:
140 print("❌ No commits on current branch.", file=sys.stderr)
141 raise SystemExit(ExitCode.USER_ERROR)
142 else:
143 commit_rec = resolve_commit_ref(root, branch, ref)
144 if commit_rec is None:
145 print(f"❌ Ref '{ref}' not found.", file=sys.stderr)
146 raise SystemExit(ExitCode.USER_ERROR)
147 commit_id = commit_rec.commit_id
148
149 result = describe_commit(
150 root,
151 repo_id,
152 commit_id,
153 include_pre=include_pre,
154 first_parent=first_parent,
155 abbrev=abbrev,
156 )
157
158 found = result["tag"] is not None
159 exit_code = 0 if found else ExitCode.USER_ERROR
160
161 if json_out:
162 print(json.dumps(_DescribeJson(
163 **make_envelope(elapsed, exit_code=exit_code),
164 commit_id=result["commit_id"],
165 tag=result["tag"],
166 distance=result["distance"],
167 short_sha=result["short_sha"],
168 description=result["description"],
169 exact=result["exact"],
170 repo_id=repo_id,
171 branch=branch,
172 )))
173 raise SystemExit(exit_code)
174 else:
175 if not found:
176 print(f"❌ No version tags found in the ancestry of {commit_id[:abbrev]}.", file=sys.stderr)
177 raise SystemExit(exit_code)
178 print(result["description"])
File History 1 commit
sha256:649be9ce77a127cd847c4f798d60104dab7fe830cbc928762fafe3deadf38839 chore: point muse-git-backup.sh's MUSE_COPY at the fresh 'm… Sonnet 5 patch 13 days ago