"""The shared git-source resolver (phase 35, task 03). One function, two callers: the in-app Sync pipeline (``app/api/sync.py::_run_sync``) and the CLI (``scripts/import_docs.py::_resolve_sources``) — both resolve the repo URLs to clone/pull through :func:`effective_git_sources`, so the admin-managed ``git_sources`` table is what actually gets cloned and indexed from either entry point (the API's ``GET /api/git-sources`` fallback list is the only other place that reads the env list — task 02). Precedence (the phase's locked decision — the env var is demoted, not removed): ``git_sources`` DB rows win, in ``(added_at, id)`` order; ``BOR_GIT_SOURCES`` is a fallback only while the table is empty; both empty → ``([], "env")`` and each caller keeps its existing fail-loud behavior (sync: ``GitSyncError``; CLI: the legacy ``DEFAULT_SOURCES`` fallback). """ from __future__ import annotations from typing import Literal from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.models import GitSource def effective_git_sources(db: Session) -> tuple[list[str], Literal["db", "env"]]: """``(urls, origin)`` — the repo URLs to clone, and where they came from. ``"db"``: the ``git_sources`` rows in ``(added_at, id)`` order (oldest first, id tie-break for same-timestamp inserts) — once the table has any row, ``BOR_GIT_SOURCES`` is ignored entirely. ``"env"``: the ``BOR_GIT_SOURCES`` list — ``Settings.git_source_list``, the phase-28 CSV parse, reused not re-implemented — used only while the table is empty; both empty → ``([], "env")``. """ rows = db.scalars( select(GitSource).order_by(GitSource.added_at.asc(), GitSource.id.asc()) ).all() if rows: return [row.url for row in rows], "db" return list(get_settings().git_source_list), "env"