103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""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}")
|