Initial curated RepoPulse source from 2524ccc3c1c27739996dccb2b7992989f88cd26d

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:52 +02:00
commit 0adf8ef5d9
90 files changed
+14315

No files matched your search

+264
View File
@@ -0,0 +1,264 @@
from __future__ import annotations
import os
import re
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from threading import Lock, RLock
from urllib.parse import urlsplit
from .models import Repository
SAFE_REVISION = re.compile(r"^[0-9a-fA-F]{7,64}$")
SAFE_BRANCH = re.compile(r"^[A-Za-z0-9._/-]+$")
SAFE_JOB_ID = re.compile(r"^[A-Za-z0-9-]+$")
class AcquisitionError(RuntimeError):
def __init__(self, code: str, detail: str) -> None:
super().__init__(detail)
self.code = code
self.detail = detail
@dataclass(frozen=True)
class AcquiredSource:
path: Path
actual_revision: str
branch: str
class GitAcquirer:
"""Maintain bounded bare mirrors and scan detached exact-revision worktrees."""
def __init__(
self,
cache_root: str | Path | None = None,
token: str | None = None,
*,
timeout: int = 180,
allowed_clone_origin: str | None = None,
) -> None:
raw_root = cache_root or os.getenv("REPOPULSE_REPOSITORY_CACHE", "data/repos")
self.cache_root = Path(raw_root).expanduser().resolve()
self.token = token if token is not None else os.getenv("REPOPULSE_GITEA_TOKEN", "")
self.timeout = max(10, timeout)
configured_origin = allowed_clone_origin or os.getenv("REPOPULSE_GITEA_URL", "")
self.allowed_clone_origin = self._origin(configured_origin)
self.mirrors_root = self.cache_root / "mirrors"
self.worktrees_root = self.cache_root / "worktrees"
self.mirrors_root.mkdir(parents=True, exist_ok=True)
self.worktrees_root.mkdir(parents=True, exist_ok=True)
self._lock_guard = Lock()
self._repository_locks: dict[str, RLock] = {}
@staticmethod
def _origin(value: str) -> str | None:
if not value:
return None
try:
parsed = urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
return None
port = parsed.port or (443 if parsed.scheme == "https" else 80)
except ValueError:
return None
return f"{parsed.scheme.casefold()}://{parsed.hostname.casefold()}:{port}"
def _repository_lock(self, repository_id: str) -> RLock:
with self._lock_guard:
return self._repository_locks.setdefault(repository_id, RLock())
def _git_env(self) -> dict[str, str]:
env = os.environ.copy()
if self.token:
env.update(
{
"GIT_CONFIG_COUNT": "1",
"GIT_CONFIG_KEY_0": "http.extraHeader",
"GIT_CONFIG_VALUE_0": f"Authorization: token {self.token}",
}
)
return env
def _run(self, args: list[str]) -> str:
try:
result = subprocess.run(
args,
check=True,
capture_output=True,
text=True,
errors="replace",
timeout=self.timeout,
env=self._git_env(),
)
except subprocess.TimeoutExpired as exc:
raise AcquisitionError("timeout", "Git source acquisition timed out") from exc
except (OSError, subprocess.CalledProcessError) as exc:
# Git stderr may contain remote URLs or server-specific details; do
# not persist or expose it because the job API is user-visible.
raise AcquisitionError("git_failed", "Git source acquisition failed") from exc
return result.stdout.strip()
def _mirror_path(self, repository: Repository) -> Path:
identity = f"{repository.provider}-{repository.provider_repository_id}"
safe = re.sub(r"[^A-Za-z0-9._-]", "_", identity)
return self.mirrors_root / f"{safe}.git"
def _worktree_path(self, job_id: str) -> Path:
if not SAFE_JOB_ID.fullmatch(job_id):
raise AcquisitionError("invalid_job", "Scan job identity is invalid")
return self.worktrees_root / job_id
@staticmethod
def _validate_branch(branch: str) -> None:
if (
not SAFE_BRANCH.fullmatch(branch)
or branch.startswith("-")
or ".." in branch
or branch.endswith(("/", "."))
):
raise AcquisitionError("invalid_branch", "Repository default branch is invalid")
def _validate_clone_url(self, clone_url: str | None) -> str:
if not clone_url or "\x00" in clone_url:
raise AcquisitionError("missing_clone_url", "Repository clone URL is unavailable")
try:
parsed = urlsplit(clone_url)
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise AcquisitionError("unsafe_clone_url", "Repository clone URL contains unsafe credentials or metadata")
if parsed.scheme in {"http", "https"}:
origin = self._origin(clone_url)
if self.token and (self.allowed_clone_origin is None or origin != self.allowed_clone_origin):
raise AcquisitionError("unsafe_clone_url", "Repository clone URL is outside the configured Gitea origin")
elif parsed.scheme != "file":
raise AcquisitionError("unsafe_clone_url", "Repository clone URL uses an unsupported scheme")
except ValueError as exc:
raise AcquisitionError("unsafe_clone_url", "Repository clone URL is invalid") from exc
return clone_url
def acquire(
self,
repository: Repository,
job_id: str,
requested_revision: str | None = None,
) -> AcquiredSource:
with self._repository_lock(repository.id):
return self._acquire(repository, job_id, requested_revision)
def _acquire(
self,
repository: Repository,
job_id: str,
requested_revision: str | None = None,
) -> AcquiredSource:
branch = repository.default_branch or "main"
self._validate_branch(branch)
if requested_revision is not None and not SAFE_REVISION.fullmatch(requested_revision):
raise AcquisitionError("invalid_revision", "Requested revision is not a valid commit identifier")
clone_url = self._validate_clone_url(repository.clone_url)
mirror = self._mirror_path(repository)
if mirror.exists() and not self._mirror_is_usable(mirror):
self._remove_mirror_if_owned(mirror)
if not mirror.exists():
self._run(["git", "clone", "--mirror", clone_url, str(mirror)])
else:
self._run(["git", "--git-dir", str(mirror), "remote", "set-url", "origin", clone_url])
self._run(
[
"git",
"--git-dir",
str(mirror),
"fetch",
"--prune",
"--no-tags",
"origin",
f"+refs/heads/{branch}:refs/remotes/origin/{branch}",
]
)
if requested_revision:
try:
actual_revision = self._run(
["git", "--git-dir", str(mirror), "rev-parse", f"{requested_revision}^{{commit}}"]
)
except AcquisitionError as exc:
raise AcquisitionError("revision_unavailable", "Requested commit was not found in the mirror") from exc
if actual_revision.casefold() != requested_revision.casefold() and len(requested_revision) == 40:
raise AcquisitionError("revision_unavailable", "Requested commit was not found in the mirror")
else:
actual_revision = self._run(
[
"git",
"--git-dir",
str(mirror),
"rev-parse",
f"refs/remotes/origin/{branch}^{{commit}}",
]
)
if not SAFE_REVISION.fullmatch(actual_revision):
raise AcquisitionError("invalid_revision", "Git did not return a usable commit identifier")
worktree = self._worktree_path(job_id)
self._remove_worktree_if_owned(mirror, worktree)
try:
self._run(
[
"git",
"--git-dir",
str(mirror),
"worktree",
"add",
"--detach",
str(worktree),
actual_revision,
]
)
except AcquisitionError:
self._remove_worktree_if_owned(mirror, worktree)
raise
return AcquiredSource(path=worktree, actual_revision=actual_revision, branch=branch)
def cleanup(self, source: AcquiredSource, repository: Repository) -> None:
with self._repository_lock(repository.id):
mirror = self._mirror_path(repository)
self._remove_worktree_if_owned(mirror, source.path)
def _mirror_is_usable(self, mirror: Path) -> bool:
try:
return self._run(["git", "--git-dir", str(mirror), "rev-parse", "--is-bare-repository"]) == "true"
except AcquisitionError:
return False
def _remove_mirror_if_owned(self, mirror: Path) -> None:
try:
relative = mirror.resolve().relative_to(self.mirrors_root.resolve())
except (OSError, RuntimeError, ValueError):
raise AcquisitionError("unsafe_mirror", "Repository mirror path escaped the repository cache")
if not relative.parts:
raise AcquisitionError("unsafe_mirror", "Repository mirror path is invalid")
if mirror.is_dir() and not mirror.is_symlink():
shutil.rmtree(mirror)
elif mirror.exists() or mirror.is_symlink():
mirror.unlink()
def _remove_worktree_if_owned(self, mirror: Path, worktree: Path) -> None:
try:
relative = worktree.resolve().relative_to(self.worktrees_root.resolve())
except (OSError, RuntimeError, ValueError):
raise AcquisitionError("unsafe_worktree", "Worktree path escaped the repository cache")
if not relative.parts:
raise AcquisitionError("unsafe_worktree", "Worktree path is invalid")
if worktree.exists():
try:
self._run(["git", "--git-dir", str(mirror), "worktree", "remove", "--force", str(worktree)])
except AcquisitionError:
# A stale registration must not prevent a safe bounded cleanup.
pass
if worktree.exists():
shutil.rmtree(worktree)
try:
self._run(["git", "--git-dir", str(mirror), "worktree", "prune"])
except AcquisitionError:
pass