gabriel / muse public

label.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:6 chore(timeline): remove unused RationalRate import in entity.py · · Jul 10, 2026
1 """``muse label`` β€” attach and query label annotations on commits.
2
3 Labels are arbitrary ``namespace:value`` strings (convention, not enforced) that
4 annotate commits with machine-readable metadata. All label operations are
5 idempotent by default: adding the same label twice to the same commit is a
6 no-op (use ``--allow-duplicate`` to bypass).
7
8 Usage::
9
10 muse label add <label> [<ref>] β€” label a commit (HEAD if omitted)
11 muse label add <label> [<ref>] --dry-run β€” preview without writing
12 muse label list [<ref>] β€” list labels (all or per-commit)
13 muse label list --match "emotion:*" β€” filter by glob pattern
14 muse label list --sort created β€” sort by creation time
15 muse label remove <label> [<ref>] β€” remove matching labels from a commit
16
17 Label conventions (not enforced)::
18
19 emotion:* β€” emotional character (emotion:melancholic, emotion:tense)
20 section:* β€” song section (section:verse, section:chorus)
21 stage:* β€” production stage (stage:rough-mix, stage:master)
22 key:* β€” musical key (key:Am, key:Eb)
23 tempo:* β€” tempo annotation (tempo:120bpm)
24 ref:* β€” reference track (ref:beatles)
25
26 JSON output (``--format json`` / ``--json``) schema for ``label add``::
27
28 {
29 "status": "labelled | already_labelled | dry_run",
30 "label_id": "<sha256:...> | null",
31 "commit_id": "<sha256:...>",
32 "label": "<label_string>",
33 "namespace": "<prefix before ':' | null>",
34 "created_at": "<iso8601> | null",
35 "dry_run": false,
36 "duration_ms": 1.234,
37 "exit_code": 0
38 }
39
40 Exit codes::
41
42 0 β€” success
43 1 β€” commit not found, label not found, invalid label name, invalid format
44 2 β€” not inside a Muse repository
45 """
46
47 import argparse
48 import datetime
49 import fnmatch
50 import json
51 import logging
52 import os
53 import re
54 import sys
55 from typing import TypedDict
56
57 from muse.core.types import JsonValue, blob_id
58 from muse.core.errors import ExitCode
59 from muse.core.repo import read_repo_id, require_repo
60 from muse.core.refs import read_current_branch
61 from muse.core.commits import resolve_commit_ref
62 from muse.core.tags import (
63 TagRecord,
64 compute_tag_id,
65 delete_tag,
66 get_all_tags,
67 get_tags_for_commit,
68 write_tag,
69 )
70 from muse.core.envelope import EnvelopeJson, make_envelope
71 from muse.core.validation import sanitize_display
72 from muse.core.timing import start_timer
73
74 logger = logging.getLogger(__name__)
75
76 _MAX_LABEL_LEN: int = 256
77 _LABEL_FORBIDDEN_RE: re.Pattern[str] = re.compile(r"[\x00-\x1f\x7f]")
78
79
80 class _LabelEntryJson(TypedDict):
81 label_id: str
82 commit_id: str
83 label: str
84 namespace: str | None
85 created_at: str
86
87
88 class _LabelAddJson(EnvelopeJson):
89 status: str
90 label_id: str | None
91 commit_id: str
92 label: str
93 namespace: str | None
94 created_at: str | None
95 dry_run: bool
96
97
98 class _LabelListJson(EnvelopeJson):
99 total: int
100 labels: list[_LabelEntryJson]
101
102
103 class _LabelRemoveJson(EnvelopeJson):
104 status: str
105 removed_count: int
106 label_ids: list[str]
107 commit_id: str
108 label: str
109
110
111 def _label_namespace(label: str) -> str | None:
112 return label.split(":", 1)[0] if ":" in label else None
113
114
115 def _validate_label_name(name: str) -> str:
116 if not name:
117 raise ValueError("Label name must not be empty.")
118 if len(name) > _MAX_LABEL_LEN:
119 raise ValueError(
120 f"Label name too long ({len(name)} chars); maximum is {_MAX_LABEL_LEN}."
121 )
122 if _LABEL_FORBIDDEN_RE.search(name):
123 raise ValueError(
124 "Label name contains forbidden control characters "
125 "(use printable ASCII/Unicode only)."
126 )
127 return name
128
129
130 def _label_to_json(t: TagRecord) -> _LabelEntryJson:
131 return _LabelEntryJson(
132 label_id=t.tag_id,
133 commit_id=t.commit_id,
134 label=t.tag,
135 namespace=_label_namespace(t.tag),
136 created_at=t.created_at.isoformat(),
137 )
138
139
140 def _sort_labels(tags: list[TagRecord], sort_key: str) -> list[TagRecord]:
141 if sort_key == "created":
142 return sorted(tags, key=lambda t: t.created_at)
143 if sort_key == "commit":
144 return sorted(tags, key=lambda t: t.commit_id)
145 return sorted(tags, key=lambda t: (t.tag, t.commit_id))
146
147
148 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
149 """Register the ``muse label`` subcommand tree."""
150 parser = subparsers.add_parser(
151 "label",
152 help="Attach and query label annotations on commits.",
153 description=__doc__,
154 formatter_class=argparse.RawDescriptionHelpFormatter,
155 )
156 subs = parser.add_subparsers(dest="subcommand", metavar="SUBCOMMAND")
157 subs.required = True
158
159 # -- add ------------------------------------------------------------------
160 add_p = subs.add_parser(
161 "add",
162 help="Attach a label annotation to a commit.",
163 formatter_class=argparse.RawDescriptionHelpFormatter,
164 )
165 add_p.add_argument("label_name", help="Label string (e.g. emotion:joyful).")
166 add_p.add_argument(
167 "ref", nargs="?", default=None,
168 help="Commit ID or branch name (default: HEAD).",
169 )
170 add_p.add_argument(
171 "--allow-duplicate", action="store_true",
172 help="Allow adding the same label twice to the same commit.",
173 )
174 add_p.add_argument(
175 "-n", "--dry-run", action="store_true",
176 help="Validate and preview without writing any data.",
177 )
178 add_p.add_argument("--json", "-j", action="store_true", dest="json_out",
179 help="Emit machine-readable JSON on stdout.")
180 add_p.set_defaults(func=run_add, json_out=False)
181
182 # -- list -----------------------------------------------------------------
183 list_p = subs.add_parser(
184 "list",
185 help="List label annotations.",
186 formatter_class=argparse.RawDescriptionHelpFormatter,
187 )
188 list_p.add_argument(
189 "ref", nargs="?", default=None,
190 help="Commit ID or branch name to list labels for (default: all commits).",
191 )
192 list_p.add_argument(
193 "--match", "-m", default=None, dest="match",
194 help="Filter by glob pattern (e.g. 'emotion:*', '*:joyful').",
195 )
196 list_p.add_argument(
197 "--sort", choices=["label", "created", "commit"], default="label",
198 help="Sort order: label (default), created, commit.",
199 )
200 list_p.add_argument("--json", "-j", action="store_true", dest="json_out",
201 help="Emit machine-readable JSON on stdout.")
202 list_p.set_defaults(func=run_list, json_out=False)
203
204 # -- remove ---------------------------------------------------------------
205 remove_p = subs.add_parser(
206 "remove",
207 help="Remove a label annotation from a commit.",
208 formatter_class=argparse.RawDescriptionHelpFormatter,
209 )
210 remove_p.add_argument("label_name", help="Label string to remove (e.g. emotion:joyful).")
211 remove_p.add_argument(
212 "ref", nargs="?", default=None,
213 help="Commit ID or branch name (default: HEAD).",
214 )
215 remove_p.add_argument("--json", "-j", action="store_true", dest="json_out",
216 help="Emit machine-readable JSON on stdout.")
217 remove_p.set_defaults(func=run_remove, json_out=False)
218
219
220 def run_add(args: argparse.Namespace) -> None:
221 """Attach a label annotation to a commit."""
222 elapsed = start_timer()
223
224 label_name: str = args.label_name
225 ref: str | None = args.ref
226 json_out: bool = args.json_out
227 dry_run: bool = args.dry_run
228 allow_duplicate: bool = args.allow_duplicate
229
230 def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
231 if json_out:
232 payload = {
233 **make_envelope(elapsed, exit_code=code),
234 "error": error_key,
235 "message": msg,
236 }
237 payload.update(extra)
238 print(json.dumps(payload))
239 else:
240 print(f"❌ {msg}", file=sys.stderr)
241 raise SystemExit(code)
242
243 try:
244 _validate_label_name(label_name)
245 except ValueError as exc:
246 _emit_error(
247 f"Invalid label name: {sanitize_display(str(exc))}",
248 ExitCode.USER_ERROR,
249 "invalid_label_name",
250 )
251
252 root = require_repo()
253 repo_id = read_repo_id(root)
254 branch = read_current_branch(root)
255
256 commit = resolve_commit_ref(root, branch, ref)
257 if commit is None:
258 _emit_error(
259 f"Commit '{sanitize_display(str(ref))}' not found.",
260 ExitCode.USER_ERROR,
261 "commit_not_found",
262 ref=str(ref),
263 )
264
265 namespace = _label_namespace(label_name)
266
267 if dry_run:
268 if json_out:
269 print(json.dumps(_LabelAddJson(
270 **make_envelope(elapsed),
271 status="dry_run",
272 label_id=None,
273 commit_id=commit.commit_id,
274 label=label_name,
275 namespace=namespace,
276 created_at=None,
277 dry_run=True,
278 )))
279 else:
280 print(
281 f"Would label {commit.commit_id} with '{sanitize_display(label_name)}'"
282 " (dry run β€” nothing written)"
283 )
284 return
285
286 if not allow_duplicate:
287 existing = get_tags_for_commit(root, repo_id, commit.commit_id)
288 if any(t.tag == label_name for t in existing):
289 if json_out:
290 existing_tag = next(t for t in existing if t.tag == label_name)
291 print(json.dumps(_LabelAddJson(
292 **make_envelope(elapsed),
293 status="already_labelled",
294 label_id=existing_tag.tag_id,
295 commit_id=commit.commit_id,
296 label=label_name,
297 namespace=namespace,
298 created_at=existing_tag.created_at.isoformat(),
299 dry_run=False,
300 )))
301 else:
302 print(
303 f"Label '{sanitize_display(label_name)}' already on "
304 f"{commit.commit_id} β€” skipped."
305 )
306 return
307
308 if allow_duplicate:
309 label_id = blob_id(os.urandom(32))
310 else:
311 label_id = compute_tag_id(repo_id=repo_id, commit_id=commit.commit_id, tag=label_name)
312 created_at = datetime.datetime.now(datetime.timezone.utc)
313 write_tag(root, TagRecord(
314 tag_id=label_id,
315 repo_id=repo_id,
316 commit_id=commit.commit_id,
317 tag=label_name,
318 created_at=created_at,
319 ))
320
321 if json_out:
322 print(json.dumps(_LabelAddJson(
323 **make_envelope(elapsed),
324 status="labelled",
325 label_id=label_id,
326 commit_id=commit.commit_id,
327 label=label_name,
328 namespace=namespace,
329 created_at=created_at.isoformat(),
330 dry_run=False,
331 )))
332 else:
333 print(f"Labelled {commit.commit_id} with '{sanitize_display(label_name)}'")
334
335
336 def run_list(args: argparse.Namespace) -> None:
337 """List label annotations, optionally filtered by commit or glob pattern."""
338 elapsed = start_timer()
339
340 ref: str | None = args.ref
341 json_out: bool = args.json_out
342 match_pattern: str | None = args.match
343 sort_key: str = args.sort
344
345 def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
346 if json_out:
347 payload = {
348 **make_envelope(elapsed, exit_code=code),
349 "error": error_key,
350 "message": msg,
351 }
352 payload.update(extra)
353 print(json.dumps(payload))
354 else:
355 print(f"❌ {msg}", file=sys.stderr)
356 raise SystemExit(code)
357
358 root = require_repo()
359 repo_id = read_repo_id(root)
360 branch = read_current_branch(root)
361
362 if ref:
363 commit = resolve_commit_ref(root, branch, ref)
364 if commit is None:
365 _emit_error(
366 f"Commit '{sanitize_display(str(ref))}' not found.",
367 ExitCode.USER_ERROR,
368 "commit_not_found",
369 ref=str(ref),
370 )
371 tags = get_tags_for_commit(root, repo_id, commit.commit_id)
372 else:
373 tags = get_all_tags(root, repo_id)
374
375 if match_pattern:
376 tags = [t for t in tags if fnmatch.fnmatch(t.tag, match_pattern)]
377
378 tags = _sort_labels(tags, sort_key)
379
380 if json_out:
381 print(json.dumps(_LabelListJson(
382 **make_envelope(elapsed),
383 total=len(tags),
384 labels=[_label_to_json(t) for t in tags],
385 )))
386 return
387
388 if not tags:
389 print("No labels.")
390 return
391 for t in tags:
392 print(f"{t.commit_id} {sanitize_display(t.tag)}")
393
394
395 def run_remove(args: argparse.Namespace) -> None:
396 """Remove all matching label annotations from a commit."""
397 elapsed = start_timer()
398
399 label_name: str = args.label_name
400 ref: str | None = args.ref
401 json_out: bool = args.json_out
402
403 def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
404 if json_out:
405 payload = {
406 **make_envelope(elapsed, exit_code=code),
407 "error": error_key,
408 "message": msg,
409 }
410 payload.update(extra)
411 print(json.dumps(payload))
412 else:
413 print(f"❌ {msg}", file=sys.stderr)
414 raise SystemExit(code)
415
416 try:
417 _validate_label_name(label_name)
418 except ValueError as exc:
419 _emit_error(
420 f"Invalid label name: {sanitize_display(str(exc))}",
421 ExitCode.USER_ERROR,
422 "invalid_label_name",
423 )
424
425 root = require_repo()
426 repo_id = read_repo_id(root)
427 branch = read_current_branch(root)
428
429 commit = resolve_commit_ref(root, branch, ref)
430 if commit is None:
431 _emit_error(
432 f"Commit '{sanitize_display(str(ref))}' not found.",
433 ExitCode.USER_ERROR,
434 "commit_not_found",
435 ref=str(ref),
436 )
437
438 tags = get_tags_for_commit(root, repo_id, commit.commit_id)
439 matching = [t for t in tags if t.tag == label_name]
440
441 if not matching:
442 if json_out:
443 print(json.dumps(_LabelRemoveJson(
444 **make_envelope(elapsed),
445 status="not_found",
446 removed_count=0,
447 label_ids=[],
448 commit_id=commit.commit_id,
449 label=label_name,
450 )))
451 else:
452 print(
453 f"Label '{sanitize_display(label_name)}' not found on "
454 f"{commit.commit_id} β€” nothing removed."
455 )
456 return
457
458 removed_ids: list[str] = []
459 for t in matching:
460 delete_tag(root, repo_id, t.tag_id)
461 removed_ids.append(t.tag_id)
462
463 if json_out:
464 print(json.dumps(_LabelRemoveJson(
465 **make_envelope(elapsed),
466 status="removed",
467 removed_count=len(matching),
468 label_ids=removed_ids,
469 commit_id=commit.commit_id,
470 label=label_name,
471 )))
472 else:
473 print(
474 f"Removed {len(matching)} label(s) '{sanitize_display(label_name)}' "
475 f"from {commit.commit_id}."
476 )