"""Docs-push service (phase 59, task 03). Commits a document into the configured docs repo and pushes it to the configured branch. **D3 (owner-locked, 2026-08-31): push only** — the flow ends at the push to the branch named in ``.env`` (``BOR_DOCS_REPO`` + ``BOR_DOCS_BRANCH``); no PR is ever created or attempted (no ``gh``, no PR URL, no URL construction, no GitHub-specific logic). The remote is **generic** — a local path, ``https://``, ``ssh://``, any host — and git is invoked exclusively through :func:`scripts.git_sync.run_git` (A11: stdlib ``subprocess`` only, no new packages): :func:`push_document` is the only caller of git for this feature besides :mod:`scripts.git_sync` itself. The push is a plain ``git push origin `` — git 2.55 no longer accepts the ``--ff-only`` flag on ``push`` — and a push without ``-f`` / a ``+`` refspec already refuses non-fast-forward updates (client-side, against the remote's live refs), so a concurrently-advanced remote fails loudly: never a force-push, never a merge. Every failure raises :class:`DocsPushError` carrying git's stderr (the ``GitSyncError`` style), and the checkout under ``work_dir`` is left as-is for inspection. """ from __future__ import annotations from pathlib import Path from scripts.git_sync import GitSyncError, run_git __all__ = ["DocsPushError", "push_document"] # Fixed per-invocation commit identity (phase 59 ASSUMPTION) — passed # with ``-c`` on the commit itself, so the push never relies on the # machine's global git config. ``commit.gpgsign=false`` keeps a # headless server from ever prompting for a GPG pinentry (this repo's # own no-GPG-signing policy). _COMMIT_NAME = "Brain of Reese" _COMMIT_EMAIL = "bor@local" class DocsPushError(RuntimeError): """A docs push failed (or git is missing); carries git's stderr.""" def _refuse_unsafe_rel_path(rel_path: str) -> None: """Defensively re-assert the upstream path guard-rails (task 02). The file is written inside the docs checkout, so a blank path, an absolute path, or any ``.``/``..`` component is refused here as well — even though the drafts API already guard-railed the value. """ rel = Path(rel_path) if not rel_path.strip() or rel.is_absolute() or any(part in (".", "..") for part in rel.parts): raise DocsPushError(f"refusing unsafe rel_path: {rel_path!r}") def push_document( repo: str, base_branch: str, branch: str, work_dir: str, rel_path: str, content: str, commit_message: str, ) -> tuple[str, str]: """Commit *content* at *rel_path* and push (ff-only) to *branch*. Step sequence (each step via :func:`scripts.git_sync.run_git`; ``cwd=work_dir`` once the checkout exists): 1. Ensure the checkout: without ``.git`` → ``git clone --depth 1 --branch `` (the base branch is explicit — the remote's default may differ). 2. ``git fetch --depth 1 origin `` — re-sync the base before every push. The fetched base tip (``FETCH_HEAD``) is captured immediately — a later *failed* fetch clears ``FETCH_HEAD``. 3. ``git fetch --depth 100 origin `` — **failure is expected** while the branch does not exist on the remote yet (swallowed and continued); on success the fetched branch tip is captured the same way. 4. Attach the local branch: - a local branch left by a previous push → ``git checkout `` — keep its own history, so a concurrently-advanced remote then fails the push loudly in step 7 instead of being silently re-based or merged; - otherwise ``git checkout -B `` onto the captured branch tip (re-attach onto the previously pushed branch — its history, so the push can fast-forward) or, on first push, onto the captured base tip (a new branch from the fresh base). The captured shas are used instead of symbolic refs: git's local transport (a local-path remote) does not create ``refs/remotes/origin/`` for a newly fetched branch, and a failed fetch clears ``FETCH_HEAD``. 5. Write the file (parent dirs created; :func:`_refuse_unsafe_rel_path` re-asserts the parts check defensively). 6. ``git add -- `` + ``commit -m `` with the fixed per-invocation identity (``-c user.name`` / ``-c user.email`` — no reliance on global git config). 7. ``git push origin `` — creates the remote branch on first push; a concurrently-advanced remote is refused non-fast-forward (never a force-push, never a merge). 8. ``sha = git rev-parse HEAD``; return ``(branch, sha)``. Raises :class:`DocsPushError` (git's stderr in the message) on any failure — the checkout is left as-is for inspection. """ _refuse_unsafe_rel_path(rel_path) work = Path(work_dir).expanduser() try: if not (work / ".git").exists(): work.parent.mkdir(parents=True, exist_ok=True) run_git( [ "git", "clone", "--depth", "1", "--branch", base_branch, repo, str(work), ], cwd=work.parent, ) run_git(["git", "fetch", "--depth", "1", "origin", base_branch], cwd=work) # Capture the fetched base tip now — a *failed* fetch clears # FETCH_HEAD, so the symbolic ref cannot be reused later. base_sha = run_git(["git", "rev-parse", "FETCH_HEAD"], cwd=work).strip() try: run_git(["git", "fetch", "--depth", "100", "origin", branch], cwd=work) branch_sha = run_git(["git", "rev-parse", "FETCH_HEAD"], cwd=work).strip() except GitSyncError: branch_sha = None # the branch does not exist on the remote yet if run_git(["git", "branch", "--list", branch], cwd=work).strip(): run_git(["git", "checkout", branch], cwd=work) else: anchor = branch_sha if branch_sha is not None else base_sha run_git(["git", "checkout", "-B", branch, anchor], cwd=work) target = work / rel_path target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") run_git(["git", "add", "--", rel_path], cwd=work) run_git( [ "git", "-c", f"user.name={_COMMIT_NAME}", "-c", f"user.email={_COMMIT_EMAIL}", "-c", "commit.gpgsign=false", "commit", "-m", commit_message, ], cwd=work, ) run_git(["git", "push", "origin", branch], cwd=work) sha = run_git(["git", "rev-parse", "HEAD"], cwd=work).strip() except GitSyncError as err: raise DocsPushError(str(err)) from err return branch, sha