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:
@@ -36,6 +36,12 @@ resolver — three pure helpers, no DB of their own:
|
||||
old-style ``https://user:pass@host/repo.git`` URL pasted into the
|
||||
API is stored bare and the embedded credential is moved into the
|
||||
``token`` column (an explicit ``token`` field wins — LOCKED A6).
|
||||
|
||||
Phase 135 (per-source branch pinning) adds the write-path branch
|
||||
validator next to them — :func:`validate_branch_name`, the single
|
||||
choke point: the API maps its ``ValueError`` to the fixed-detail 422
|
||||
(the raw input is never echoed, the house credential-safety
|
||||
discipline, applied for consistency).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -153,6 +159,51 @@ def normalize_credential(url: str, token: str | None) -> tuple[str, str | None]:
|
||||
return sanitize_url(url), effective
|
||||
|
||||
|
||||
#: Phase 135 (ASSUMPTION B2) — the safe branch-name charset:
|
||||
#: alphanumerics + dot, underscore, hyphen, and slash (a slash allows
|
||||
#: ``release/1.2``-style names — git branch names are paths). Anything
|
||||
#: else (whitespace, ``@``, ``~``, ``^``, ``:``, ``?``, ``*``, ``[``,
|
||||
#: backslash, …) is rejected by the character-set check.
|
||||
_BRANCH_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
|
||||
|
||||
|
||||
def validate_branch_name(raw: str) -> str:
|
||||
"""Trim + validate a branch name (phase 135, ASSUMPTION B2).
|
||||
|
||||
Returns the trimmed name; raises ``ValueError`` (fixed reason
|
||||
strings, for logs only — the API maps to the fixed-detail 422 and
|
||||
the raw input is never echoed) when the name is empty, > 200
|
||||
chars, outside the safe charset (``[A-Za-z0-9._/-]+``), contains
|
||||
``..`` or ``//``, starts with ``-``/``.``/``/``, ends with ``/``
|
||||
or ``.``, or is ``HEAD`` (case-insensitive — git's refname; a
|
||||
branch literally named ``head`` is a documented cost of the simple
|
||||
rule, the plan's design note).
|
||||
|
||||
Pure — no DB, no logging of the raw value (the reason strings are
|
||||
fixed, input-free: the same discipline that keeps credentials out
|
||||
of the 422 details, applied here for consistency).
|
||||
"""
|
||||
name = raw.strip()
|
||||
if not name:
|
||||
raise ValueError("branch name must not be empty")
|
||||
if len(name) > 200:
|
||||
raise ValueError("branch name exceeds 200 characters")
|
||||
if not _BRANCH_RE.match(name):
|
||||
raise ValueError("branch name contains invalid characters")
|
||||
if ".." in name or "//" in name:
|
||||
raise ValueError("branch name contains a disallowed path sequence")
|
||||
if name[0] in ("-", ".", "/"):
|
||||
raise ValueError("branch name has a disallowed leading character")
|
||||
if name[-1] in ("/", "."):
|
||||
raise ValueError("branch name has a disallowed trailing character")
|
||||
if name.upper() == "HEAD":
|
||||
# Fixed reason, no echo: the input here is the reserved name
|
||||
# itself, and the reason must stay input-free (the log-line
|
||||
# discipline — a typed name never surfaces, reserved or not).
|
||||
raise ValueError("branch name is reserved")
|
||||
return name
|
||||
|
||||
|
||||
def effective_sources(db: Session) -> tuple[list[GitSource], Literal["db", "env"]]:
|
||||
"""``(rows, origin)`` — the effective source rows (both kinds) and
|
||||
where they came from.
|
||||
|
||||
Reference in New Issue
Block a user