cgcardona / muse public
tag.py python
106 lines 3.3 KB
bda49bdb feat: redesign .museignore as TOML with domain-scoped sections (#100) Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """muse tag — attach and query semantic tags on commits.
2
3 Usage::
4
5 muse tag add emotion:joyful <commit> — tag a commit
6 muse tag list — list all tags in the repo
7 muse tag list <commit> — list tags on a specific commit
8 muse tag remove <tag> <commit> — remove a tag
9
10 Tag conventions::
11
12 emotion:* — emotional character (emotion:melancholic, emotion:tense)
13 section:* — song section (section:verse, section:chorus)
14 stage:* — production stage (stage:rough-mix, stage:master)
15 key:* — musical key (key:Am, key:Eb)
16 tempo:* — tempo annotation (tempo:120bpm)
17 ref:* — reference track (ref:beatles)
18 """
19
20 from __future__ import annotations
21
22 import json
23 import logging
24 import pathlib
25 import uuid
26
27 import typer
28
29 from muse.core.errors import ExitCode
30 from muse.core.repo import require_repo
31 from muse.core.store import (
32 TagRecord,
33 get_all_tags,
34 get_tags_for_commit,
35 resolve_commit_ref,
36 write_tag,
37 )
38
39 logger = logging.getLogger(__name__)
40
41 app = typer.Typer()
42 add_app = typer.Typer()
43 list_app = typer.Typer()
44 remove_app = typer.Typer()
45
46 app.add_typer(add_app, name="add", help="Attach a tag to a commit.")
47 app.add_typer(list_app, name="list", help="List tags.")
48 app.add_typer(remove_app, name="remove", help="Remove a tag from a commit.")
49
50
51 def _read_branch(root: pathlib.Path) -> str:
52 head_ref = (root / ".muse" / "HEAD").read_text().strip()
53 return head_ref.removeprefix("refs/heads/").strip()
54
55
56 def _read_repo_id(root: pathlib.Path) -> str:
57 return str(json.loads((root / ".muse" / "repo.json").read_text())["repo_id"])
58
59
60 @add_app.callback(invoke_without_command=True)
61 def add(
62 ctx: typer.Context,
63 tag_name: str = typer.Argument(..., help="Tag string (e.g. emotion:joyful)."),
64 ref: str | None = typer.Argument(None, help="Commit ID or branch (default: HEAD)."),
65 ) -> None:
66 """Attach a tag to a commit."""
67 root = require_repo()
68 repo_id = _read_repo_id(root)
69 branch = _read_branch(root)
70
71 commit = resolve_commit_ref(root, repo_id, branch, ref)
72 if commit is None:
73 typer.echo(f"❌ Commit '{ref}' not found.")
74 raise typer.Exit(code=ExitCode.USER_ERROR)
75
76 write_tag(root, TagRecord(
77 tag_id=str(uuid.uuid4()),
78 repo_id=repo_id,
79 commit_id=commit.commit_id,
80 tag=tag_name,
81 ))
82 typer.echo(f"Tagged {commit.commit_id[:8]} with '{tag_name}'")
83
84
85 @list_app.callback(invoke_without_command=True)
86 def list_tags(
87 ctx: typer.Context,
88 ref: str | None = typer.Argument(None, help="Commit ID to list tags for (default: all)."),
89 ) -> None:
90 """List tags."""
91 root = require_repo()
92 repo_id = _read_repo_id(root)
93 branch = _read_branch(root)
94
95 if ref:
96 commit = resolve_commit_ref(root, repo_id, branch, ref)
97 if commit is None:
98 typer.echo(f"❌ Commit '{ref}' not found.")
99 raise typer.Exit(code=ExitCode.USER_ERROR)
100 tags = get_tags_for_commit(root, repo_id, commit.commit_id)
101 for t in sorted(tags, key=lambda x: x.tag):
102 typer.echo(f"{t.commit_id[:8]} {t.tag}")
103 else:
104 tags = get_all_tags(root, repo_id)
105 for t in sorted(tags, key=lambda x: (x.tag, x.commit_id)):
106 typer.echo(f"{t.commit_id[:8]} {t.tag}")