polyglotminer 1.0.1__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.
- polyglotminer/__init__.py +3 -0
- polyglotminer/cache.py +246 -0
- polyglotminer/campaign.py +1079 -0
- polyglotminer/cli.py +1231 -0
- polyglotminer/cochange.py +648 -0
- polyglotminer/completion.py +264 -0
- polyglotminer/config.py +141 -0
- polyglotminer/db.py +1372 -0
- polyglotminer/doctor.py +281 -0
- polyglotminer/evolution.py +1281 -0
- polyglotminer/languages.py +749 -0
- polyglotminer/media/logo.png +0 -0
- polyglotminer/models.py +440 -0
- polyglotminer/scanner.py +527 -0
- polyglotminer/server.py +708 -0
- polyglotminer/storage.py +318 -0
- polyglotminer/templates/base.html +1232 -0
- polyglotminer/templates/dashboard.html +1094 -0
- polyglotminer/templates/discovery.html +923 -0
- polyglotminer/templates/evolution_dashboard.html +3588 -0
- polyglotminer/templates/evolution_report.html +2572 -0
- polyglotminer/templates/report.html +1409 -0
- polyglotminer/wizard.py +144 -0
- polyglotminer-1.0.1.dist-info/METADATA +262 -0
- polyglotminer-1.0.1.dist-info/RECORD +29 -0
- polyglotminer-1.0.1.dist-info/WHEEL +5 -0
- polyglotminer-1.0.1.dist-info/entry_points.txt +3 -0
- polyglotminer-1.0.1.dist-info/licenses/LICENSE +21 -0
- polyglotminer-1.0.1.dist-info/top_level.txt +1 -0
polyglotminer/cache.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Persistent Repository Cache Pool for rapid Git history operations.
|
|
2
|
+
|
|
3
|
+
Maintains local clones in `.cache/repos/` so repositories are cloned once
|
|
4
|
+
using standard git CLI and updated via sub-second `git fetch`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
from polyglotminer.scanner import (
|
|
15
|
+
format_git_auth_header,
|
|
16
|
+
get_github_auth_token,
|
|
17
|
+
invalidate_github_token,
|
|
18
|
+
is_auth_failure_error,
|
|
19
|
+
resolve_remote_url,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_cache_root() -> Path:
|
|
24
|
+
"""Return the absolute path to workspace repository cache pool (.cache/repos)."""
|
|
25
|
+
base = Path(__file__).resolve().parent.parent / ".cache" / "repos"
|
|
26
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return base
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_cached_repo_dir(remote_target: Union[str, Path]) -> Tuple[Path, str, str]:
|
|
31
|
+
"""Return (local_cache_path, clone_url, repo_slug)."""
|
|
32
|
+
clone_url, default_name = resolve_remote_url(remote_target)
|
|
33
|
+
slug = default_name.replace("/", "_").replace("\\", "_")
|
|
34
|
+
short_name = default_name.split("/")[-1] if "/" in default_name else default_name
|
|
35
|
+
|
|
36
|
+
root = get_cache_root()
|
|
37
|
+
# Prioritize the unique owner-qualified slug to prevent collisions
|
|
38
|
+
if (root / slug / ".git").exists():
|
|
39
|
+
return root / slug, clone_url, default_name
|
|
40
|
+
if (root / short_name / ".git").exists():
|
|
41
|
+
return root / short_name, clone_url, default_name
|
|
42
|
+
|
|
43
|
+
target_dir = root / slug
|
|
44
|
+
return target_dir, clone_url, default_name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_repo_path(
|
|
48
|
+
target: Union[str, Path],
|
|
49
|
+
reports_dir: Optional[Union[str, Path]] = None,
|
|
50
|
+
) -> Optional[Path]:
|
|
51
|
+
"""Resolve a repository target string or Path to an existing local git repository Path if available."""
|
|
52
|
+
if not target:
|
|
53
|
+
return None
|
|
54
|
+
p = Path(target).resolve()
|
|
55
|
+
if p.exists() and (p / ".git").exists():
|
|
56
|
+
return p
|
|
57
|
+
cached_dir, _, _ = get_cached_repo_dir(str(target))
|
|
58
|
+
if cached_dir.exists() and (cached_dir / ".git").exists():
|
|
59
|
+
return cached_dir
|
|
60
|
+
root = get_cache_root()
|
|
61
|
+
slug = str(target).replace("/", "_").replace("\\", "_")
|
|
62
|
+
short_name = str(target).split("/")[-1] if "/" in str(target) else str(target)
|
|
63
|
+
for candidate in (root / slug, root / short_name, root / str(target)):
|
|
64
|
+
if candidate.exists() and (candidate / ".git").exists():
|
|
65
|
+
return candidate
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _force_rmtree(path: Path) -> None:
|
|
70
|
+
"""Force recursive directory removal on Windows handling read-only git files."""
|
|
71
|
+
import stat
|
|
72
|
+
if not path.exists():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
def _handle_remove_readonly(func, p, exc=None):
|
|
76
|
+
try:
|
|
77
|
+
os.chmod(p, stat.S_IWRITE)
|
|
78
|
+
func(p)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if sys.version_info >= (3, 12):
|
|
84
|
+
shutil.rmtree(path, onexc=_handle_remove_readonly)
|
|
85
|
+
else:
|
|
86
|
+
shutil.rmtree(path, onerror=_handle_remove_readonly)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
if path.exists() and sys.platform == "win32":
|
|
91
|
+
try:
|
|
92
|
+
subprocess.run(["cmd", "/c", "rmdir", "/s", "/q", str(path)], capture_output=True, timeout=5)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_or_clone_repository(
|
|
98
|
+
remote_target: Union[str, Path],
|
|
99
|
+
blobless: bool = True,
|
|
100
|
+
fetch: bool = False,
|
|
101
|
+
progress_callback: Optional[callable] = None,
|
|
102
|
+
) -> Path:
|
|
103
|
+
"""Obtain a cached repository Path, cloning if missing or fetching if present."""
|
|
104
|
+
env = dict(os.environ)
|
|
105
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
106
|
+
env["GIT_ASKPASS"] = ""
|
|
107
|
+
|
|
108
|
+
cached_dir, clone_url, repo_slug = get_cached_repo_dir(remote_target)
|
|
109
|
+
|
|
110
|
+
# Clean up any partial/broken directories
|
|
111
|
+
git_dir = cached_dir / ".git"
|
|
112
|
+
if cached_dir.exists():
|
|
113
|
+
if git_dir.exists():
|
|
114
|
+
test_res = subprocess.run(
|
|
115
|
+
["git", "-C", str(cached_dir), "rev-parse", "HEAD"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
env=env,
|
|
118
|
+
timeout=5,
|
|
119
|
+
)
|
|
120
|
+
if test_res.returncode == 0:
|
|
121
|
+
# Valid existing clone
|
|
122
|
+
if not blobless:
|
|
123
|
+
# Check if existing clone is blobless (missing blobs)
|
|
124
|
+
check_filter = subprocess.run(
|
|
125
|
+
["git", "-C", str(cached_dir), "config", "--get", "remote.origin.partialclonefilter"],
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
env=env,
|
|
129
|
+
)
|
|
130
|
+
if "blob:none" in check_filter.stdout:
|
|
131
|
+
# Re-clone cleanly with full objects so cat-file line counting is 100% local and instantaneous
|
|
132
|
+
_force_rmtree(cached_dir)
|
|
133
|
+
|
|
134
|
+
if cached_dir.exists():
|
|
135
|
+
if fetch:
|
|
136
|
+
if progress_callback:
|
|
137
|
+
progress_callback(1, 2, f"Updating cached repository {repo_slug} (git fetch)...")
|
|
138
|
+
try:
|
|
139
|
+
subprocess.run(
|
|
140
|
+
["git", "fetch", "--all"],
|
|
141
|
+
cwd=str(cached_dir),
|
|
142
|
+
env=env,
|
|
143
|
+
stdin=subprocess.DEVNULL,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
check=False,
|
|
146
|
+
timeout=30,
|
|
147
|
+
)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
return cached_dir
|
|
151
|
+
|
|
152
|
+
# Corrupted / invalid directory: remove cleanly
|
|
153
|
+
_force_rmtree(cached_dir)
|
|
154
|
+
|
|
155
|
+
# Repository not in cache: perform robust initial clone via git CLI
|
|
156
|
+
if progress_callback:
|
|
157
|
+
progress_callback(1, 2, f"Cloning {repo_slug} into cache pool...")
|
|
158
|
+
|
|
159
|
+
cached_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
token = get_github_auth_token()
|
|
161
|
+
|
|
162
|
+
def _execute_clone(auth_token: Optional[str]) -> subprocess.CompletedProcess:
|
|
163
|
+
cmd = [
|
|
164
|
+
"git",
|
|
165
|
+
"-c", "credential.helper=",
|
|
166
|
+
"-c", "core.askPass=",
|
|
167
|
+
]
|
|
168
|
+
if auth_token:
|
|
169
|
+
cmd.extend(["-c", f"http.extraHeader={format_git_auth_header(auth_token)}"])
|
|
170
|
+
cmd.extend(["clone", "--no-checkout"])
|
|
171
|
+
|
|
172
|
+
if blobless:
|
|
173
|
+
cmd.append("--filter=blob:none")
|
|
174
|
+
|
|
175
|
+
cmd.extend([
|
|
176
|
+
clone_url,
|
|
177
|
+
str(cached_dir),
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
return subprocess.run(
|
|
181
|
+
cmd,
|
|
182
|
+
env=env,
|
|
183
|
+
stdin=subprocess.DEVNULL,
|
|
184
|
+
capture_output=True,
|
|
185
|
+
text=True,
|
|
186
|
+
timeout=120,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
result = _execute_clone(token)
|
|
190
|
+
|
|
191
|
+
# Detect if clone failed due to authentication issues (e.g. expired/revoked token)
|
|
192
|
+
if result.returncode != 0 and token:
|
|
193
|
+
err_msg = result.stderr or result.stdout or ""
|
|
194
|
+
if is_auth_failure_error(err_msg):
|
|
195
|
+
invalidate_github_token()
|
|
196
|
+
if cached_dir.exists():
|
|
197
|
+
_force_rmtree(cached_dir)
|
|
198
|
+
if progress_callback:
|
|
199
|
+
progress_callback(1, 2, f"GitHub token rejected for {repo_slug}. Retrying unauthenticated clone...")
|
|
200
|
+
result = _execute_clone(None)
|
|
201
|
+
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
if cached_dir.exists():
|
|
204
|
+
_force_rmtree(cached_dir)
|
|
205
|
+
clean_url = clone_url.split("@")[-1] if "@" in clone_url else clone_url
|
|
206
|
+
raise RuntimeError(f"Git clone failed for {clean_url}: {result.stderr or result.stdout}")
|
|
207
|
+
|
|
208
|
+
# Verify that the cloned repository is valid
|
|
209
|
+
test_res = subprocess.run(
|
|
210
|
+
["git", "-C", str(cached_dir), "rev-parse", "HEAD"],
|
|
211
|
+
capture_output=True,
|
|
212
|
+
env=env,
|
|
213
|
+
timeout=5,
|
|
214
|
+
)
|
|
215
|
+
if test_res.returncode != 0:
|
|
216
|
+
_force_rmtree(cached_dir)
|
|
217
|
+
raise RuntimeError(f"Cloned repository {clone_url} has no valid HEAD ref.")
|
|
218
|
+
|
|
219
|
+
return cached_dir
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def clear_repo_cache(target_slug: Optional[Union[str, Path]] = None) -> int:
|
|
223
|
+
"""Clear cached repositories. If target_slug is None, clears all."""
|
|
224
|
+
root = get_cache_root()
|
|
225
|
+
if not root.exists():
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
count = 0
|
|
229
|
+
if target_slug:
|
|
230
|
+
cached_dir, _, _ = get_cached_repo_dir(str(target_slug))
|
|
231
|
+
if cached_dir.exists():
|
|
232
|
+
_force_rmtree(cached_dir)
|
|
233
|
+
count = 1
|
|
234
|
+
else:
|
|
235
|
+
slug = str(target_slug).replace("/", "_").replace("\\", "_")
|
|
236
|
+
short = str(target_slug).split("/")[-1]
|
|
237
|
+
for p in [root / slug, root / short, root / str(target_slug)]:
|
|
238
|
+
if p.exists():
|
|
239
|
+
_force_rmtree(p)
|
|
240
|
+
count += 1
|
|
241
|
+
else:
|
|
242
|
+
for child in root.iterdir():
|
|
243
|
+
if child.is_dir():
|
|
244
|
+
_force_rmtree(child)
|
|
245
|
+
count += 1
|
|
246
|
+
return count
|