"""Git source sync for import_docs (phase 28). clone_or_pull(url, dest) clones ``url`` into ``dest`` (shallow, depth 1) the first time, or fast-forwards an existing checkout with ``git pull --ff-only`` on subsequent runs. Auth: nothing special — an ``https://…`` URL uses the OS credential helper / prompts; a ``git@host:repo.git`` URL uses the machine's SSH key. No credentials are stored here; whatever the URL/SSH config supplies is used. This module is the only place the ``git`` CLI is invoked (A11: stdlib ``subprocess`` only, no new packages). """ from __future__ import annotations import subprocess from pathlib import Path __all__ = ["GitSyncError", "clone_or_pull"] class GitSyncError(RuntimeError): """A git clone/pull failed (or git is missing); carries git's stderr.""" def clone_or_pull(url: str, dest: Path | str) -> Path: """Clone ``url`` into ``dest`` (shallow, first run) or fast-forward it. - dest without a ``.git`` (or absent) → ``git clone --depth 1 url dest`` (shallow: the KB is re-imported incrementally anyway). - dest with a ``.git`` → ``git pull --ff-only`` (refuses to merge unrelated histories — a broken checkout fails loudly rather than producing a dirty index). Returns the destination path. Raises :class:`GitSyncError` when git is missing or a git invocation exits non-zero (with git's stderr in the message, so the caller can name the failing repo + reason). """ dest = Path(dest) if not dest.exists() or not (dest / ".git").exists(): dest.parent.mkdir(parents=True, exist_ok=True) _run(["git", "clone", "--depth", "1", url, str(dest)], cwd=dest.parent) else: _run(["git", "pull", "--ff-only"], cwd=dest) return dest def _run(argv: list[str], cwd: Path) -> str: """Run a git command, capturing output; raise GitSyncError on failure.""" try: proc = subprocess.run(argv, cwd=cwd, capture_output=True, text=True) except FileNotFoundError: raise GitSyncError("git was not found on PATH — install git and retry") from None if proc.returncode != 0: raise GitSyncError( f"git {' '.join(argv[1:])} failed (exit {proc.returncode}): " f"{proc.stderr.strip()}" ) return proc.stdout