From f74f3cd133ed9809f432f7f8b1a233e5e26e466d Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 3 Sep 2026 02:44:24 +0900 Subject: [PATCH] fix: reject conflicting repository import intent --- crates/workspace-server/src/server.rs | 41 ++++++++++ crates/workspace-server/src/store.rs | 107 ++++++++++++++++++-------- 2 files changed, 116 insertions(+), 32 deletions(-) diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 55dddf93..2ba7ae69 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -18175,6 +18175,47 @@ mod tests { SqliteWorkspaceStore::open(&config.database_path).unwrap() } + #[tokio::test] + async fn configured_repository_import_rejects_changed_source_intent() { + let dir = tempfile::tempdir().unwrap(); + let mut config = test_server_config(dir.path()); + let store = test_control_store(&config); + seed_test_registered_workspace(&store, &config) + .await + .unwrap(); + + import_configured_repositories(&store, &config).unwrap(); + let persisted = store + .get_repository_by_key(TEST_WORKSPACE_ID, "test-repository") + .unwrap() + .unwrap(); + assert_eq!(persisted.source, config.repositories[0].source); + + let changed_source = workspace_api::RepositorySource { + kind: workspace_api::RepositorySourceKind::LocalPath, + uri: dir.path().join("different-source").display().to_string(), + }; + config.repositories[0].source_fingerprint = + crate::repository_source::repository_source_fingerprint(&changed_source); + config.repositories[0].source = changed_source; + + let error = import_configured_repositories(&store, &config).unwrap_err(); + assert!( + matches!(error, crate::Error::WorkspaceConfigConflict(_)), + "unexpected error: {error}" + ); + let after_rejected_import = store + .get_repository_by_key(TEST_WORKSPACE_ID, "test-repository") + .unwrap() + .unwrap(); + assert_eq!(after_rejected_import.repository_id, persisted.repository_id); + assert_eq!(after_rejected_import.source, persisted.source); + assert_eq!( + after_rejected_import.source_fingerprint, + persisted.source_fingerprint + ); + } + #[tokio::test] async fn server_router_serves_workspace_chooser_before_first_workspace_exists() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index 29397463..558c784d 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -2197,38 +2197,67 @@ impl ControlPlaneStore for SqliteWorkspaceStore { fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> { validate_repository_record_identity(record)?; - self.with_conn(|conn| { - conn.execute( - r#"INSERT INTO repositories ( - workspace_id, repository_id, repository_key, kind, provider, uri, - source_kind, source_uri, default_ref, source_revision, - source_fingerprint, observed_status, observed_at, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15) - ON CONFLICT(workspace_id, repository_key) DO UPDATE SET - kind = excluded.kind, - provider = excluded.provider, - default_ref = excluded.default_ref, - observed_status = excluded.observed_status, - observed_at = excluded.observed_at, - updated_at = excluded.updated_at"#, - params![ - record.workspace_id, - record.repository_id, - record.repository_key, - record.kind, - record.provider, - record.source.uri, - record.source.kind.as_str(), - record.source.uri, - record.default_ref, - record.source_revision, - record.source_fingerprint, - record.observed_status.as_str(), - record.observed_at, - record.created_at, - record.updated_at, - ], - )?; + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let existing = tx + .query_row( + r#"SELECT workspace_id, repository_id, repository_key, kind, provider, + source_kind, source_uri, default_ref, source_revision, + source_fingerprint, observed_status, observed_at, created_at, updated_at + FROM repositories + WHERE workspace_id = ?1 AND repository_key = ?2"#, + params![record.workspace_id, record.repository_key], + read_repository_record, + ) + .optional()?; + if let Some(existing) = existing { + if !repository_registration_intent_matches(&existing, record) { + return Err(Error::WorkspaceConfigConflict(format!( + "Repository key `{}` already exists with different registration intent", + record.repository_key + ))); + } + tx.execute( + r#"UPDATE repositories + SET observed_status = ?3, + observed_at = ?4, + updated_at = ?5 + WHERE workspace_id = ?1 AND repository_id = ?2"#, + params![ + record.workspace_id, + existing.repository_id, + record.observed_status.as_str(), + record.observed_at, + record.updated_at, + ], + )?; + } else { + tx.execute( + r#"INSERT INTO repositories ( + workspace_id, repository_id, repository_key, kind, provider, uri, + source_kind, source_uri, default_ref, source_revision, + source_fingerprint, observed_status, observed_at, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)"#, + params![ + record.workspace_id, + record.repository_id, + record.repository_key, + record.kind, + record.provider, + record.source.uri, + record.source.kind.as_str(), + record.source.uri, + record.default_ref, + record.source_revision, + record.source_fingerprint, + record.observed_status.as_str(), + record.observed_at, + record.created_at, + record.updated_at, + ], + )?; + } + tx.commit()?; Ok(()) }) } @@ -5358,6 +5387,20 @@ fn read_workspace_record(row: &rusqlite::Row<'_>) -> rusqlite::Result bool { + existing.workspace_id == requested.workspace_id + && existing.repository_key == requested.repository_key + && existing.kind == requested.kind + && existing.provider == requested.provider + && existing.source == requested.source + && existing.default_ref == requested.default_ref + && existing.source_revision == requested.source_revision + && existing.source_fingerprint == requested.source_fingerprint +} + fn validate_repository_record_identity(record: &RepositoryRecord) -> Result<()> { workspace_api::validate_repository_key(&record.repository_key) .map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?;