code-search-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import sqlite3
7
+ import time
8
+ from collections import Counter
9
+ from dataclasses import asdict, dataclass
10
+ from pathlib import Path
11
+
12
+ from .models import Section, SourceFile
13
+ from .query import document_terms
14
+ from .repository import RepositoryReader, SourceSignature
15
+ from .structure import sections_for
16
+
17
+
18
+ SCHEMA_VERSION = "1"
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class IndexSnapshot:
23
+ files: list[SourceFile]
24
+ document_terms: dict[str, Counter[str]]
25
+ sections: dict[str, list[Section]]
26
+
27
+
28
+ @dataclass(slots=True)
29
+ class IndexRefresh:
30
+ index_path: str
31
+ source_files: int
32
+ added: int
33
+ updated: int
34
+ removed: int
35
+ unchanged: int
36
+ elapsed_ms: float
37
+
38
+ def to_dict(self) -> dict[str, str | int | float]:
39
+ return asdict(self)
40
+
41
+
42
+ @dataclass(slots=True)
43
+ class IndexStatus:
44
+ index_path: str
45
+ exists: bool
46
+ fresh: bool
47
+ source_files: int
48
+ added: int
49
+ updated: int
50
+ removed: int
51
+ revision: str | None
52
+
53
+ def to_dict(self) -> dict[str, str | int | bool | None]:
54
+ return asdict(self)
55
+
56
+
57
+ def cache_root() -> Path:
58
+ override = os.environ.get("CODE_SEARCH_CACHE_HOME")
59
+ if override:
60
+ return Path(override).expanduser()
61
+ xdg = os.environ.get("XDG_CACHE_HOME")
62
+ return (Path(xdg).expanduser() if xdg else Path.home() / ".cache") / "code-search"
63
+
64
+
65
+ class PersistentIndex:
66
+ def __init__(self, reader: RepositoryReader):
67
+ self.reader = reader
68
+ identity = f"{reader.root}\0{reader.ref or 'WORKTREE'}".encode()
69
+ self.path = cache_root() / f"{hashlib.sha256(identity).hexdigest()[:24]}.sqlite3"
70
+
71
+ def refresh(self) -> IndexRefresh:
72
+ started = time.perf_counter()
73
+ self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
74
+ if not self.path.exists():
75
+ self.path.touch(mode=0o600)
76
+ with self._connect() as connection:
77
+ self._initialize(connection)
78
+ if self.reader.ref:
79
+ counts = self._refresh_revision(connection)
80
+ else:
81
+ counts = self._refresh_worktree(connection)
82
+ source_files = int(connection.execute("SELECT COUNT(*) FROM files").fetchone()[0])
83
+ return IndexRefresh(
84
+ str(self.path),
85
+ source_files,
86
+ counts["added"],
87
+ counts["updated"],
88
+ counts["removed"],
89
+ counts["unchanged"],
90
+ round((time.perf_counter() - started) * 1000, 2),
91
+ )
92
+
93
+ def load(self) -> IndexSnapshot:
94
+ files = []
95
+ terms = {}
96
+ sections = {}
97
+ with self._connect() as connection:
98
+ self._initialize(connection)
99
+ rows = connection.execute(
100
+ "SELECT path, content, terms, sections FROM files ORDER BY path"
101
+ )
102
+ for path, content, raw_terms, raw_sections in rows:
103
+ source = SourceFile(path, content)
104
+ files.append(source)
105
+ terms[path] = Counter(json.loads(raw_terms))
106
+ lines = content.splitlines()
107
+ sections[path] = [
108
+ Section(
109
+ path,
110
+ item["name"],
111
+ item["kind"],
112
+ item["start_line"],
113
+ item["end_line"],
114
+ "\n".join(lines[item["start_line"] - 1 : item["end_line"]]),
115
+ )
116
+ for item in json.loads(raw_sections)
117
+ ]
118
+ return IndexSnapshot(files, terms, sections)
119
+
120
+ def status(self) -> IndexStatus:
121
+ if not self.path.exists():
122
+ return IndexStatus(str(self.path), False, False, 0, 0, 0, 0, self.reader.resolved_revision())
123
+ with self._connect() as connection:
124
+ self._initialize(connection)
125
+ source_files = int(connection.execute("SELECT COUNT(*) FROM files").fetchone()[0])
126
+ if self.reader.ref:
127
+ revision = self.reader.resolved_revision()
128
+ indexed_revision = self._meta(connection, "revision")
129
+ fresh = self._meta(connection, "initialized") == "1" and revision == indexed_revision
130
+ return IndexStatus(
131
+ str(self.path), True, fresh, source_files,
132
+ 0 if fresh else source_files, 0, 0 if fresh else source_files, revision,
133
+ )
134
+ current = {item.path: item for item in self.reader.source_signatures()}
135
+ stored = self._stored_signatures(connection)
136
+ added = len(current.keys() - stored.keys())
137
+ removed = len(stored.keys() - current.keys())
138
+ updated = sum(
139
+ current[path].size != stored[path][0] or current[path].mtime_ns != stored[path][1]
140
+ for path in current.keys() & stored.keys()
141
+ )
142
+ return IndexStatus(
143
+ str(self.path), True, not (added or updated or removed), source_files,
144
+ added, updated, removed, None,
145
+ )
146
+
147
+ def clean(self) -> bool:
148
+ removed = False
149
+ for path in (self.path, Path(f"{self.path}-wal"), Path(f"{self.path}-shm")):
150
+ if path.exists():
151
+ path.unlink()
152
+ removed = True
153
+ return removed
154
+
155
+ def _refresh_worktree(self, connection: sqlite3.Connection) -> dict[str, int]:
156
+ current = {item.path: item for item in self.reader.source_signatures()}
157
+ stored = self._stored_signatures(connection)
158
+ added_paths = current.keys() - stored.keys()
159
+ removed_paths = stored.keys() - current.keys()
160
+ updated_paths = {
161
+ path for path in current.keys() & stored.keys()
162
+ if current[path].size != stored[path][0] or current[path].mtime_ns != stored[path][1]
163
+ }
164
+ for path in removed_paths:
165
+ connection.execute("DELETE FROM files WHERE path = ?", (path,))
166
+ indexed_added = 0
167
+ indexed_updated = 0
168
+ for path in sorted(added_paths | updated_paths):
169
+ try:
170
+ source = self.reader.read_file(path)
171
+ except OSError:
172
+ connection.execute("DELETE FROM files WHERE path = ?", (path,))
173
+ continue
174
+ if "\x00" in source.content:
175
+ connection.execute("DELETE FROM files WHERE path = ?", (path,))
176
+ continue
177
+ signature = current[path]
178
+ self._store_source(connection, source, signature)
179
+ if path in added_paths:
180
+ indexed_added += 1
181
+ else:
182
+ indexed_updated += 1
183
+ self._set_meta(connection, "revision", "")
184
+ self._set_meta(connection, "initialized", "1")
185
+ return {
186
+ "added": indexed_added,
187
+ "updated": indexed_updated,
188
+ "removed": len(removed_paths),
189
+ "unchanged": len(current) - len(added_paths) - len(updated_paths),
190
+ }
191
+
192
+ def _refresh_revision(self, connection: sqlite3.Connection) -> dict[str, int]:
193
+ revision = self.reader.resolved_revision()
194
+ previous = self._meta(connection, "revision")
195
+ initialized = self._meta(connection, "initialized") == "1"
196
+ previous_count = int(connection.execute("SELECT COUNT(*) FROM files").fetchone()[0])
197
+ if initialized and revision == previous:
198
+ return {"added": 0, "updated": 0, "removed": 0, "unchanged": previous_count}
199
+ connection.execute("DELETE FROM files")
200
+ added = 0
201
+ for source in self.reader.iter_source_files():
202
+ self._store_source(connection, source, SourceSignature(source.path, len(source.content), 0))
203
+ added += 1
204
+ self._set_meta(connection, "revision", revision or "")
205
+ self._set_meta(connection, "initialized", "1")
206
+ return {"added": added, "updated": 0, "removed": previous_count, "unchanged": 0}
207
+
208
+ def _store_source(
209
+ self,
210
+ connection: sqlite3.Connection,
211
+ source: SourceFile,
212
+ signature: SourceSignature,
213
+ ) -> None:
214
+ section_metadata = [
215
+ {
216
+ "name": item.name,
217
+ "kind": item.kind,
218
+ "start_line": item.start_line,
219
+ "end_line": item.end_line,
220
+ }
221
+ for item in sections_for(source)
222
+ ]
223
+ connection.execute(
224
+ """
225
+ INSERT INTO files(path, size, mtime_ns, content, terms, sections)
226
+ VALUES (?, ?, ?, ?, ?, ?)
227
+ ON CONFLICT(path) DO UPDATE SET
228
+ size=excluded.size,
229
+ mtime_ns=excluded.mtime_ns,
230
+ content=excluded.content,
231
+ terms=excluded.terms,
232
+ sections=excluded.sections
233
+ """,
234
+ (
235
+ source.path,
236
+ signature.size,
237
+ signature.mtime_ns,
238
+ source.content,
239
+ json.dumps(document_terms(source.content), separators=(",", ":")),
240
+ json.dumps(section_metadata, separators=(",", ":")),
241
+ ),
242
+ )
243
+
244
+ def _stored_signatures(self, connection: sqlite3.Connection) -> dict[str, tuple[int, int]]:
245
+ return {
246
+ path: (int(size), int(mtime_ns))
247
+ for path, size, mtime_ns in connection.execute("SELECT path, size, mtime_ns FROM files")
248
+ }
249
+
250
+ def _connect(self) -> sqlite3.Connection:
251
+ return sqlite3.connect(self.path, timeout=30)
252
+
253
+ def _initialize(self, connection: sqlite3.Connection) -> None:
254
+ connection.execute("CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL)")
255
+ version = self._meta(connection, "schema_version")
256
+ if version and version != SCHEMA_VERSION:
257
+ connection.execute("DROP TABLE IF EXISTS files")
258
+ connection.execute("DELETE FROM metadata")
259
+ connection.execute(
260
+ """
261
+ CREATE TABLE IF NOT EXISTS files(
262
+ path TEXT PRIMARY KEY,
263
+ size INTEGER NOT NULL,
264
+ mtime_ns INTEGER NOT NULL,
265
+ content TEXT NOT NULL,
266
+ terms TEXT NOT NULL,
267
+ sections TEXT NOT NULL
268
+ )
269
+ """
270
+ )
271
+ self._set_meta(connection, "schema_version", SCHEMA_VERSION)
272
+ self._set_meta(connection, "repository", str(self.reader.root))
273
+ self._set_meta(connection, "ref", self.reader.ref or "")
274
+
275
+ @staticmethod
276
+ def _meta(connection: sqlite3.Connection, key: str) -> str | None:
277
+ row = connection.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone()
278
+ return str(row[0]) if row else None
279
+
280
+ @staticmethod
281
+ def _set_meta(connection: sqlite3.Connection, key: str, value: str) -> None:
282
+ connection.execute(
283
+ "INSERT INTO metadata(key, value) VALUES (?, ?) "
284
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
285
+ (key, value),
286
+ )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+
9
+ InstallAction = Literal["created", "updated", "unchanged"]
10
+ SKILL_FILES = (Path("SKILL.md"), Path("agents/openai.yaml"))
11
+
12
+
13
+ def codex_skills_root() -> Path:
14
+ codex_home = os.environ.get("CODEX_HOME")
15
+ return (Path(codex_home).expanduser() if codex_home else Path.home() / ".codex") / "skills"
16
+
17
+
18
+ def install_codex_skill(*, force: bool = False) -> tuple[Path, InstallAction]:
19
+ target = codex_skills_root() / "code-search"
20
+ source = files("code_retrieval").joinpath("resources/code-search")
21
+ existed = target.exists()
22
+ changed = False
23
+
24
+ for relative in SKILL_FILES:
25
+ destination = target / relative
26
+ content = source.joinpath(relative.as_posix()).read_text(encoding="utf-8")
27
+ if destination.exists():
28
+ existing = destination.read_text(encoding="utf-8")
29
+ if existing == content:
30
+ continue
31
+ if not force:
32
+ raise FileExistsError(
33
+ f"refusing to overwrite modified skill file: {destination}; rerun with --force"
34
+ )
35
+ destination.parent.mkdir(parents=True, exist_ok=True)
36
+ destination.write_text(content, encoding="utf-8")
37
+ changed = True
38
+
39
+ action: InstallAction = "unchanged" if not changed else ("updated" if existed else "created")
40
+ return target, action
41
+
42
+
43
+ def uninstall_codex_skill(*, force: bool = False) -> tuple[Path, Literal["removed", "not-found"]]:
44
+ target = codex_skills_root() / "code-search"
45
+ if not target.exists():
46
+ return target, "not-found"
47
+
48
+ source = files("code_retrieval").joinpath("resources/code-search")
49
+ for relative in SKILL_FILES:
50
+ destination = target / relative
51
+ if not destination.exists():
52
+ continue
53
+ expected = source.joinpath(relative.as_posix()).read_text(encoding="utf-8")
54
+ if destination.read_text(encoding="utf-8") != expected and not force:
55
+ raise FileExistsError(
56
+ f"refusing to remove modified skill file: {destination}; rerun with --force"
57
+ )
58
+ destination.unlink()
59
+
60
+ agents = target / "agents"
61
+ if agents.exists() and not any(agents.iterdir()):
62
+ agents.rmdir()
63
+ if target.exists() and not any(target.iterdir()):
64
+ target.rmdir()
65
+ return target, "removed"
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class SourceFile:
9
+ path: str
10
+ content: str
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class Section:
15
+ path: str
16
+ name: str
17
+ kind: str
18
+ start_line: int
19
+ end_line: int
20
+ content: str
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class Evidence:
25
+ path: str
26
+ start_line: int
27
+ end_line: int
28
+ kind: str
29
+ name: str
30
+ score: float
31
+ reasons: list[str]
32
+ content: str
33
+ estimated_tokens: int
34
+
35
+ def key(self) -> tuple[str, int, int]:
36
+ return self.path, self.start_line, self.end_line
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class RetrievalResult:
41
+ task: str
42
+ repository: str
43
+ ref: str | None
44
+ used_tokens: int
45
+ query_terms: list[str]
46
+ evidence: list[Evidence]
47
+ stats: dict[str, Any] = field(default_factory=dict)
48
+
49
+ @property
50
+ def files(self) -> list[str]:
51
+ return list(dict.fromkeys(item.path for item in self.evidence))
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ data = asdict(self)
55
+ data["files"] = self.files
56
+ return data
57
+
58
+ def to_markdown(self) -> str:
59
+ ref = self.ref or "working tree"
60
+ lines = [
61
+ "# Retrieval evidence",
62
+ "",
63
+ f"- Task: {self.task}",
64
+ f"- Repository: `{self.repository}`",
65
+ f"- Revision: `{ref}`",
66
+ f"- Files: {len(self.files)}; evidence spans: {len(self.evidence)}; "
67
+ f"estimated payload: {self.used_tokens} tokens",
68
+ "",
69
+ ]
70
+ for index, item in enumerate(self.evidence, 1):
71
+ suffix = _language_for(item.path)
72
+ lines.extend(
73
+ [
74
+ f"## {index}. `{item.path}:{item.start_line}-{item.end_line}`",
75
+ "",
76
+ f"Score `{item.score:.2f}` · {item.kind} `{item.name}` · "
77
+ + "; ".join(item.reasons),
78
+ "",
79
+ f"```{suffix}",
80
+ item.content.rstrip(),
81
+ "```",
82
+ "",
83
+ ]
84
+ )
85
+ return "\n".join(lines)
86
+
87
+
88
+ def _language_for(path: str) -> str:
89
+ suffix = path.rsplit(".", 1)[-1].lower() if "." in path else "text"
90
+ return {"py": "python", "js": "javascript", "ts": "typescript"}.get(suffix, suffix)
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import Counter
5
+
6
+
7
+ WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*|[\u4e00-\u9fff]+")
8
+ CAMEL_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+")
9
+
10
+ STOP_WORDS = {
11
+ "a", "an", "and", "are", "be", "for", "from", "in", "is", "it", "of", "on", "or",
12
+ "so", "that", "the", "this", "to", "with", "not", "fix", "resolve", "detect", "issue", "problem",
13
+ "missing", "missed", "change", "changes", "code", "configuration",
14
+ }
15
+
16
+ # Generic engineering vocabulary, not repository- or competitor-specific rules.
17
+ ALIASES: dict[str, tuple[str, ...]] = {
18
+ "nullpointerexception": ("null", "nullable", "optional"),
19
+ "npe": ("null", "nullable"),
20
+ "scheduled": ("schedule", "scheduler", "cron", "trigger"),
21
+ "schedule": ("scheduler", "cron", "trigger"),
22
+ "configuration": ("config", "properties", "settings"),
23
+ "config": ("configuration", "properties", "settings"),
24
+ "callback": ("listener", "subscriber", "event", "notify", "publish", "handler"),
25
+ "utf8": ("utf", "charset", "encoding", "encode"),
26
+ "md5": ("digest", "checksum", "hash"),
27
+ "dependency": ("depends", "dependson", "bean"),
28
+ "injection": ("autowired", "inject", "bean", "component"),
29
+ "order": ("dependson", "postconstruct", "initialize", "initialization", "init"),
30
+ "startup": ("postconstruct", "initialize", "initialization", "init", "bean"),
31
+ "event": ("notify", "publish", "subscriber", "listener"),
32
+ "events": ("event", "notify", "publish", "subscriber", "listener"),
33
+ }
34
+
35
+
36
+ def split_identifier(value: str) -> list[str]:
37
+ parts: list[str] = []
38
+ for raw in WORD_RE.findall(value.replace("_", " ")):
39
+ if raw.isascii():
40
+ camel = CAMEL_RE.findall(raw)
41
+ parts.extend(piece.lower() for piece in camel if piece)
42
+ joined = raw.lower()
43
+ if joined not in parts:
44
+ parts.append(joined)
45
+ else:
46
+ parts.append(raw)
47
+ return parts
48
+
49
+
50
+ def document_terms(value: str) -> Counter[str]:
51
+ return Counter(split_identifier(value))
52
+
53
+
54
+ def query_terms(task: str) -> tuple[dict[str, float], list[str]]:
55
+ raw = split_identifier(task)
56
+ original = list(dict.fromkeys(term for term in raw if term not in STOP_WORDS and len(term) > 1))
57
+ weighted = {term: 3.0 for term in original}
58
+ for term in raw:
59
+ for alias in ALIASES.get(term, ()):
60
+ weighted.setdefault(alias, 1.0)
61
+ return weighted, original
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ from .models import SourceFile
10
+
11
+
12
+ SOURCE_SUFFIXES = {
13
+ ".c", ".cc", ".cpp", ".cs", ".go", ".h", ".hpp", ".java", ".js", ".jsx", ".kt",
14
+ ".kts", ".php", ".py", ".rb", ".rs", ".scala", ".sh", ".sql", ".swift", ".ts", ".tsx",
15
+ ".vue", ".xml", ".yaml", ".yml", ".toml", ".properties", ".gradle",
16
+ }
17
+ SKIP_DIRS = {
18
+ ".git", ".idea", ".mypy_cache", ".pytest_cache", ".tox", ".venv", "build", "coverage",
19
+ "dist", "node_modules", "target", "vendor", "venv",
20
+ }
21
+ MAX_FILE_BYTES = 1_000_000
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class SourceSignature:
26
+ path: str
27
+ size: int
28
+ mtime_ns: int
29
+
30
+
31
+ class RepositoryReader:
32
+ def __init__(self, root: str | Path, ref: str | None = None):
33
+ self.root = Path(root).expanduser().resolve()
34
+ self.ref = ref
35
+ if not self.root.is_dir():
36
+ raise ValueError(f"repository is not a directory: {self.root}")
37
+ if ref:
38
+ self._verify_ref(ref)
39
+
40
+ def iter_source_files(self) -> Iterator[SourceFile]:
41
+ if self.ref:
42
+ yield from self._iter_git_ref()
43
+ else:
44
+ yield from self._iter_worktree()
45
+
46
+ def source_signatures(self) -> list[SourceSignature]:
47
+ if self.ref:
48
+ raise ValueError("source signatures are only available for a working tree")
49
+ signatures = []
50
+ for path, relative in self._worktree_paths():
51
+ try:
52
+ stat = path.stat()
53
+ except OSError:
54
+ continue
55
+ if stat.st_size <= MAX_FILE_BYTES:
56
+ signatures.append(SourceSignature(relative, stat.st_size, stat.st_mtime_ns))
57
+ return signatures
58
+
59
+ def resolved_revision(self) -> str | None:
60
+ if not self.ref:
61
+ return None
62
+ return subprocess.run(
63
+ ["git", "rev-parse", "--verify", f"{self.ref}^{{commit}}"],
64
+ cwd=self.root,
65
+ capture_output=True,
66
+ text=True,
67
+ check=True,
68
+ ).stdout.strip()
69
+
70
+ def read_file(self, path: str) -> SourceFile:
71
+ if not _is_source(path):
72
+ raise ValueError(f"unsupported source file: {path}")
73
+ if self.ref:
74
+ result = subprocess.run(
75
+ ["git", "show", f"{self.ref}:{path}"], cwd=self.root, capture_output=True, check=True
76
+ )
77
+ return SourceFile(path, _decode(result.stdout))
78
+ resolved = (self.root / path).resolve()
79
+ if not resolved.is_relative_to(self.root):
80
+ raise ValueError("path escapes repository")
81
+ return SourceFile(path, _decode(resolved.read_bytes()))
82
+
83
+ def _iter_worktree(self) -> Iterator[SourceFile]:
84
+ for path, relative in self._worktree_paths():
85
+ try:
86
+ if path.stat().st_size > MAX_FILE_BYTES:
87
+ continue
88
+ data = path.read_bytes()
89
+ except OSError:
90
+ continue
91
+ if b"\x00" not in data:
92
+ yield SourceFile(relative, _decode(data))
93
+
94
+ def _worktree_paths(self) -> Iterator[tuple[Path, str]]:
95
+ for current, dirs, files in os.walk(self.root):
96
+ dirs[:] = sorted(name for name in dirs if name not in SKIP_DIRS)
97
+ for name in sorted(files):
98
+ path = Path(current, name)
99
+ relative = path.relative_to(self.root).as_posix()
100
+ if not _is_source(relative):
101
+ continue
102
+ try:
103
+ if not path.resolve().is_relative_to(self.root):
104
+ continue
105
+ except OSError:
106
+ continue
107
+ yield path, relative
108
+
109
+ def _iter_git_ref(self) -> Iterator[SourceFile]:
110
+ listing = subprocess.run(
111
+ ["git", "ls-tree", "-r", "-z", "--name-only", self.ref],
112
+ cwd=self.root,
113
+ capture_output=True,
114
+ check=True,
115
+ ).stdout
116
+ paths = [path.decode("utf-8", "surrogateescape") for path in listing.split(b"\x00") if path]
117
+ paths = [path for path in paths if _is_source(path) and not _has_skipped_dir(path)]
118
+ process = subprocess.Popen(
119
+ ["git", "cat-file", "--batch"],
120
+ cwd=self.root,
121
+ stdin=subprocess.PIPE,
122
+ stdout=subprocess.PIPE,
123
+ )
124
+ assert process.stdin is not None and process.stdout is not None
125
+ try:
126
+ for path in paths:
127
+ process.stdin.write(f"{self.ref}:{path}\n".encode())
128
+ process.stdin.flush()
129
+ header = process.stdout.readline().decode().strip().split()
130
+ if len(header) != 3 or header[1] != "blob":
131
+ continue
132
+ size = int(header[2])
133
+ data = process.stdout.read(size)
134
+ process.stdout.read(1)
135
+ if size <= MAX_FILE_BYTES and b"\x00" not in data:
136
+ yield SourceFile(path, _decode(data))
137
+ finally:
138
+ process.stdin.close()
139
+ process.stdout.close()
140
+ process.wait(timeout=10)
141
+
142
+ def _verify_ref(self, ref: str) -> None:
143
+ result = subprocess.run(
144
+ ["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
145
+ cwd=self.root,
146
+ capture_output=True,
147
+ )
148
+ if result.returncode:
149
+ raise ValueError(f"unknown git revision: {ref}")
150
+
151
+
152
+ def _is_source(path: str) -> bool:
153
+ return Path(path).suffix.lower() in SOURCE_SUFFIXES
154
+
155
+
156
+ def _has_skipped_dir(path: str) -> bool:
157
+ return any(part in SKIP_DIRS for part in Path(path).parts[:-1])
158
+
159
+
160
+ def _decode(data: bytes) -> str:
161
+ return data.decode("utf-8", errors="replace")