gabriel / musehub public
symbol_anchor.py python
127 lines 3.9 KB
Raw
sha256:c3910cc561368d2b40576c1fbb0841b5d3abefd0a65c96c85114a7238222c77c fix: root-of-push snapshots were never hash-verified before… Sonnet 5 patch 5 days ago
1 """Parse and resolve symbol anchor strings.
2
3 Anchor grammar
4 --------------
5 anchor := cross_anchor | same_anchor
6 cross_anchor := owner '/' repo '::' file_path ('::' symbol)? ('@' ref)?
7 same_anchor := file_path ('::' symbol)? ('@' ref)?
8
9 owner/repo — exactly one '/', no dots in either segment (not a file path)
10 file_path — slash-separated path; distinguished from owner/repo by having
11 a file extension (dot in last segment) or more than one slash
12 symbol — function/class name; may contain dots (e.g. 'GitExporter.fix_file_modes')
13 ref — branch name OR 'sha256:<64hex>'
14
15 Examples
16 --------
17 same-repo:
18 muse/cli/commands/bridge.py::GitExporter.fix_file_modes
19 src/main.py::Fn@dev
20 src/main.py@sha256:fa002...
21
22 cross-repo:
23 gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes
24 gabriel/muse::muse/cli/commands/bridge.py::GitExporter._has_shebang@dev
25 acme/infra::deploy/scripts/install.sh@sha256:fa002...
26 """
27 from __future__ import annotations
28
29 from dataclasses import dataclass
30
31
32 @dataclass(frozen=True)
33 class SymbolAnchorRef:
34 owner: str | None # None → same repo as the issue
35 repo: str | None # None → same repo as the issue
36 file_path: str
37 symbol_name: str | None # None → file-level anchor
38 ref: str | None # None → caller supplies default (usually "main")
39
40 @property
41 def is_cross_repo(self) -> bool:
42 return self.owner is not None
43
44 def blob_url(
45 self,
46 base_url: str,
47 default_owner: str,
48 default_repo: str,
49 default_ref: str = "main",
50 ) -> str:
51 owner = self.owner or default_owner
52 repo = self.repo or default_repo
53 ref = self.ref or default_ref
54 url = f"{base_url}/{owner}/{repo}/blob/{ref}/{self.file_path}"
55 if self.symbol_name:
56 url += f"#S:{self.symbol_name}"
57 return url
58
59 def display_label(self) -> str:
60 parts = []
61 if self.is_cross_repo:
62 parts.append(f"{self.owner}/{self.repo}::")
63 parts.append(self.file_path)
64 if self.symbol_name:
65 parts.append(f"::{self.symbol_name}")
66 if self.ref:
67 parts.append(f"@{self.ref}")
68 return "".join(parts)
69
70
71 def _looks_like_owner_repo(segment: str) -> bool:
72 """Return True iff segment is 'owner/repo' — exactly one slash, no dots in either part."""
73 slash_count = segment.count("/")
74 if slash_count != 1:
75 return False
76 owner, repo = segment.split("/", 1)
77 return (
78 bool(owner) and bool(repo)
79 and "." not in owner
80 and "." not in repo
81 )
82
83
84 def _strip_ref(s: str) -> tuple[str, str | None]:
85 """Split 'name@ref' into (name, ref). Returns (s, None) if no '@'."""
86 at = s.rfind("@")
87 if at == -1:
88 return s, None
89 return s[:at], s[at + 1:] or None
90
91
92 def parse_symbol_anchor(raw: str) -> SymbolAnchorRef:
93 """Parse a symbol anchor string into a SymbolAnchorRef.
94
95 Handles same-repo and cross-repo anchors, with optional ref pins.
96 """
97 parts = raw.split("::")
98
99 owner: str | None = None
100 repo: str | None = None
101 ref: str | None = None
102
103 if len(parts) >= 2 and _looks_like_owner_repo(parts[0]):
104 # Cross-repo: owner/repo :: file (:: symbol)?
105 owner, repo = parts[0].split("/", 1)
106 file_path = parts[1]
107 symbol_raw = parts[2] if len(parts) >= 3 else None
108 else:
109 # Same-repo: file (:: symbol)?
110 file_path = parts[0]
111 symbol_raw = parts[1] if len(parts) >= 2 else None
112
113 # Strip @ref from whichever is the last token.
114 if symbol_raw is not None:
115 symbol_raw, ref = _strip_ref(symbol_raw)
116 symbol_name: str | None = symbol_raw or None
117 else:
118 file_path, ref = _strip_ref(file_path)
119 symbol_name = None
120
121 return SymbolAnchorRef(
122 owner=owner,
123 repo=repo,
124 file_path=file_path,
125 symbol_name=symbol_name,
126 ref=ref,
127 )
File History 1 commit
sha256:c3910cc561368d2b40576c1fbb0841b5d3abefd0a65c96c85114a7238222c77c fix: root-of-push snapshots were never hash-verified before… Sonnet 5 patch 5 days ago