phase: 135_git_source_branch
Build and Push Containers / build-and-push-app (push) Successful in 2m34s
Build and Push Containers / build-and-push-db (push) Successful in 14s

All verification complete. No edits were needed by this pass — the phase work and docs were already consistent and green.

**Phase 135 final verification pass — report**

- Verified all 5 tasks' work present & correct: migration 0024 (reversible), `GitSource.branch` + 4 schema shapes, `validate_branch_name` (full check order per design), `clone_or_pull(…, branch)` (HEAD guard first, `--branch` clone, fetch+ff merge), both call sites pass `row.branch`, UI (add-form field, editor field, row tag), `file://` URL transport.
- Completion criteria: (1) branch-mirrored through Sync + CLI — **pass** (E2E + integration); (2) `branch=None` byte-identical — **pass** (`test_branch_none_argv_is_byte_identical_pre_phase_sequence` green); (3) fixed 422s / local-row 422 / tri-state — **pass**; (4) mismatch fails loudly with remove-and-re-add — **pass** (E2E); (5) coverage 99% >90%, E2E isolated green — **pass**; (6) no regressions — **pass**.
- Tests: `uv run pytest --cov=app --cov-report=term-missing` → 3223 passed, app/ 99%; `uv run pytest tests/e2e/test_git_source_branch.py -v --no-cov` → 4 passed (16.4 s); `uv run ruff check .` clean; `uv run pyright` → 0 errors, 0 warnings.
- Documentation: README.md reviewed in full — phase-135 branch section accurate; the phase's README diff also correctly fixed a pre-existing stale line (shallow `--depth 1` clone vs full-history, phase 107). Routes, env defaults, feature lists verified against code. AGENTS.md — all 10 rules + quick-reference verified (referenced tests, PLAN.md, phases dirs, commands all accurate). **Verified consistent — no changes needed.**
- No live-infrastructure changes (dev DB already running and healthy; no live steps in this phase).
- Next pending phase: `.agents/phases/todo/136_force_push_reset`.
This commit is contained in:
2026-09-28 21:15:03 -04:00
parent fcac876d40
commit d3f8c7f2c8
50 changed files with 3955 additions and 140 deletions
+44 -1
View File
@@ -759,6 +759,18 @@ class GitSourceIn(BaseModel):
WRITE-ONLY field — stored in the dedicated ``git_sources.token``
column and NEVER echoed back by any output shape (``GitSourceOut``
/ ``GitSourceRow`` carry no token field by contract).
``branch`` (phase 135) is the branch the git source mirrors:
optional at create time (absent/None → stored NULL = the repo's
DEFAULT branch — every such row keeps its byte-identical
clone/pull behavior), trimmed *before* the length constraint runs
(the ``_trim_url`` precedent), max 200. Note the split from PATCH:
a trimmed-EMPTY value is treated as ABSENT at create (stored NULL,
same as omitted) — at PATCH, ``""`` is the tri-state's CLEAR leg
(see :class:`GitSourcePatchIn`). Non-empty values are validated at
the API boundary (``app.rag.git_sources.validate_branch_name``);
a ``branch`` on a ``local`` row is a 422 there (local rows are
walked, not cloned).
"""
kind: Literal["git", "local"] = "git"
@@ -767,6 +779,7 @@ class GitSourceIn(BaseModel):
ignore_paths: list[str] | None = Field(default=None)
include_hidden: bool | None = Field(default=None)
token: str | None = Field(default=None, max_length=500)
branch: str | None = Field(default=None, max_length=200)
@field_validator("url", mode="before")
@classmethod
@@ -783,6 +796,11 @@ class GitSourceIn(BaseModel):
def _trim_token(cls, v: object) -> object:
return v.strip() if isinstance(v, str) else v
@field_validator("branch", mode="before")
@classmethod
def _trim_branch(cls, v: object) -> object:
return v.strip() if isinstance(v, str) else v
class GitSourceOut(BaseModel):
"""One created git source as returned by ``POST`` (phase 35, task 02).
@@ -804,6 +822,12 @@ class GitSourceOut(BaseModel):
the omission a structural contract, not an accident: constructing
this model with a ``token`` key raises, so a regression that tries
to echo the credential back cannot even build the shape.
``branch`` (phase 135) is the stored value — the row's
``git_sources.branch`` (NULL → ``null`` in the response; NULL =
the repo's default branch). The field is explicit on the shape, so
``extra="forbid"`` carries no new omission risk — the token's
structural-omission contract above is untouched.
"""
model_config = ConfigDict(extra="forbid")
@@ -813,6 +837,7 @@ class GitSourceOut(BaseModel):
added_at: datetime | None
ignore_paths: list[str]
include_hidden: bool
branch: str | None
class GitSourceRow(BaseModel):
@@ -829,6 +854,10 @@ class GitSourceRow(BaseModel):
store a list on) report ``[]``. ``include_hidden`` (phase 105) is
the row's stored flag — env-fallback rows (no DB row to store a flag
on) report ``False`` (the ``ignore_paths: []`` precedent).
``branch`` (phase 135) is the row's stored value (NULL → ``null``
= the repo's default branch); env-fallback synthetic rows report
``None`` — there is no DB row to store one on (the
``ignore_paths: []`` / ``include_hidden: False`` precedent).
There is deliberately NO ``token`` field (phase 121, LOCKED A2):
same contract as :class:`GitSourceOut` — the credential never
@@ -846,6 +875,7 @@ class GitSourceRow(BaseModel):
added_at: datetime | None
ignore_paths: list[str]
include_hidden: bool
branch: str | None
class GitSourcePatchIn(BaseModel):
@@ -865,18 +895,31 @@ class GitSourcePatchIn(BaseModel):
clear** (the UI offers replace; clear exists for API completeness).
Trimmed *before* the length constraints run (the ``GitSourceIn``
``_trim_token`` precedent — whitespace-only counts as a clear),
max 500. All absent → 200 no-op (the row is untouched).
max 500. ``branch`` (phase 135) is TRI-STATE with the token's
exact three-way semantics: **absent/None = no change** (keep the
row's stored branch), **non-empty = set** (re-validated at the API
boundary — ``app.rag.git_sources.validate_branch_name``), **empty
string = clear** (stored NULL = the repo's default branch). Trimmed
*before* the length constraint runs (the ``_trim_token`` precedent
— whitespace-only counts as a clear), max 200. All absent → 200
no-op (the row is untouched).
"""
ignore_paths: list[str] | None = Field(default=None)
include_hidden: bool | None = Field(default=None)
token: str | None = Field(default=None, max_length=500)
branch: str | None = Field(default=None, max_length=200)
@field_validator("token", mode="before")
@classmethod
def _trim_token(cls, v: object) -> object:
return v.strip() if isinstance(v, str) else v
@field_validator("branch", mode="before")
@classmethod
def _trim_branch(cls, v: object) -> object:
return v.strip() if isinstance(v, str) else v
class GitSourceList(BaseModel):
"""``GET /api/git-sources`` response (phase 35, task 02).