describe.py
python
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