#!/usr/bin/env python3 import os import subprocess import sys from pathlib import Path ROOT = Path(".").resolve() TEST_DIRS = [ ROOT / "tests", ROOT / "test", ROOT / "qt6-after" / "qt6-migrated-benchmark" / "tests", ] CPP_EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"} ALLOWED_SIGNERS_FILE = ROOT / "ci" / "allowed_signers" TEST_PATTERNS = [ "test_{name}.cpp", "test_{name}.h", "{name}_test.cpp", "{name}_test.h", "{name}test.cpp", "{name}test.h", "{name}gtest.cpp", "{name}gtest.h", ] def run(cmd: str) -> str: return subprocess.check_output(cmd, shell=True, text=True).strip() def git_ref_exists(ref: str) -> bool: try: subprocess.check_call( f"git rev-parse --verify --quiet '{ref}'", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) return True except subprocess.CalledProcessError: return False def is_pull_request_build() -> bool: return bool(os.environ.get("CHANGE_ID")) def resolve_target_ref() -> str | None: raw = os.environ.get("CHANGE_TARGET", "").strip() candidates = [] if raw: candidates.append(raw) if not raw.startswith("origin/"): candidates.append(f"origin/{raw}") candidates.extend(["origin/main", "main"]) seen = set() for ref in candidates: if ref and ref not in seen: seen.add(ref) if git_ref_exists(ref): return ref return None def changed_files_and_base() -> tuple[list[str], str]: target = resolve_target_ref() if target: print(f"Vergleiche gegen Ziel-Ref: {target}") out = run(f"git diff --name-only {target}...HEAD") files = [line.strip() for line in out.splitlines() if line.strip()] return files, target if is_pull_request_build(): print("ERROR: Kein gültiger Ziel-Branch-Ref für PR-Build gefunden.") sys.exit(1) if git_ref_exists("HEAD~1"): print("Kein Ziel-Branch-Ref gefunden, verwende HEAD~1 für Branch-Build.") out = run("git diff --name-only HEAD~1..HEAD") files = [line.strip() for line in out.splitlines() if line.strip()] return files, "HEAD~1" print("Keine Vergleichsbasis gefunden, behandle alle versionierten Dateien als geändert.") out = run("git ls-files") files = [line.strip() for line in out.splitlines() if line.strip()] return files, "" def commits_in_range(base: str) -> list[str]: if base: if base == "HEAD~1": out = run("git rev-list HEAD~1..HEAD") else: out = run(f"git rev-list {base}..HEAD") else: out = run("git rev-list HEAD") return [line.strip() for line in out.splitlines() if line.strip()] def is_cpp_file(path: Path) -> bool: return path.suffix.lower() in CPP_EXTENSIONS def require_clang_format(): if not (ROOT / ".clang-format").exists(): print("ERROR: .clang-format fehlt im Repository-Root.") sys.exit(1) def require_allowed_signers(): if not ALLOWED_SIGNERS_FILE.exists(): print(f"ERROR: {ALLOWED_SIGNERS_FILE} fehlt.") sys.exit(1) def check_format(path: Path): subprocess.check_call(f"clang-format --dry-run --Werror '{path}'", shell=True) def candidate_names(path: Path): stem = path.stem.lower() names = {stem} for suffix in ["_p", "_impl", "_private", "_priv", "_ui"]: if stem.endswith(suffix): names.add(stem[:-len(suffix)]) return sorted(n for n in names if n) def iter_test_files(): for test_dir in TEST_DIRS: if test_dir.exists(): yield from (p for p in test_dir.rglob("*") if p.is_file()) def has_related_test(path: Path) -> bool: test_files = list(iter_test_files()) if not test_files: return False test_names = [p.name.lower() for p in test_files] for name in candidate_names(path): for pattern in TEST_PATTERNS: if pattern.format(name=name).lower() in test_names: return True for test_name in test_names: if name in test_name and ("test" in test_name or "gtest" in test_name): return True return False def verify_commit_signature(commit: str): env = os.environ.copy() env["GIT_CONFIG_COUNT"] = "1" env["GIT_CONFIG_KEY_0"] = "gpg.ssh.allowedSignersFile" env["GIT_CONFIG_VALUE_0"] = str(ALLOWED_SIGNERS_FILE) subprocess.check_call( f"git verify-commit {commit}", shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def main(): require_clang_format() require_allowed_signers() files, base = changed_files_and_base() if not files: print("Keine geänderten Dateien erkannt.") return commits = commits_in_range(base) if not commits: print("Keine neuen Commits im Vergleichsbereich erkannt.") else: for commit in commits: print(f"Prüfe SSH-Signatur: {commit}") try: verify_commit_signature(commit) except subprocess.CalledProcessError: print( "ERROR: Commit ist nicht gültig mit erlaubtem SSH-Signierer signiert: " f"{commit}" ) sys.exit(1) for rel in files: path = ROOT / rel if not path.exists(): continue if is_cpp_file(path): print(f"Prüfe: {rel}") try: check_format(path) except subprocess.CalledProcessError: print(f"ERROR: Formatfehler in {rel}") sys.exit(1) if not has_related_test(path): print( "ERROR: Kein passender Test unter tests/, test/ oder " "qt6-after/qt6-migrated-benchmark/tests/ gefunden für: " f"{rel}" ) sys.exit(1) print("PR-Policy-Prüfungen erfolgreich.") if __name__ == "__main__": main()