gitinstall 1.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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- gitinstall-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
multi_source.py - 多源代码托管平台支持
|
|
3
|
+
========================================
|
|
4
|
+
|
|
5
|
+
灵感来源:OpenClaw 22+ 渠道集成模式
|
|
6
|
+
|
|
7
|
+
支持从多个代码托管平台安装项目:
|
|
8
|
+
1. GitHub (github.com) — 主力支持
|
|
9
|
+
2. GitLab (gitlab.com) — 完整支持
|
|
10
|
+
3. Bitbucket (bitbucket.org) — 完整支持
|
|
11
|
+
4. Gitee (gitee.com) — 国内镜像
|
|
12
|
+
5. Codeberg (codeberg.org) — 开源替代
|
|
13
|
+
6. 自定义 Git URL — 通用 git clone
|
|
14
|
+
|
|
15
|
+
架构:
|
|
16
|
+
统一的 SourceProvider 接口,每个平台一个实现。
|
|
17
|
+
自动从 URL 识别平台 → 路由到对应 Provider。
|
|
18
|
+
|
|
19
|
+
零外部依赖,纯 Python 标准库。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import urllib.error
|
|
28
|
+
import urllib.parse
|
|
29
|
+
import urllib.request
|
|
30
|
+
from abc import ABC, abstractmethod
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class RepoMetadata:
|
|
37
|
+
"""统一的仓库元数据"""
|
|
38
|
+
platform: str # github, gitlab, bitbucket, gitee, codeberg
|
|
39
|
+
owner: str
|
|
40
|
+
repo: str
|
|
41
|
+
full_name: str # owner/repo
|
|
42
|
+
description: str = ""
|
|
43
|
+
stars: int = 0
|
|
44
|
+
language: str = ""
|
|
45
|
+
clone_url: str = ""
|
|
46
|
+
homepage: str = ""
|
|
47
|
+
license: str = ""
|
|
48
|
+
default_branch: str = "main"
|
|
49
|
+
topics: list[str] = field(default_factory=list)
|
|
50
|
+
is_fork: bool = False
|
|
51
|
+
is_archived: bool = False
|
|
52
|
+
last_push: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SourceProvider(ABC):
|
|
56
|
+
"""代码托管平台统一接口"""
|
|
57
|
+
|
|
58
|
+
platform_name: str = ""
|
|
59
|
+
api_base: str = ""
|
|
60
|
+
raw_base: str = ""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
64
|
+
"""获取仓库元数据"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
68
|
+
"""获取 README 内容"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
72
|
+
"""获取指定文件内容"""
|
|
73
|
+
|
|
74
|
+
def get_clone_url(self, owner: str, repo: str) -> str:
|
|
75
|
+
"""获取 clone URL"""
|
|
76
|
+
return f"https://{self.platform_name}/{owner}/{repo}.git"
|
|
77
|
+
|
|
78
|
+
def _api_get(self, url: str, headers: dict = None) -> dict:
|
|
79
|
+
"""通用 API GET 请求"""
|
|
80
|
+
hdrs = {"User-Agent": "gitinstall/1.0", "Accept": "application/json"}
|
|
81
|
+
if headers:
|
|
82
|
+
hdrs.update(headers)
|
|
83
|
+
req = urllib.request.Request(url, headers=hdrs)
|
|
84
|
+
try:
|
|
85
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
86
|
+
return json.loads(resp.read().decode())
|
|
87
|
+
except urllib.error.HTTPError as e:
|
|
88
|
+
if e.code == 404:
|
|
89
|
+
raise FileNotFoundError(f"仓库不存在: {url}")
|
|
90
|
+
raise ConnectionError(f"API 请求失败 ({e.code}): {url}")
|
|
91
|
+
except urllib.error.URLError as e:
|
|
92
|
+
raise ConnectionError(f"网络错误: {e}")
|
|
93
|
+
|
|
94
|
+
def _raw_get(self, url: str) -> str:
|
|
95
|
+
"""获取原始文本内容"""
|
|
96
|
+
hdrs = {"User-Agent": "gitinstall/1.0"}
|
|
97
|
+
req = urllib.request.Request(url, headers=hdrs)
|
|
98
|
+
try:
|
|
99
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
100
|
+
return resp.read().decode(errors="replace")
|
|
101
|
+
except Exception:
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ─────────────────────────────────────────────
|
|
106
|
+
# GitHub Provider
|
|
107
|
+
# ─────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
class GitHubProvider(SourceProvider):
|
|
110
|
+
platform_name = "github.com"
|
|
111
|
+
api_base = "https://api.github.com"
|
|
112
|
+
raw_base = "https://raw.githubusercontent.com"
|
|
113
|
+
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self._token = os.getenv("GITHUB_TOKEN", "").strip()
|
|
116
|
+
|
|
117
|
+
def _auth_headers(self) -> dict:
|
|
118
|
+
h = {}
|
|
119
|
+
if self._token:
|
|
120
|
+
h["Authorization"] = f"token {self._token}"
|
|
121
|
+
return h
|
|
122
|
+
|
|
123
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
124
|
+
url = f"{self.api_base}/repos/{owner}/{repo}"
|
|
125
|
+
data = self._api_get(url, self._auth_headers())
|
|
126
|
+
return RepoMetadata(
|
|
127
|
+
platform="github",
|
|
128
|
+
owner=data.get("owner", {}).get("login", owner),
|
|
129
|
+
repo=data.get("name", repo),
|
|
130
|
+
full_name=data.get("full_name", f"{owner}/{repo}"),
|
|
131
|
+
description=data.get("description", "") or "",
|
|
132
|
+
stars=data.get("stargazers_count", 0),
|
|
133
|
+
language=data.get("language", "") or "",
|
|
134
|
+
clone_url=data.get("clone_url", self.get_clone_url(owner, repo)),
|
|
135
|
+
homepage=data.get("homepage", "") or "",
|
|
136
|
+
license=(data.get("license") or {}).get("spdx_id", ""),
|
|
137
|
+
default_branch=data.get("default_branch", "main"),
|
|
138
|
+
topics=data.get("topics", []),
|
|
139
|
+
is_fork=data.get("fork", False),
|
|
140
|
+
is_archived=data.get("archived", False),
|
|
141
|
+
last_push=data.get("pushed_at", ""),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
145
|
+
branch = branch or "main"
|
|
146
|
+
for fname in ["README.md", "README.rst", "README.txt", "README", "readme.md"]:
|
|
147
|
+
content = self._raw_get(f"{self.raw_base}/{owner}/{repo}/{branch}/{fname}")
|
|
148
|
+
if content:
|
|
149
|
+
return content[:50000]
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
153
|
+
branch = branch or "main"
|
|
154
|
+
return self._raw_get(f"{self.raw_base}/{owner}/{repo}/{branch}/{path}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─────────────────────────────────────────────
|
|
158
|
+
# GitLab Provider
|
|
159
|
+
# ─────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
class GitLabProvider(SourceProvider):
|
|
162
|
+
platform_name = "gitlab.com"
|
|
163
|
+
api_base = "https://gitlab.com/api/v4"
|
|
164
|
+
|
|
165
|
+
def __init__(self):
|
|
166
|
+
self._token = os.getenv("GITLAB_TOKEN", "").strip()
|
|
167
|
+
|
|
168
|
+
def _auth_headers(self) -> dict:
|
|
169
|
+
h = {}
|
|
170
|
+
if self._token:
|
|
171
|
+
h["PRIVATE-TOKEN"] = self._token
|
|
172
|
+
return h
|
|
173
|
+
|
|
174
|
+
def _project_id(self, owner: str, repo: str) -> str:
|
|
175
|
+
return urllib.parse.quote(f"{owner}/{repo}", safe="")
|
|
176
|
+
|
|
177
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
178
|
+
pid = self._project_id(owner, repo)
|
|
179
|
+
url = f"{self.api_base}/projects/{pid}"
|
|
180
|
+
data = self._api_get(url, self._auth_headers())
|
|
181
|
+
ns = data.get("namespace", {})
|
|
182
|
+
return RepoMetadata(
|
|
183
|
+
platform="gitlab",
|
|
184
|
+
owner=ns.get("path", owner),
|
|
185
|
+
repo=data.get("path", repo),
|
|
186
|
+
full_name=data.get("path_with_namespace", f"{owner}/{repo}"),
|
|
187
|
+
description=data.get("description", "") or "",
|
|
188
|
+
stars=data.get("star_count", 0),
|
|
189
|
+
language="", # GitLab API 需要额外请求获取语言
|
|
190
|
+
clone_url=data.get("http_url_to_repo", self.get_clone_url(owner, repo)),
|
|
191
|
+
homepage=data.get("web_url", ""),
|
|
192
|
+
license="",
|
|
193
|
+
default_branch=data.get("default_branch", "main"),
|
|
194
|
+
topics=data.get("topics", []) or data.get("tag_list", []),
|
|
195
|
+
is_fork=data.get("forked_from_project") is not None,
|
|
196
|
+
is_archived=data.get("archived", False),
|
|
197
|
+
last_push=data.get("last_activity_at", ""),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
201
|
+
pid = self._project_id(owner, repo)
|
|
202
|
+
branch = branch or "main"
|
|
203
|
+
for fname in ["README.md", "README.rst", "README.txt", "README"]:
|
|
204
|
+
encoded = urllib.parse.quote(fname, safe="")
|
|
205
|
+
url = f"{self.api_base}/projects/{pid}/repository/files/{encoded}/raw?ref={branch}"
|
|
206
|
+
content = self._raw_get(url)
|
|
207
|
+
if content:
|
|
208
|
+
return content[:50000]
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
212
|
+
pid = self._project_id(owner, repo)
|
|
213
|
+
branch = branch or "main"
|
|
214
|
+
encoded = urllib.parse.quote(path, safe="")
|
|
215
|
+
url = f"{self.api_base}/projects/{pid}/repository/files/{encoded}/raw?ref={branch}"
|
|
216
|
+
return self._raw_get(url)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ─────────────────────────────────────────────
|
|
220
|
+
# Bitbucket Provider
|
|
221
|
+
# ─────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
class BitbucketProvider(SourceProvider):
|
|
224
|
+
platform_name = "bitbucket.org"
|
|
225
|
+
api_base = "https://api.bitbucket.org/2.0"
|
|
226
|
+
|
|
227
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
228
|
+
url = f"{self.api_base}/repositories/{owner}/{repo}"
|
|
229
|
+
data = self._api_get(url)
|
|
230
|
+
return RepoMetadata(
|
|
231
|
+
platform="bitbucket",
|
|
232
|
+
owner=owner,
|
|
233
|
+
repo=repo,
|
|
234
|
+
full_name=data.get("full_name", f"{owner}/{repo}"),
|
|
235
|
+
description=data.get("description", "") or "",
|
|
236
|
+
stars=0, # Bitbucket 不公开 star 数
|
|
237
|
+
language=data.get("language", "") or "",
|
|
238
|
+
clone_url=f"https://bitbucket.org/{owner}/{repo}.git",
|
|
239
|
+
homepage=data.get("website", "") or "",
|
|
240
|
+
license="",
|
|
241
|
+
default_branch=data.get("mainbranch", {}).get("name", "main"),
|
|
242
|
+
is_fork=data.get("parent") is not None,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
246
|
+
branch = branch or "main"
|
|
247
|
+
for fname in ["README.md", "README.rst", "README.txt", "README"]:
|
|
248
|
+
url = f"https://bitbucket.org/{owner}/{repo}/raw/{branch}/{fname}"
|
|
249
|
+
content = self._raw_get(url)
|
|
250
|
+
if content:
|
|
251
|
+
return content[:50000]
|
|
252
|
+
return ""
|
|
253
|
+
|
|
254
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
255
|
+
branch = branch or "main"
|
|
256
|
+
url = f"https://bitbucket.org/{owner}/{repo}/raw/{branch}/{path}"
|
|
257
|
+
return self._raw_get(url)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ─────────────────────────────────────────────
|
|
261
|
+
# Gitee Provider (国内镜像)
|
|
262
|
+
# ─────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
class GiteeProvider(SourceProvider):
|
|
265
|
+
platform_name = "gitee.com"
|
|
266
|
+
api_base = "https://gitee.com/api/v5"
|
|
267
|
+
|
|
268
|
+
def __init__(self):
|
|
269
|
+
self._token = os.getenv("GITEE_TOKEN", "").strip()
|
|
270
|
+
|
|
271
|
+
def _auth_params(self) -> str:
|
|
272
|
+
if self._token:
|
|
273
|
+
return f"?access_token={self._token}"
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
277
|
+
url = f"{self.api_base}/repos/{owner}/{repo}{self._auth_params()}"
|
|
278
|
+
data = self._api_get(url)
|
|
279
|
+
return RepoMetadata(
|
|
280
|
+
platform="gitee",
|
|
281
|
+
owner=data.get("owner", {}).get("login", owner),
|
|
282
|
+
repo=data.get("path", repo),
|
|
283
|
+
full_name=data.get("full_name", f"{owner}/{repo}"),
|
|
284
|
+
description=data.get("description", "") or "",
|
|
285
|
+
stars=data.get("stargazers_count", 0),
|
|
286
|
+
language=data.get("language", "") or "",
|
|
287
|
+
clone_url=data.get("html_url", "") + ".git" if data.get("html_url") else self.get_clone_url(owner, repo),
|
|
288
|
+
homepage=data.get("homepage", "") or "",
|
|
289
|
+
license=(data.get("license") or ""),
|
|
290
|
+
default_branch=data.get("default_branch", "master"),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
294
|
+
branch = branch or "master"
|
|
295
|
+
for fname in ["README.md", "README.rst", "README.txt", "README"]:
|
|
296
|
+
url = f"https://gitee.com/{owner}/{repo}/raw/{branch}/{fname}"
|
|
297
|
+
content = self._raw_get(url)
|
|
298
|
+
if content:
|
|
299
|
+
return content[:50000]
|
|
300
|
+
return ""
|
|
301
|
+
|
|
302
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
303
|
+
branch = branch or "master"
|
|
304
|
+
url = f"https://gitee.com/{owner}/{repo}/raw/{branch}/{path}"
|
|
305
|
+
return self._raw_get(url)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ─────────────────────────────────────────────
|
|
309
|
+
# Codeberg Provider
|
|
310
|
+
# ─────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
class CodebergProvider(SourceProvider):
|
|
313
|
+
platform_name = "codeberg.org"
|
|
314
|
+
api_base = "https://codeberg.org/api/v1"
|
|
315
|
+
|
|
316
|
+
def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
|
|
317
|
+
url = f"{self.api_base}/repos/{owner}/{repo}"
|
|
318
|
+
data = self._api_get(url)
|
|
319
|
+
return RepoMetadata(
|
|
320
|
+
platform="codeberg",
|
|
321
|
+
owner=data.get("owner", {}).get("login", owner),
|
|
322
|
+
repo=data.get("name", repo),
|
|
323
|
+
full_name=data.get("full_name", f"{owner}/{repo}"),
|
|
324
|
+
description=data.get("description", "") or "",
|
|
325
|
+
stars=data.get("stars_count", 0),
|
|
326
|
+
language=data.get("language", "") or "",
|
|
327
|
+
clone_url=data.get("clone_url", self.get_clone_url(owner, repo)),
|
|
328
|
+
homepage=data.get("website", "") or "",
|
|
329
|
+
default_branch=data.get("default_branch", "main"),
|
|
330
|
+
is_fork=data.get("fork", False),
|
|
331
|
+
is_archived=data.get("archived", False),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
|
|
335
|
+
branch = branch or "main"
|
|
336
|
+
url = f"https://codeberg.org/{owner}/{repo}/raw/branch/{branch}/README.md"
|
|
337
|
+
content = self._raw_get(url)
|
|
338
|
+
if content:
|
|
339
|
+
return content[:50000]
|
|
340
|
+
return ""
|
|
341
|
+
|
|
342
|
+
def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
|
|
343
|
+
branch = branch or "main"
|
|
344
|
+
url = f"https://codeberg.org/{owner}/{repo}/raw/branch/{branch}/{path}"
|
|
345
|
+
return self._raw_get(url)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ─────────────────────────────────────────────
|
|
349
|
+
# 平台自动识别 + 路由
|
|
350
|
+
# ─────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
# 平台 URL 模式 → Provider 映射
|
|
353
|
+
_PLATFORM_PATTERNS = [
|
|
354
|
+
(r'github\.com[:/]([^/]+)/([^/\s\.]+)', "github"),
|
|
355
|
+
(r'gitlab\.com[:/]([^/]+)/([^/\s\.]+)', "gitlab"),
|
|
356
|
+
(r'bitbucket\.org[:/]([^/]+)/([^/\s\.]+)', "bitbucket"),
|
|
357
|
+
(r'gitee\.com[:/]([^/]+)/([^/\s\.]+)', "gitee"),
|
|
358
|
+
(r'codeberg\.org[:/]([^/]+)/([^/\s\.]+)', "codeberg"),
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
_PROVIDERS = {
|
|
362
|
+
"github": GitHubProvider,
|
|
363
|
+
"gitlab": GitLabProvider,
|
|
364
|
+
"bitbucket": BitbucketProvider,
|
|
365
|
+
"gitee": GiteeProvider,
|
|
366
|
+
"codeberg": CodebergProvider,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def detect_platform(identifier: str) -> tuple[str, str, str]:
|
|
371
|
+
"""
|
|
372
|
+
从 URL 或标识符自动检测平台。
|
|
373
|
+
|
|
374
|
+
返回:(platform, owner, repo)
|
|
375
|
+
platform 为 "github", "gitlab", "bitbucket", "gitee", "codeberg" 之一,
|
|
376
|
+
未识别时默认 "github"。
|
|
377
|
+
"""
|
|
378
|
+
identifier = identifier.strip().rstrip("/")
|
|
379
|
+
|
|
380
|
+
for pattern, platform in _PLATFORM_PATTERNS:
|
|
381
|
+
match = re.search(pattern, identifier, re.IGNORECASE)
|
|
382
|
+
if match:
|
|
383
|
+
owner = match.group(1)
|
|
384
|
+
repo = match.group(2).removesuffix(".git")
|
|
385
|
+
return platform, owner, repo
|
|
386
|
+
|
|
387
|
+
# 没有匹配任何平台 URL → 假设是 GitHub 的 owner/repo
|
|
388
|
+
if "/" in identifier and not identifier.startswith("http"):
|
|
389
|
+
parts = identifier.split("/")
|
|
390
|
+
if len(parts) >= 2:
|
|
391
|
+
return "github", parts[0], parts[1]
|
|
392
|
+
|
|
393
|
+
# 仅项目名
|
|
394
|
+
return "github", "", identifier
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_provider(platform: str) -> SourceProvider:
|
|
398
|
+
"""获取对应平台的 Provider 实例"""
|
|
399
|
+
cls = _PROVIDERS.get(platform)
|
|
400
|
+
if not cls:
|
|
401
|
+
raise ValueError(f"不支持的平台: {platform}(支持: {', '.join(_PROVIDERS.keys())})")
|
|
402
|
+
return cls()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def fetch_from_any_source(identifier: str) -> RepoMetadata:
|
|
406
|
+
"""
|
|
407
|
+
从任意源获取仓库元数据。
|
|
408
|
+
|
|
409
|
+
自动检测平台并路由到对应 Provider。
|
|
410
|
+
支持: GitHub, GitLab, Bitbucket, Gitee, Codeberg URL 或 owner/repo 格式。
|
|
411
|
+
"""
|
|
412
|
+
platform, owner, repo = detect_platform(identifier)
|
|
413
|
+
provider = get_provider(platform)
|
|
414
|
+
return provider.get_repo_metadata(owner, repo)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_supported_platforms() -> list[dict]:
|
|
418
|
+
"""返回支持的平台列表"""
|
|
419
|
+
return [
|
|
420
|
+
{"name": "GitHub", "domain": "github.com", "key": "github", "env_token": "GITHUB_TOKEN"},
|
|
421
|
+
{"name": "GitLab", "domain": "gitlab.com", "key": "gitlab", "env_token": "GITLAB_TOKEN"},
|
|
422
|
+
{"name": "Bitbucket", "domain": "bitbucket.org", "key": "bitbucket", "env_token": ""},
|
|
423
|
+
{"name": "Gitee", "domain": "gitee.com", "key": "gitee", "env_token": "GITEE_TOKEN"},
|
|
424
|
+
{"name": "Codeberg", "domain": "codeberg.org", "key": "codeberg", "env_token": ""},
|
|
425
|
+
]
|