generated from coulomb/repo-seed
ui improvements on error when registering repos
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
</nav>
|
||||
</header>
|
||||
<main>{body}</main>
|
||||
<script>
|
||||
document.addEventListener("submit", (event) => {{
|
||||
const form = event.target;
|
||||
if (form instanceof HTMLFormElement) {{
|
||||
form.classList.add("is-submitting");
|
||||
const button = form.querySelector('button[type="submit"]');
|
||||
if (button) button.setAttribute("disabled", "disabled");
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@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"""
|
||||
<div class="notice error" role="alert">
|
||||
<strong>Registration failed.</strong>
|
||||
<p>{escape(error_message)}</p>
|
||||
</div>
|
||||
"""
|
||||
if error_message
|
||||
else ""
|
||||
)
|
||||
body = f"""
|
||||
<h1>Repositories</h1>
|
||||
{error}
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<h2>Register Repository</h2>
|
||||
<form class="stack" method="post" action="/ui/repos">
|
||||
<label>Git URL or local path <input name="url" required></label>
|
||||
<label>Branch <input name="branch" value="main"></label>
|
||||
<button type="submit">Register</button>
|
||||
<div class="actions">
|
||||
<button type="submit">Register</button>
|
||||
<span data-pending>Registering repository...</span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="panel">
|
||||
@@ -175,7 +219,14 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user