fix: defer local repository validation to runtimes
This commit is contained in:
@@ -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<Command, CliError> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -271,22 +271,15 @@ impl ServerConfig {
|
||||
workspace: &WorkspaceRecord,
|
||||
repositories: Vec<RepositoryRecord>,
|
||||
) -> Result<Self> {
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<St
|
||||
}
|
||||
|
||||
fn validate_repository_source(uri: &str) -> Result<RepositorySource> {
|
||||
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<PathBuf> {
|
||||
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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
<p class="workspace-catalog-eyebrow">New team space</p>
|
||||
<h2 id="workspace-create-title">Create Workspace</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<form onsubmit={submitCreation}>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div>
|
||||
<h3>{data.repository.item.display_name}</h3>
|
||||
</div>
|
||||
<span class="status-pill" class:warn={data.repository.item.git?.status !== 'clean'}>{data.repository.item.git?.status ?? 'not observed'}</span>
|
||||
<span class="status-pill" class:warn={data.repository.item.observed_status !== 'ready'}>{data.repository.item.observed_status}</span>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
@@ -27,6 +27,18 @@
|
||||
<dt>Provider</dt>
|
||||
<dd>{data.repository.item.provider}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>{data.repository.item.source.kind} · {data.repository.item.source.uri}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source revision</dt>
|
||||
<dd>{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Observed</dt>
|
||||
<dd>{data.repository.item.observed_at ?? 'not observed'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Record authority</dt>
|
||||
<dd>{data.repository.item.record_authority}</dd>
|
||||
|
||||
Reference in New Issue
Block a user