From 6203316aa1d6d841f15b09afcedf9eacfa47fd61 Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 24 Aug 2026 13:46:02 +0900 Subject: [PATCH] fix: defer local repository validation to runtimes --- crates/workspace-server/src/main.rs | 33 ++-------- crates/workspace-server/src/repositories.rs | 9 +++ crates/workspace-server/src/server.rs | 25 +++---- .../workspace-server/src/workspace_catalog.rs | 65 ++++--------------- .../src/lib/workspace/sidebar/types.ts | 8 +++ web/workspace/src/routes/+page.svelte | 2 +- .../repositories/[repositoryId]/+page.svelte | 14 +++- 7 files changed, 58 insertions(+), 98 deletions(-) diff --git a/crates/workspace-server/src/main.rs b/crates/workspace-server/src/main.rs index 36045227..e9680ee5 100644 --- a/crates/workspace-server/src/main.rs +++ b/crates/workspace-server/src/main.rs @@ -711,36 +711,15 @@ fn infer_workspace_root_from_repositories( ))); }; - match repository.source.kind { - workspace_api::RepositorySourceKind::LocalPath => { - let repository_path = PathBuf::from(&repository.source.uri); - if !repository_path.is_absolute() { - return Err(CliError(format!( - "repository `{}` has relative local source `{}`; local repository sources used by serve must be absolute paths", - repository.repository_id, repository.source.uri - ))); - } - Ok(repository_path) - } - workspace_api::RepositorySourceKind::File => url::Url::parse(&repository.source.uri) - .ok() - .and_then(|uri| uri.to_file_path().ok()) - .ok_or_else(|| { - CliError(format!( - "repository `{}` has invalid file source `{}`", - repository.repository_id, repository.source.uri - )) - }), - workspace_api::RepositorySourceKind::Ssh - | workspace_api::RepositorySourceKind::Http - | workspace_api::RepositorySourceKind::Https => Ok( - ServerConfig::default_workspace_backend_data_root(&workspace.workspace_id), - ), - workspace_api::RepositorySourceKind::Invalid => Err(CliError(format!( + if repository.source.kind == workspace_api::RepositorySourceKind::Invalid { + return Err(CliError(format!( "repository `{}` has an invalid migrated source and cannot be used by serve", repository.repository_id - ))), + ))); } + Ok(ServerConfig::default_workspace_backend_data_root( + &workspace.workspace_id, + )) } fn parse_config_command(args: &[String]) -> Result { diff --git a/crates/workspace-server/src/repositories.rs b/crates/workspace-server/src/repositories.rs index d639b6e7..8e8b5d80 100644 --- a/crates/workspace-server/src/repositories.rs +++ b/crates/workspace-server/src/repositories.rs @@ -325,6 +325,15 @@ impl RepositoryRegistryReader { .clone() .unwrap_or_else(|| repository.id.clone()); let mut diagnostics = Vec::new(); + if repository.source.kind == workspace_api::RepositorySourceKind::Http { + diagnostics.push(RepositoryDiagnostic { + severity: "warning".to_string(), + code: "repository_source_insecure_http".to_string(), + message: + "HTTP Repository source is unencrypted; prefer HTTPS or SSH when available." + .to_string(), + }); + } let git = match repository.provider.as_str() { "git" if repository.path.is_none() => { diagnostics.push(RepositoryDiagnostic { diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index a524555b..5d1b7745 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -271,22 +271,15 @@ impl ServerConfig { workspace: &WorkspaceRecord, repositories: Vec, ) -> Result { - let primary = repositories - .iter() - .find(|repository| repository.repository_id == "main") - .or_else(|| repositories.first()) - .ok_or_else(|| { - Error::Config(format!( - "Workspace {} has no registered repository", - workspace.workspace_id - )) - })?; + if repositories.is_empty() { + return Err(Error::Config(format!( + "Workspace {} has no registered repository", + workspace.workspace_id + ))); + } let workspace_data_root = Self::default_workspace_backend_data_root(&workspace.workspace_id); - let primary_path = repository_local_path(&primary.source); - let workspace_root = primary_path - .clone() - .unwrap_or_else(|| workspace_data_root.clone()); + let workspace_root = workspace_data_root.clone(); let repositories = repositories .into_iter() .map(|repository| ConfiguredRepository { @@ -15077,9 +15070,9 @@ mod tests { scoped.repositories[0].source.kind, workspace_api::RepositorySourceKind::Https ); - assert_ne!( + assert_eq!( scoped.workspace_root, - PathBuf::from("https://example.test/org/repository.git") + ServerConfig::default_workspace_backend_data_root("remote-workspace") ); } diff --git a/crates/workspace-server/src/workspace_catalog.rs b/crates/workspace-server/src/workspace_catalog.rs index 1e6ff1cc..4bcafefd 100644 --- a/crates/workspace-server/src/workspace_catalog.rs +++ b/crates/workspace-server/src/workspace_catalog.rs @@ -1,4 +1,3 @@ -use std::path::{Path, PathBuf}; use std::sync::Arc; use chrono::{SecondsFormat, Utc}; @@ -6,7 +5,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use uuid::Uuid; -use workspace_api::{RepositoryObservedStatus, RepositorySource, RepositorySourceKind}; +use workspace_api::{RepositoryObservedStatus, RepositorySource}; use crate::repository_source::{parse_repository_source, repository_source_fingerprint}; use crate::store::{ @@ -200,53 +199,7 @@ fn normalize_required(field: &str, value: String, max_bytes: usize) -> Result Result { - let mut source = parse_repository_source(uri)?; - match source.kind { - RepositorySourceKind::LocalPath => { - let canonical = validate_local_git_path(Path::new(&source.uri))?; - source.uri = canonical.to_string_lossy().into_owned(); - } - RepositorySourceKind::File => { - let url = url::Url::parse(&source.uri).map_err(|error| { - Error::InvalidInput(format!("invalid file repository URI: {error}")) - })?; - let path = url.to_file_path().map_err(|_| { - Error::InvalidInput("file repository URI must contain an absolute path".to_string()) - })?; - let canonical = validate_local_git_path(&path)?; - source.uri = url::Url::from_file_path(canonical) - .map_err(|_| { - Error::InvalidInput("invalid canonical file repository path".to_string()) - })? - .to_string(); - } - RepositorySourceKind::Ssh | RepositorySourceKind::Http | RepositorySourceKind::Https => {} - RepositorySourceKind::Invalid => { - return Err(Error::InvalidInput( - "invalid initial repository source".to_string(), - )); - } - } - Ok(source) -} - -fn validate_local_git_path(path: &Path) -> Result { - let path = path.canonicalize().map_err(|error| { - Error::InvalidInput(format!("initial repository path is unavailable: {error}")) - })?; - if !path.is_dir() { - return Err(Error::InvalidInput( - "initial repository path must be a directory".to_string(), - )); - } - let normal_git = path.join(".git").exists(); - let bare_git = path.join("HEAD").is_file() && path.join("objects").is_dir(); - if !normal_git && !bare_git { - return Err(Error::InvalidInput( - "initial repository path is not a Git repository".to_string(), - )); - } - Ok(path) + parse_repository_source(uri) } fn workspace_create_fingerprint( @@ -283,6 +236,7 @@ fn workspace_create_fingerprint( mod tests { use super::*; use crate::store::SqliteWorkspaceStore; + use workspace_api::RepositorySourceKind; fn git_repository() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); @@ -407,13 +361,18 @@ mod tests { } #[test] - fn repository_intent_accepts_remote_and_rejects_non_git_local_paths() { + fn repository_intent_accepts_unavailable_local_sources_without_server_io() { let remote = validate_repository_source("https://example.test/repo.git").unwrap(); assert_eq!(remote.kind, RepositorySourceKind::Https); - let dir = tempfile::tempdir().unwrap(); - let non_git = validate_repository_source(&dir.path().display().to_string()).unwrap_err(); - assert!(non_git.to_string().contains("not a Git repository")); + let local = validate_repository_source("/runtime-only/missing/repository").unwrap(); + assert_eq!(local.kind, RepositorySourceKind::LocalPath); + assert_eq!(local.uri, "/runtime-only/missing/repository"); + + let file = validate_repository_source("file:///runtime-only/missing/repository").unwrap(); + assert_eq!(file.kind, RepositorySourceKind::File); + + assert!(validate_repository_source("relative/repository").is_err()); } #[test] diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index 43afa4f2..3f99df9b 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -269,6 +269,14 @@ export type RepositorySummary = { display_name: string; kind: string; provider: string; + source: { + kind: "local_path" | "file" | "ssh" | "http" | "https" | "invalid"; + uri: string; + }; + source_revision: number; + source_fingerprint: string; + observed_status: "unverified" | "ready" | "invalid"; + observed_at?: string | null; default_selector?: string | null; record_authority: string; git?: GitRepositorySummary | null; diff --git a/web/workspace/src/routes/+page.svelte b/web/workspace/src/routes/+page.svelte index bccc0c9c..807a51b9 100644 --- a/web/workspace/src/routes/+page.svelte +++ b/web/workspace/src/routes/+page.svelte @@ -155,7 +155,7 @@

New team space

Create Workspace

- Repository sources are interpreted by Backend authority. Supported Git sources are absolute local paths, file://, ssh://, http(s)://, and user@host:path; Browser-local paths and embedded credentials are not authority. + Repository sources are interpreted by Backend authority. Supported Git sources are absolute local paths, file://, ssh://, http(s)://, and user@host:path; Browser-local paths and embedded credentials are not authority. Plain HTTP is unencrypted, so prefer HTTPS or SSH.

diff --git a/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryId]/+page.svelte index 1434dcc2..c06eb9c8 100644 --- a/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryId]/+page.svelte @@ -16,7 +16,7 @@

{data.repository.item.display_name}

- {data.repository.item.git?.status ?? 'not observed'} + {data.repository.item.observed_status}
@@ -27,6 +27,18 @@
Provider
{data.repository.item.provider}
+
+
Source
+
{data.repository.item.source.kind} · {data.repository.item.source.uri}
+
+
+
Source revision
+
{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}
+
+
+
Observed
+
{data.repository.item.observed_at ?? 'not observed'}
+
Record authority
{data.repository.item.record_authority}