From 14e63ca5b64e8f41f506b4f9a64c4174bef909a6 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 5 Jul 2026 05:10:23 +0900 Subject: [PATCH] fix: derive repository UI from registry --- crates/workspace-server/src/repositories.rs | 54 ++++++++++++-- web/workspace/deno.json | 2 +- .../lib/workspace-pages/WorkspacePage.svelte | 48 +++++++++---- .../RepositoriesNavSection.svelte | 44 ++++++++---- .../workspace-sidebar/WorkspaceSidebar.svelte | 14 +++- .../workspace-sidebar/repository-nav.test.ts | 70 +++++++++++++++++++ .../lib/workspace-sidebar/repository-nav.ts | 36 ++++++++++ .../src/lib/workspace-sidebar/types.ts | 7 ++ .../repositories/[repositoryId]/+page.svelte | 2 +- 9 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 web/workspace/src/lib/workspace-sidebar/repository-nav.test.ts create mode 100644 web/workspace/src/lib/workspace-sidebar/repository-nav.ts diff --git a/crates/workspace-server/src/repositories.rs b/crates/workspace-server/src/repositories.rs index 8bc8bfe1..1f318b46 100644 --- a/crates/workspace-server/src/repositories.rs +++ b/crates/workspace-server/src/repositories.rs @@ -1,4 +1,8 @@ -use std::{collections::BTreeSet, path::PathBuf, process::Command}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, + process::Command, +}; use serde::{Deserialize, Serialize}; @@ -336,15 +340,35 @@ fn parse_git_log(raw: &str) -> Vec { } fn sanitize_remote_url(url: &str) -> String { - let Some((scheme, rest)) = url.split_once("://") else { - return url.to_string(); + let trimmed = url.trim(); + if is_local_path_like(trimmed) { + return "".to_string(); + } + + let Some((scheme, rest)) = trimmed.split_once("://") else { + return trimmed.to_string(); }; + if scheme.eq_ignore_ascii_case("file") { + return "file://".to_string(); + } let Some((_credentials, host_path)) = rest.split_once('@') else { - return url.to_string(); + return trimmed.to_string(); }; format!("{scheme}://@{host_path}") } +fn is_local_path_like(value: &str) -> bool { + Path::new(value).is_absolute() || is_windows_absolute_path_like(value) +} + +fn is_windows_absolute_path_like(value: &str) -> bool { + let bytes = value.as_bytes(); + bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && matches!(bytes[2], b'\\' | b'/') +} + fn non_empty_string(value: &str) -> Option { if value.is_empty() { None @@ -358,7 +382,7 @@ mod tests { use super::*; #[test] - fn sanitizes_remote_credentials() { + fn sanitizes_remote_credentials_and_local_paths() { assert_eq!( sanitize_remote_url("https://user:token@example.com/org/repo.git"), "https://@example.com/org/repo.git" @@ -367,6 +391,26 @@ mod tests { sanitize_remote_url("git@example.com:org/repo.git"), "git@example.com:org/repo.git" ); + assert_eq!( + sanitize_remote_url("/home/alice/private/repo.git"), + "" + ); + assert_eq!( + sanitize_remote_url("/Users/alice/private/repo.git"), + "" + ); + assert_eq!( + sanitize_remote_url("C:\\Users\\alice\\private\\repo.git"), + "" + ); + assert_eq!( + sanitize_remote_url("file:///home/alice/private/repo.git"), + "file://" + ); + assert_eq!( + sanitize_remote_url("file://localhost/home/alice/private/repo.git"), + "file://" + ); } #[test] diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 60c8b090..ae8656b4 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts src/lib/workspace-sidebar/worker-launch.test.ts", + "test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts src/lib/workspace-sidebar/worker-launch.test.ts src/lib/workspace-sidebar/repository-nav.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte index 85aa6876..e9d99d4d 100644 --- a/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte +++ b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte @@ -8,6 +8,7 @@ ObjectiveDetail, ObjectiveListResponse, RepositoryDetailResponse, + RepositoryListResponse, RepositorySummary, RepositoryTicketsResponse, Worker, @@ -25,12 +26,13 @@ let { view = 'overview', objectiveId = null, - repositoryId = 'main' - }: { view?: WorkspaceView; repositoryId?: string; objectiveId?: string | null } = $props(); + repositoryId = null + }: { view?: WorkspaceView; repositoryId?: string | null; objectiveId?: string | null } = $props(); let workspace = $state(null); let hosts = $state | null>(null); let workers = $state | null>(null); + let repositories = $state(null); let repository = $state(null); let repositoryTickets = $state(null); let objectives = $state(null); @@ -39,6 +41,7 @@ let workspaceError = $state(null); let hostsError = $state(null); let workersError = $state(null); + let repositoriesError = $state(null); let repositoryError = $state(null); let repositoryTicketsError = $state(null); let objectivesError = $state(null); @@ -86,12 +89,21 @@ } } - async function loadRepository() { + async function loadRepositories() { + repositoriesError = null; + try { + repositories = await getJson('/api/repositories'); + } catch (error) { + repositoriesError = error instanceof Error ? error.message : String(error); + repositories = null; + } + } + + async function loadRepository(id: string) { repositoryError = null; - const selectedRepositoryId = route.page === 'repository' ? route.repositoryId : repositoryId; try { const detail = await getJson( - `/api/repositories/${encodeURIComponent(selectedRepositoryId)}` + `/api/repositories/${encodeURIComponent(id)}` ); repository = detail.item; } catch (error) { @@ -100,12 +112,11 @@ } } - async function loadRepositoryTickets() { + async function loadRepositoryTickets(id: string) { repositoryTicketsError = null; - const selectedRepositoryId = route.page === 'repository' ? route.repositoryId : repositoryId; try { repositoryTickets = await getJson( - `/api/repositories/${encodeURIComponent(selectedRepositoryId)}/tickets` + `/api/repositories/${encodeURIComponent(id)}/tickets` ); } catch (error) { repositoryTicketsError = error instanceof Error ? error.message : String(error); @@ -147,9 +158,9 @@ function routeFromView( view: WorkspaceView, objectiveId: string | null, - repositoryId: string + repositoryId: string | null ): RouteState { - if (view === 'repository') { + if (view === 'repository' && repositoryId) { return { page: 'repository', repositoryId }; } if (view === 'objective' && objectiveId) { @@ -182,11 +193,22 @@ void loadWorkspace(); void loadHosts(); void loadWorkers(); - void loadRepository(); - void loadRepositoryTickets(); + void loadRepositories(); void loadObjectives(); }); + $effect(() => { + if (route.page === 'repository') { + void loadRepository(route.repositoryId); + void loadRepositoryTickets(route.repositoryId); + } else { + repository = null; + repositoryTickets = null; + repositoryError = null; + repositoryTicketsError = null; + } + }); + $effect(() => { const selectedObjectiveId = route.page === 'objective' ? route.objectiveId : null; if (selectedObjectiveId) { @@ -210,7 +232,7 @@
- +
{#if route.page === 'repository'} diff --git a/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte b/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte index 33e3dd15..2642eed3 100644 --- a/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte +++ b/web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte @@ -1,26 +1,46 @@ diff --git a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte index 7279cfaf..27521b03 100644 --- a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte +++ b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte @@ -2,15 +2,23 @@ import ObjectivesNavSection from './ObjectivesNavSection.svelte'; import RepositoriesNavSection from './RepositoriesNavSection.svelte'; import WorkersNavSection from './WorkersNavSection.svelte'; - import type { WorkspaceResponse } from './types'; + import type { RepositoryListResponse, WorkspaceResponse } from './types'; type Props = { workspace: WorkspaceResponse | null; workspaceError?: string | null; + repositories?: RepositoryListResponse | null; + repositoriesError?: string | null; currentPath?: string; }; - let { workspace, workspaceError = null, currentPath = '/' }: Props = $props(); + let { + workspace, + workspaceError = null, + repositories = null, + repositoriesError = null, + currentPath = '/' + }: Props = $props(); let settingsActive = $derived(currentPath.startsWith("/settings")); @@ -43,7 +51,7 @@