diff --git a/src/evalopt_graph/checks.py b/src/evalopt_graph/checks.py index 3083e43..73d1ada 100644 --- a/src/evalopt_graph/checks.py +++ b/src/evalopt_graph/checks.py @@ -13,6 +13,7 @@ from collections.abc import Callable, Iterable from dataclasses import asdict, dataclass from typing import Any +from urllib.parse import urlsplit # gate name -> key in profile["commands"] GATE_TO_CMD_KEY = {"tests": "test", "lint": "lint", "typecheck": "typecheck", "build": "build"} @@ -144,11 +145,36 @@ def is_git_repo(repo_path: str) -> bool: return code == 0 and out.strip() == "true" +_SCP_REMOTE_RE = re.compile(r"^(?:[^@/:\s]+@)?(?P[^/:\s]+):\S+$") + + +def _remote_hostname(remote_url: str) -> str | None: + """Return a normalized hostname for URL and Git scp-style remotes.""" + candidate = remote_url.strip() + if not candidate: + return None + try: + if "://" in candidate: + parsed = urlsplit(candidate) + if parsed.scheme.lower() not in {"git", "http", "https", "ssh"}: + return None + hostname = parsed.hostname + else: + match = _SCP_REMOTE_RE.fullmatch(candidate) + hostname = match.group("host") if match else None + except ValueError: + return None + return hostname.removesuffix(".").lower() if hostname else None + + def is_github_repo(repo_path: str) -> bool: """True if the repo has a github.com remote or a .github directory (best-effort, read-only).""" code, out = _git(repo_path, "remote", "-v") - if code == 0 and "github.com" in out.lower(): - return True + if code == 0: + for line in out.splitlines(): + fields = line.split() + if len(fields) >= 2 and _remote_hostname(fields[1]) == "github.com": + return True return os.path.isdir(os.path.join(repo_path, ".github")) diff --git a/tests/test_quality_gate.py b/tests/test_quality_gate.py index 942009c..31c29dd 100644 --- a/tests/test_quality_gate.py +++ b/tests/test_quality_gate.py @@ -2,6 +2,7 @@ from __future__ import annotations +import subprocess import sys from evalopt_graph import MockProvider, evaluate, passes_quality_gate @@ -10,6 +11,7 @@ all_required_pass, detect_test_weakening, failing_gates, + is_github_repo, run_gates, ) @@ -90,6 +92,61 @@ def test_run_gates_real_subprocess_fail(): assert results[0].exit_code == 3 +def _set_remote(tmp_path, remote_url: str): + repo = tmp_path / "repo" + if not repo.exists(): + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run(["git", "-C", str(repo), "remote", "add", "origin", remote_url], check=True) + else: + subprocess.run(["git", "-C", str(repo), "remote", "set-url", "origin", remote_url], check=True) + return repo + + +def test_is_github_repo_accepts_exact_github_remote_hosts(tmp_path): + repo = _set_remote(tmp_path, "https://github.com/owner/repo.git") + assert is_github_repo(str(repo)) is True + + _set_remote(tmp_path, "git@github.com:owner/repo.git") + assert is_github_repo(str(repo)) is True + + _set_remote(tmp_path, "ssh://git@github.com/owner/repo.git") + assert is_github_repo(str(repo)) is True + + _set_remote(tmp_path, "https://GitHub.com./owner/repo.git") + assert is_github_repo(str(repo)) is True + + +def test_is_github_repo_rejects_github_text_outside_exact_host(tmp_path): + repo = _set_remote(tmp_path, "https://evil.example/github.com/owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "https://github.com.evil.example/owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "https://github.com@evil.example/owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "git@github.com.evil.example:owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "https://github.com.../owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "file:///tmp/github.com/owner/repo.git") + assert is_github_repo(str(repo)) is False + + _set_remote(tmp_path, "file://github.com/owner/repo.git") + assert is_github_repo(str(repo)) is False + + +def test_is_github_repo_preserves_dot_github_fallback(tmp_path): + repo = tmp_path / "not-a-git-repo" + repo.mkdir() + assert is_github_repo(str(repo)) is False + (repo / ".github").mkdir(parents=True) + assert is_github_repo(str(repo)) is True + + # ---------------- anti-gaming: test-weakening detector ----------------