gabriel / muse public
describe.py python
162 lines 5.3 KB
Raw
sha256:1a5c0bab2cb0d4c3eed597349ccaa433a4a6add8726890f2d6f577c17dc54972 docs: file follow-up issue for symlog filename-too-long (muse#61) Sonnet 4.6 23 days ago
1 """Tag-based commit description for ``muse describe``.
2
3 Walks backward from a commit through the ancestor graph and finds the nearest
4 semantic version tag. Returns a human-readable label where the format depends
5 on the hop count:
6
7 - Exactly on a tag (distance=0): ``v1.0.0``
8 - N commits past a tag: ``v1.0.0-N-<abbrev hex>`` (no sha256: prefix)
9
10 When no eligible version tag is found (no stable tags, or no tags at all)
11 ``DescribeResult.tag`` is ``None`` and ``distance`` is ``None``.
12
13 Pre-release filtering::
14
15 include_pre=False (default) — only stable version tags are eligible
16 include_pre=True (--pre) — all version tags are eligible
17
18 Walk budget::
19
20 _MAX_WALK = 50_000 commits.
21
22 Tag-selection tie-breaking::
23
24 When multiple tags point to the same commit the one with the highest
25 semver precedence wins (``semver_key`` descending).
26 """
27
28 import logging
29 import pathlib
30 from typing import TypedDict
31
32 from muse.core.types import split_id
33 from muse.core.commits import read_commit
34 from muse.core.version_tags import list_version_tags, VersionTagRecord, semver_key
35
36 logger = logging.getLogger(__name__)
37
38 _MAX_WALK = 50_000
39
40
41 class DescribeResult(TypedDict):
42 """Result of describing a commit by its nearest version tag.
43
44 Fields:
45 commit_id: Full SHA-256 hex of the described commit.
46 tag: Nearest version tag string, or ``None`` if no tag was found.
47 distance: Hop count from the tag's commit to this commit, or ``None``.
48 short_sha: First ``abbrev`` hex characters of the commit ID (no algo prefix).
49 description: Human-readable description string, or ``None`` if no tag found.
50 exact: ``True`` when distance == 0 and tag is not ``None``.
51 """
52
53 commit_id: str
54 tag: str | None
55 distance: int | None
56 short_sha: str
57 description: str | None
58 exact: bool
59
60
61 def describe_commit(
62 root: pathlib.Path,
63 repo_id: str,
64 commit_id: str,
65 *,
66 include_pre: bool = False,
67 first_parent: bool = False,
68 abbrev: int = 8,
69 ) -> DescribeResult:
70 """Return a human-readable description of *commit_id* using version tags.
71
72 Walks backward from *commit_id* through the parent chain using BFS and
73 finds the nearest eligible version tag.
74
75 When *include_pre* is ``False`` (the default) only stable version tags
76 (no pre-release component) are eligible. Set *include_pre=True* to
77 include all version tags.
78
79 Args:
80 root: Repository root directory.
81 repo_id: Repository content ID (used to look up version tags).
82 commit_id: Starting commit to describe (typically HEAD).
83 include_pre: Include pre-release version tags as eligible reference points.
84 first_parent: Follow only the first-parent chain (skip merge second-parents).
85 abbrev: Length of the hex suffix in the description string (default 8).
86
87 Returns:
88 A :class:`DescribeResult` with the nearest tag name, hop count, and
89 formatted description string. When no eligible tag is found,
90 ``tag``, ``distance``, and ``description`` are all ``None``.
91 """
92 algo, hex_str = split_id(commit_id)
93 short_sha = hex_str[:abbrev]
94
95 _no_tag = DescribeResult(
96 commit_id=commit_id,
97 tag=None,
98 distance=None,
99 short_sha=short_sha,
100 description=None,
101 exact=False,
102 )
103
104 # Load all version tags and filter by pre-release preference.
105 all_vtags: list[VersionTagRecord] = list_version_tags(root)
106 if not include_pre:
107 all_vtags = [t for t in all_vtags if not t.semver["pre"]]
108
109 if not all_vtags:
110 return _no_tag
111
112 # Build commit_id → best version tag map. Multiple tags on the same
113 # commit: keep the one with the highest semver precedence.
114 tag_by_commit: dict[str, VersionTagRecord] = {}
115 for vtag in all_vtags:
116 existing = tag_by_commit.get(vtag.commit_id)
117 if existing is None or semver_key(vtag.semver) > semver_key(existing.semver):
118 tag_by_commit[vtag.commit_id] = vtag
119
120 if not tag_by_commit:
121 return _no_tag
122
123 # BFS backward tracking hop distance per node.
124 distances: dict[str, int] = {commit_id: 0}
125
126 def _adjacency(cid: str) -> list[str]:
127 rec = read_commit(root, cid)
128 if rec is None:
129 return []
130 parents = [p for p in (rec.parent_commit_id, rec.parent2_commit_id) if p]
131 if first_parent:
132 parents = parents[:1]
133 dist = distances.get(cid, 0)
134 for p in parents:
135 if p not in distances:
136 distances[p] = dist + 1
137 return parents
138
139 from muse.core.graph import walk_dag
140
141 nodes_yielded = 0
142 for cid in walk_dag(commit_id, _adjacency, max_nodes=_MAX_WALK):
143 nodes_yielded += 1
144 dist = distances.get(cid, 0)
145 if cid in tag_by_commit:
146 vtag = tag_by_commit[cid]
147 if dist == 0:
148 description = vtag.tag
149 else:
150 description = f"{vtag.tag}-{dist}-{short_sha}"
151 return DescribeResult(
152 commit_id=commit_id,
153 tag=vtag.tag,
154 distance=dist,
155 short_sha=short_sha,
156 description=description,
157 exact=(dist == 0),
158 )
159
160 if nodes_yielded >= _MAX_WALK:
161 logger.warning("⚠️ describe: reached %d-commit walk limit", _MAX_WALK)
162 return _no_tag
File History 1 commit
sha256:1a5c0bab2cb0d4c3eed597349ccaa433a4a6add8726890f2d6f577c17dc54972 docs: file follow-up issue for symlog filename-too-long (muse#61) Sonnet 4.6 23 days ago