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:
|
def _run_git(self, args: list[str], *, cwd: Path | None) -> None:
|
||||||
if shutil.which("git") is None:
|
if shutil.which("git") is None:
|
||||||
raise RuntimeError("git executable was not found")
|
raise RuntimeError("git executable was not found")
|
||||||
result = subprocess.run(
|
command = ["git", *args]
|
||||||
["git", *args],
|
try:
|
||||||
cwd=cwd,
|
result = subprocess.run(
|
||||||
check=False,
|
command,
|
||||||
capture_output=True,
|
cwd=cwd,
|
||||||
text=True,
|
check=False,
|
||||||
timeout=120,
|
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:
|
if result.returncode != 0:
|
||||||
message = result.stderr.strip() or result.stdout.strip()
|
message = result.stderr.strip() or result.stdout.strip()
|
||||||
raise RuntimeError(f"git {' '.join(args)} failed: {message}")
|
raise RuntimeError(f"git {' '.join(args)} failed: {message}")
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
--accent: #0f766e;
|
--accent: #0f766e;
|
||||||
--accent-dark: #115e59;
|
--accent-dark: #115e59;
|
||||||
--warn: #9a3412;
|
--warn: #9a3412;
|
||||||
|
--danger: #b42318;
|
||||||
|
--danger-bg: #fff4f2;
|
||||||
}}
|
}}
|
||||||
* {{ box-sizing: border-box; }}
|
* {{ box-sizing: border-box; }}
|
||||||
body {{
|
body {{
|
||||||
@@ -64,6 +66,17 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
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; }}
|
.stack {{ display: grid; gap: 12px; }}
|
||||||
.muted {{ color: var(--muted); }}
|
.muted {{ color: var(--muted); }}
|
||||||
.pill {{
|
.pill {{
|
||||||
@@ -111,6 +124,9 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
.tree li {{ margin: 6px 0; }}
|
.tree li {{ margin: 6px 0; }}
|
||||||
.source {{ color: var(--muted); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }}
|
.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; }}
|
.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) {{
|
@media (max-width: 780px) {{
|
||||||
header {{ padding: 12px 16px; }}
|
header {{ padding: 12px 16px; }}
|
||||||
main {{ padding: 16px; }}
|
main {{ padding: 16px; }}
|
||||||
@@ -132,14 +148,28 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>{body}</main>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ui")
|
def render_repository_index(
|
||||||
def repository_index(service: RegistryService = Depends(get_service)) -> HTMLResponse:
|
service: RegistryService,
|
||||||
|
*,
|
||||||
|
error_message: str | None = None,
|
||||||
|
status_code: int = 200,
|
||||||
|
) -> HTMLResponse:
|
||||||
repositories = service.list_repositories()
|
repositories = service.list_repositories()
|
||||||
rows = "\n".join(
|
rows = "\n".join(
|
||||||
f"""
|
f"""
|
||||||
@@ -152,15 +182,29 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
|
|||||||
"""
|
"""
|
||||||
for repo in repositories
|
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"""
|
body = f"""
|
||||||
<h1>Repositories</h1>
|
<h1>Repositories</h1>
|
||||||
|
{error}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Register Repository</h2>
|
<h2>Register Repository</h2>
|
||||||
<form class="stack" method="post" action="/ui/repos">
|
<form class="stack" method="post" action="/ui/repos">
|
||||||
<label>Git URL or local path <input name="url" required></label>
|
<label>Git URL or local path <input name="url" required></label>
|
||||||
<label>Branch <input name="branch" value="main"></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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -175,7 +219,14 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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")
|
@router.get("/ui/discovery")
|
||||||
@@ -360,11 +411,18 @@ def create_repository_from_form(
|
|||||||
url: str = Form(...),
|
url: str = Form(...),
|
||||||
branch: str = Form("main"),
|
branch: str = Form("main"),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
):
|
||||||
repository = service.register_repository(
|
try:
|
||||||
url=url,
|
repository = service.register_repository(
|
||||||
branch=branch or "main",
|
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)
|
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")
|
index_response = client.get("/ui")
|
||||||
assert index_response.status_code == 200
|
assert index_response.status_code == 200
|
||||||
assert "Register Repository" in index_response.text
|
assert "Register Repository" in index_response.text
|
||||||
|
assert "Registering repository..." in index_response.text
|
||||||
|
|
||||||
create_response = client.post(
|
create_response = client.post(
|
||||||
"/ui/repos",
|
"/ui/repos",
|
||||||
@@ -1201,6 +1202,32 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
|||||||
app.dependency_overrides.clear()
|
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):
|
def test_ui_manual_registry_entry_loop(tmp_path):
|
||||||
source = tmp_path / "manual-repo"
|
source = tmp_path / "manual-repo"
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ configured trusted automation mode.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0003-T01
|
id: RREG-WP-0003-T01
|
||||||
status: todo
|
status: in_progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "ab718ce7-d38f-4080-9385-99be1bf80475"
|
state_hub_task_id: "ab718ce7-d38f-4080-9385-99be1bf80475"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user