"""Non-interactive, branch-specific Git updates of the configured wiki.""" from __future__ import annotations import os import re import subprocess from pathlib import Path from urllib.parse import urlsplit GIT_TIMEOUT_SECONDS = 120 def expected_branch() -> str: branch = os.getenv("EXPECTED_GITEA_BRANCH", "master").strip() if ( not branch or branch.startswith(("-", "/")) or branch.endswith(("/", ".")) or ".." in branch or "@{" in branch or branch == "@" or re.search(r"[\s\x00-\x1f\x7f~^:?*\[\\]", branch) or any( not part or part.startswith(".") or part.endswith(".lock") for part in branch.split("/") ) ): raise RuntimeError("EXPECTED_GITEA_BRANCH nie je platný názov Git vetvy") return branch def repository_from_url(url: str) -> str | None: if "://" in url: parsed = urlsplit(url) if parsed.scheme not in {"ssh", "https", "http", "git"}: return None path = parsed.path elif re.match(r"[^/\s]+:[^\s]+", url): path = url.split(":", 1)[1] else: return None path = path.strip("/").removesuffix(".git") return path if len(path.split("/")) == 2 else None def update_wiki(repo_path: Path) -> None: if not repo_path.exists(): raise RuntimeError(f"ZPWIKI_ROOT neexistuje: {repo_path}") if not (repo_path / ".git").exists(): raise RuntimeError(f"Nie je to git repozitár: {repo_path}") environment = {**os.environ, "GIT_TERMINAL_PROMPT": "0"} def git(*args): try: result = subprocess.run( ["git", *args], cwd=repo_path, env=environment, text=True, capture_output=True, timeout=GIT_TIMEOUT_SECONDS, check=False, ) except subprocess.TimeoutExpired as error: raise RuntimeError( f"Git aktualizácia prekročila limit {GIT_TIMEOUT_SECONDS} sekúnd" ) from error if result.returncode: # Remote URLs and credentials can occur in Git stderr; do not place # them in API responses or ordinary application logs. raise RuntimeError( f"Git operácia {args[0]} zlyhala s návratovým kódom {result.returncode}" ) return result.stdout.strip() expected = os.getenv("EXPECTED_GITEA_REPOSITORY", "").strip() origin = repository_from_url(git("remote", "get-url", "origin")) if not expected or origin is None or origin.casefold() != expected.casefold(): raise RuntimeError("Git origin nezodpovedá EXPECTED_GITEA_REPOSITORY") branch = expected_branch() if git("branch", "--show-current") != branch: raise RuntimeError("Aktívna Git vetva nezodpovedá EXPECTED_GITEA_BRANCH") if git("status", "--porcelain", "--untracked-files=normal"): raise RuntimeError("Wiki obsahuje lokálne zmeny; automatická aktualizácia ich neprepíše") if os.getenv("SYNC_UPSTREAM_FORK", "0") == "1": if expected != "jp170na/zpwiki" or branch != "master": raise RuntimeError("Upstream fork synchronization is restricted to jp170na/zpwiki master") key_file = os.getenv("ZPWIKI_SYNC_KEY_FILE", "/run/secrets/zpwiki_ssh_key") script = Path(__file__).resolve().parents[1] / "git_runner/sync_fork.sh" result = subprocess.run(["bash", str(script), "--key-file", key_file], env=environment, capture_output=True, text=True, timeout=GIT_TIMEOUT_SECONDS, check=False) if result.returncode: raise RuntimeError("Fork synchronization failed; local indexes were not changed") git( "fetch", "--no-tags", "--no-recurse-submodules", "origin", f"refs/heads/{branch}:refs/remotes/origin/{branch}", ) git("merge", "--ff-only", f"refs/remotes/origin/{branch}")