diff --git a/src/repo_registry/repo_ingestion/git.py b/src/repo_registry/repo_ingestion/git.py index c65f243..b327515 100644 --- a/src/repo_registry/repo_ingestion/git.py +++ b/src/repo_registry/repo_ingestion/git.py @@ -63,14 +63,20 @@ class GitIngestionService: def _run_git(self, args: list[str], *, cwd: Path | None) -> None: if shutil.which("git") is None: raise RuntimeError("git executable was not found") - result = subprocess.run( - ["git", *args], - cwd=cwd, - check=False, - capture_output=True, - text=True, - timeout=120, - ) + command = ["git", *args] + try: + result = subprocess.run( + command, + cwd=cwd, + check=False, + capture_output=True, + text=True, + timeout=120, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError( + f"git {' '.join(args)} timed out after {exc.timeout} seconds" + ) from exc if result.returncode != 0: message = result.stderr.strip() or result.stdout.strip() raise RuntimeError(f"git {' '.join(args)} failed: {message}") diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 328d992..b8bbc7f 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -34,6 +34,8 @@ def page(title: str, body: str) -> HTMLResponse: --accent: #0f766e; --accent-dark: #115e59; --warn: #9a3412; + --danger: #b42318; + --danger-bg: #fff4f2; }} * {{ box-sizing: border-box; }} body {{ @@ -64,6 +66,17 @@ def page(title: str, body: str) -> HTMLResponse: border-radius: 8px; padding: 16px; }} + .notice {{ + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 12px; + }} + .notice.error {{ + border-color: #f3b8ae; + background: var(--danger-bg); + color: var(--danger); + }} .stack {{ display: grid; gap: 12px; }} .muted {{ color: var(--muted); }} .pill {{ @@ -111,6 +124,9 @@ def page(title: str, body: str) -> HTMLResponse: .tree li {{ margin: 6px 0; }} .source {{ color: var(--muted); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }} .actions {{ display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }} + [data-pending] {{ display: none; color: var(--muted); }} + form.is-submitting [data-pending] {{ display: inline; }} + form.is-submitting button[type="submit"] {{ opacity: .7; cursor: wait; }} @media (max-width: 780px) {{ header {{ padding: 12px 16px; }} main {{ padding: 16px; }} @@ -132,14 +148,28 @@ def page(title: str, body: str) -> HTMLResponse:
{body}
+ """ ) -@router.get("/ui") -def repository_index(service: RegistryService = Depends(get_service)) -> HTMLResponse: +def render_repository_index( + service: RegistryService, + *, + error_message: str | None = None, + status_code: int = 200, +) -> HTMLResponse: repositories = service.list_repositories() rows = "\n".join( f""" @@ -152,15 +182,29 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes """ for repo in repositories ) + error = ( + f""" + + """ + if error_message + else "" + ) body = f"""

Repositories

+ {error}

Register Repository

- +
+ + Registering repository... +
@@ -175,7 +219,14 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
""" - return page("Repositories", body) + response = page("Repositories", body) + response.status_code = status_code + return response + + +@router.get("/ui") +def repository_index(service: RegistryService = Depends(get_service)) -> HTMLResponse: + return render_repository_index(service) @router.get("/ui/discovery") @@ -360,11 +411,18 @@ def create_repository_from_form( url: str = Form(...), branch: str = Form("main"), service: RegistryService = Depends(get_service), -) -> RedirectResponse: - repository = service.register_repository( - url=url, - branch=branch or "main", - ) +): + try: + repository = service.register_repository( + url=url, + branch=branch or "main", + ) + except (RuntimeError, ValueError) as exc: + return render_repository_index( + service, + error_message=str(exc), + status_code=400, + ) return RedirectResponse(f"/ui/repos/{repository.id}", status_code=303) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index f07cc73..ec7a59c 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -1073,6 +1073,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path): index_response = client.get("/ui") assert index_response.status_code == 200 assert "Register Repository" in index_response.text + assert "Registering repository..." in index_response.text create_response = client.post( "/ui/repos", @@ -1201,6 +1202,32 @@ def test_ui_register_analyze_and_approve_loop(tmp_path): app.dependency_overrides.clear() +def test_ui_registration_failure_returns_feedback(tmp_path): + def override_settings(): + return Settings( + database_path=str(tmp_path / "ui-error.sqlite3"), + checkout_root=str(tmp_path / "ui-error-checkouts"), + ) + + app.dependency_overrides[get_settings] = override_settings + client = TestClient(app) + try: + response = client.post( + "/ui/repos", + data={ + "url": str(tmp_path / "missing-repo"), + "branch": "main", + }, + ) + + assert response.status_code == 400 + assert "Registration failed." in response.text + assert "git clone" in response.text + assert "Register Repository" in response.text + finally: + app.dependency_overrides.clear() + + def test_ui_manual_registry_entry_loop(tmp_path): source = tmp_path / "manual-repo" source.mkdir() diff --git a/workplans/RREG-WP-0003-automatic-repository-exploration.md b/workplans/RREG-WP-0003-automatic-repository-exploration.md index 3af9df3..80b4429 100644 --- a/workplans/RREG-WP-0003-automatic-repository-exploration.md +++ b/workplans/RREG-WP-0003-automatic-repository-exploration.md @@ -27,7 +27,7 @@ configured trusted automation mode. ```task id: RREG-WP-0003-T01 -status: todo +status: in_progress priority: high state_hub_task_id: "ab718ce7-d38f-4080-9385-99be1bf80475" ```