describe.py python
187 lines 6.3 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago
1 """Tag-based commit description for ``muse describe``.
2
3 Walks backward from a commit through its ancestor graph and finds the nearest
4 tag. Returns a human-readable ``<tag>~N`` label where N is the hop count from
5 the tag's commit to the described commit. N=0 means the commit is exactly on
6 the tag.
7
8 The walk is a BFS that visits each ancestor at most once (cycle-safe). It
9 stops as soon as the first matching tag is found, so it is O(commits between
10 tag and HEAD) — not O(all commits).
11
12 If no matching tag is found, ``DescribeResult.tag`` is ``None`` and ``name``
13 falls back to the short SHA.
14
15 Walk budget::
16
17 _MAX_WALK = 50_000 commits. When the budget is exhausted the walk stops
18 and the nearest tag found so far (if any) is returned. The budget check
19 uses ``>=`` so exactly 50 000 commits are visited — not 50 001.
20
21 Tag-selection tie-breaking::
22
23 When multiple tags point to the same commit the lexicographically greatest
24 tag name wins. This is consistent with Git's ``--tags`` behaviour for
25 lightweight tags.
26
27 Pattern matching::
28
29 ``match_pattern`` is a ``fnmatch``-style glob applied to tag names before
30 they are eligible for selection. Only tags whose name matches the pattern
31 are considered. When ``None`` (the default) all tags are eligible.
32 """
33
34 from __future__ import annotations
35
36 import fnmatch
37 import logging
38 import pathlib
39 from collections import deque
40 from typing import TypedDict
41
42 from muse.core.store import get_all_tags, read_commit
43
44
45 type _StrMap = dict[str, str]
46 logger = logging.getLogger(__name__)
47
48 _MAX_WALK = 50_000
49
50
51 class DescribeResult(TypedDict):
52 """Result of describing a commit by its nearest tag.
53
54 Fields:
55 commit_id: Full SHA-256 hex of the described commit.
56 tag: Nearest tag name, or ``None`` if no tag was found.
57 distance: Hop count from the tag's commit to this commit.
58 short_sha: First ``abbrev`` characters of ``commit_id``.
59 name: Human-readable description string.
60 exact: ``True`` when ``distance == 0`` and ``tag`` is not ``None``.
61 """
62
63 commit_id: str
64 tag: str | None
65 distance: int
66 short_sha: str
67 name: str
68 exact: bool
69
70
71 def describe_commit(
72 root: pathlib.Path,
73 repo_id: str,
74 commit_id: str,
75 *,
76 long_format: bool = False,
77 match_pattern: str | None = None,
78 first_parent: bool = False,
79 abbrev: int = 12,
80 exact_match: bool = False,
81 ) -> DescribeResult:
82 """Return a human-readable description of *commit_id*.
83
84 Walks backward from *commit_id* through the parent chain using BFS and
85 finds the nearest tag that satisfies the given constraints. The
86 description is ``<tag>~N`` where N is the number of hops from the tag's
87 commit to *commit_id*.
88
89 When *long_format* is ``True`` the name always includes the distance and
90 short SHA even when N=0, matching Git's ``--long`` behaviour::
91
92 v1.0.0-0-gabc12345 # long: on the tag itself (distance=0)
93 v1.0.0~3-gabc12345 # long: 3 hops past the tag
94
95 When *exact_match* is ``True`` only distance-0 hits are accepted; if no
96 exact tag match exists ``tag`` is ``None`` and the walk returns the short
97 SHA fallback immediately.
98
99 When *first_parent* is ``True`` only the first parent of each merge commit
100 is followed, limiting the walk to the main-line ancestry.
101
102 When *match_pattern* is given, only tag names matching the
103 ``fnmatch``-style glob are considered.
104
105 Args:
106 root: Repository root directory.
107 repo_id: Repository UUID (used to look up tags).
108 commit_id: Starting commit to describe (typically HEAD).
109 long_format: Always include distance and short SHA in the name.
110 match_pattern: ``fnmatch`` glob to filter eligible tag names.
111 first_parent: Follow only the first-parent chain (skip second parents).
112 abbrev: Length of the short SHA suffix (default 12).
113 exact_match: Only accept distance-0 hits; return SHA fallback otherwise.
114
115 Returns:
116 A :class:`DescribeResult` with the nearest tag name, hop count, and
117 formatted description string.
118 """
119 short_sha = commit_id[:abbrev]
120
121 _no_tag = DescribeResult(
122 commit_id=commit_id,
123 tag=None,
124 distance=0,
125 short_sha=short_sha,
126 name=short_sha,
127 exact=False,
128 )
129
130 # Build commit_id → tag_name map. Multiple tags on the same commit:
131 # keep the lexicographically greatest name (stable tie-break).
132 all_tags = get_all_tags(root, repo_id)
133 tag_by_commit: _StrMap = {}
134 for t in all_tags:
135 if match_pattern is not None and not fnmatch.fnmatch(t.tag, match_pattern):
136 continue
137 existing = tag_by_commit.get(t.commit_id)
138 if existing is None or t.tag > existing:
139 tag_by_commit[t.commit_id] = t.tag
140
141 if not tag_by_commit:
142 return _no_tag
143
144 # BFS backward through parent chain.
145 visited: set[str] = set()
146 queue: deque[tuple[str, int]] = deque([(commit_id, 0)])
147
148 while queue:
149 cid, distance = queue.popleft()
150 if cid in visited:
151 continue
152 visited.add(cid)
153
154 if len(visited) >= _MAX_WALK:
155 logger.warning("⚠️ describe: reached %d-commit walk limit", _MAX_WALK)
156 break
157
158 if cid in tag_by_commit:
159 if exact_match and distance != 0:
160 # On exact_match mode, a non-zero distance means the commit
161 # isn't on a tag — stop immediately with SHA fallback.
162 return _no_tag
163 tag_name = tag_by_commit[cid]
164 if long_format:
165 name = f"{tag_name}-{distance}-g{short_sha}"
166 elif distance == 0:
167 name = tag_name
168 else:
169 name = f"{tag_name}~{distance}"
170 return DescribeResult(
171 commit_id=commit_id,
172 tag=tag_name,
173 distance=distance,
174 short_sha=short_sha,
175 name=name,
176 exact=(distance == 0),
177 )
178
179 commit = read_commit(root, cid)
180 if commit is None:
181 continue
182 if commit.parent_commit_id:
183 queue.append((commit.parent_commit_id, distance + 1))
184 if not first_parent and commit.parent2_commit_id:
185 queue.append((commit.parent2_commit_id, distance + 1))
186
187 return _no_tag
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago