phase: 135_git_source_branch
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:
+44
-1
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user