diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index a66d7a1856c195a41a495123b468dc2b6ac8a1ca..5342b0bbd4b11afb24ccbaa6d4bf17df036cec76 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -859,9 +859,11 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m cx_a.update(|cx| { release_channel::init(semver::Version::new(0, 0, 0), cx); + project::trusted_worktrees::init(HashMap::default(), None, None, cx); }); server_cx.update(|cx| { release_channel::init(semver::Version::new(0, 0, 0), cx); + project::trusted_worktrees::init(HashMap::default(), None, None, cx); }); let mut server = TestServer::start(cx_a.executor().clone()).await; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f379b2b4e014ae7f51b5d8ffd842112dba54279b..84d3837d1cb516b6f70f6998ce588beed9bd9804 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -29370,6 +29370,7 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) { #[gpui::test] async fn test_local_worktree_trust(cx: &mut TestAppContext) { init_test(cx, |_| {}); + cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx)); cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index b589af2d50c77b68da6d94334904505f104b37e8..f39c368218511b6ddf560dda1198ef5c06bd0a2e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -40,7 +40,6 @@ clock.workspace = true collections.workspace = true context_server.workspace = true dap.workspace = true -db.workspace = true extension.workspace = true fancy-regex.workspace = true fs.workspace = true diff --git a/crates/project/src/persistence.rs b/crates/project/src/persistence.rs index be844c58384aa001fdbffa5fbac5dc513e98c535..5c4e664bdeba02a317da0610cf857e948bd5c93e 100644 --- a/crates/project/src/persistence.rs +++ b/crates/project/src/persistence.rs @@ -37,143 +37,7 @@ impl Domain for ProjectDb { db::static_connection!(PROJECT_DB, ProjectDb, []); -impl ProjectDb { - pub(crate) async fn save_trusted_worktrees( - &self, - trusted_worktrees: HashMap, HashSet>, - trusted_workspaces: HashSet>, - ) -> anyhow::Result<()> { - use anyhow::Context as _; - use db::sqlez::statement::Statement; - use itertools::Itertools as _; - - PROJECT_DB - .clear_trusted_worktrees() - .await - .context("clearing previous trust state")?; - - let trusted_worktrees = trusted_worktrees - .into_iter() - .flat_map(|(host, abs_paths)| { - abs_paths - .into_iter() - .map(move |abs_path| (Some(abs_path), host.clone())) - }) - .chain(trusted_workspaces.into_iter().map(|host| (None, host))) - .collect::>(); - let mut first_worktree; - let mut last_worktree = 0_usize; - for (count, placeholders) in std::iter::once("(?, ?, ?)") - .cycle() - .take(trusted_worktrees.len()) - .chunks(MAX_QUERY_PLACEHOLDERS / 3) - .into_iter() - .map(|chunk| { - let mut count = 0; - let placeholders = chunk - .inspect(|_| { - count += 1; - }) - .join(", "); - (count, placeholders) - }) - .collect::>() - { - first_worktree = last_worktree; - last_worktree = last_worktree + count; - let query = format!( - r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name) -VALUES {placeholders};"# - ); - - let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec(); - self.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = 1; - for (abs_path, host) in trusted_worktrees { - let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy()); - next_index = statement.bind( - &abs_path.as_ref().map(|abs_path| abs_path.as_ref()), - next_index, - )?; - next_index = statement.bind( - &host - .as_ref() - .and_then(|host| Some(host.user_name.as_ref()?.as_str())), - next_index, - )?; - next_index = statement.bind( - &host.as_ref().map(|host| host.host_identifier.as_str()), - next_index, - )?; - } - statement.exec() - }) - .await - .context("inserting new trusted state")?; - } - Ok(()) - } - - pub(crate) fn fetch_trusted_worktrees( - &self, - worktree_store: Option>, - host: Option, - cx: &App, - ) -> anyhow::Result, HashSet>> { - let trusted_worktrees = PROJECT_DB.trusted_worktrees()?; - Ok(trusted_worktrees - .into_iter() - .map(|(abs_path, user_name, host_name)| { - let db_host = match (user_name, host_name) { - (_, None) => None, - (None, Some(host_name)) => Some(RemoteHostLocation { - user_name: None, - host_identifier: SharedString::new(host_name), - }), - (Some(user_name), Some(host_name)) => Some(RemoteHostLocation { - user_name: Some(SharedString::new(user_name)), - host_identifier: SharedString::new(host_name), - }), - }; - - match abs_path { - Some(abs_path) => { - if db_host != host { - (db_host, PathTrust::AbsPath(abs_path)) - } else if let Some(worktree_store) = &worktree_store { - find_worktree_in_store(worktree_store.read(cx), &abs_path, cx) - .map(PathTrust::Worktree) - .map(|trusted_worktree| (host.clone(), trusted_worktree)) - .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path))) - } else { - (db_host, PathTrust::AbsPath(abs_path)) - } - } - None => (db_host, PathTrust::Workspace), - } - }) - .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| { - acc.entry(remote_host) - .or_insert_with(HashSet::default) - .insert(path_trust); - acc - })) - } - - query! { - fn trusted_worktrees() -> Result, Option, Option)>> { - SELECT absolute_path, user_name, host_name - FROM trusted_worktrees - } - } - - query! { - pub async fn clear_trusted_worktrees() -> Result<()> { - DELETE FROM trusted_worktrees - } - } -} +impl ProjectDb {} #[cfg(test)] mod tests { @@ -192,220 +56,5 @@ mod tests { trusted_worktrees::{PathTrust, RemoteHostLocation}, }; - static TEST_LOCK: Mutex<()> = Mutex::new(()); - - #[gpui::test] - async fn test_save_and_fetch_trusted_worktrees(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - let _guard = TEST_LOCK.lock().await; - PROJECT_DB.clear_trusted_worktrees().await.unwrap(); - cx.update(|cx| { - if cx.try_global::().is_none() { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - } - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/"), - json!({ - "project_a": { "main.rs": "" }, - "project_b": { "lib.rs": "" } - }), - ) - .await; - - let project = Project::test( - fs, - [path!("/project_a").as_ref(), path!("/project_b").as_ref()], - cx, - ) - .await; - let worktree_store = project.read_with(cx, |p, _| p.worktree_store()); - - let mut trusted_paths: HashMap, HashSet> = - HashMap::default(); - trusted_paths.insert( - None, - HashSet::from_iter([ - PathBuf::from(path!("/project_a")), - PathBuf::from(path!("/project_b")), - ]), - ); - - PROJECT_DB - .save_trusted_worktrees(trusted_paths, HashSet::default()) - .await - .unwrap(); - - let fetched = cx.update(|cx| { - PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx) - }); - let fetched = fetched.unwrap(); - - let local_trust = fetched.get(&None).expect("should have local host entry"); - assert_eq!(local_trust.len(), 2); - assert!( - local_trust - .iter() - .all(|p| matches!(p, PathTrust::Worktree(_))) - ); - - let fetched_no_store = cx - .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx)) - .unwrap(); - let local_trust_no_store = fetched_no_store - .get(&None) - .expect("should have local host entry"); - assert_eq!(local_trust_no_store.len(), 2); - assert!( - local_trust_no_store - .iter() - .all(|p| matches!(p, PathTrust::AbsPath(_))) - ); - } - - #[gpui::test] - async fn test_save_and_fetch_workspace_trust(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - let _guard = TEST_LOCK.lock().await; - PROJECT_DB.clear_trusted_worktrees().await.unwrap(); - cx.update(|cx| { - if cx.try_global::().is_none() { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - } - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({ "main.rs": "" })) - .await; - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |p, _| p.worktree_store()); - - let trusted_workspaces = HashSet::from_iter([None]); - PROJECT_DB - .save_trusted_worktrees(HashMap::default(), trusted_workspaces) - .await - .unwrap(); - - let fetched = cx.update(|cx| { - PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx) - }); - let fetched = fetched.unwrap(); - - let local_trust = fetched.get(&None).expect("should have local host entry"); - assert!(local_trust.contains(&PathTrust::Workspace)); - - let fetched_no_store = cx - .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx)) - .unwrap(); - let local_trust_no_store = fetched_no_store - .get(&None) - .expect("should have local host entry"); - assert!(local_trust_no_store.contains(&PathTrust::Workspace)); - } - - #[gpui::test] - async fn test_save_and_fetch_remote_host_trust(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - let _guard = TEST_LOCK.lock().await; - PROJECT_DB.clear_trusted_worktrees().await.unwrap(); - cx.update(|cx| { - if cx.try_global::().is_none() { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - } - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({ "main.rs": "" })) - .await; - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |p, _| p.worktree_store()); - - let remote_host = Some(RemoteHostLocation { - user_name: Some(SharedString::from("testuser")), - host_identifier: SharedString::from("remote.example.com"), - }); - - let mut trusted_paths: HashMap, HashSet> = - HashMap::default(); - trusted_paths.insert( - remote_host.clone(), - HashSet::from_iter([PathBuf::from("/home/testuser/project")]), - ); - - PROJECT_DB - .save_trusted_worktrees(trusted_paths, HashSet::default()) - .await - .unwrap(); - - let fetched = cx.update(|cx| { - PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx) - }); - let fetched = fetched.unwrap(); - - let remote_trust = fetched - .get(&remote_host) - .expect("should have remote host entry"); - assert_eq!(remote_trust.len(), 1); - assert!(remote_trust - .iter() - .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project")))); - - let fetched_no_store = cx - .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx)) - .unwrap(); - let remote_trust_no_store = fetched_no_store - .get(&remote_host) - .expect("should have remote host entry"); - assert_eq!(remote_trust_no_store.len(), 1); - assert!(remote_trust_no_store - .iter() - .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project")))); - } - - #[gpui::test] - async fn test_clear_trusted_worktrees(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - let _guard = TEST_LOCK.lock().await; - PROJECT_DB.clear_trusted_worktrees().await.unwrap(); - cx.update(|cx| { - if cx.try_global::().is_none() { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - } - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({ "main.rs": "" })) - .await; - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |p, _| p.worktree_store()); - - let trusted_workspaces = HashSet::from_iter([None]); - PROJECT_DB - .save_trusted_worktrees(HashMap::default(), trusted_workspaces) - .await - .unwrap(); - - PROJECT_DB.clear_trusted_worktrees().await.unwrap(); - - let fetched = cx.update(|cx| { - PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx) - }); - let fetched = fetched.unwrap(); - - assert!(fetched.is_empty(), "should be empty after clear"); - - let fetched_no_store = cx - .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx)) - .unwrap(); - assert!(fetched_no_store.is_empty(), "should be empty after clear"); - } + static TEST_WORKTREE_TRUST_LOCK: Mutex<()> = Mutex::new(()); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 79d37f0e99f35f5c059a98017f5036e95e18bf01..bbf91eb3c18a53f32f48f1a044c991bf5cfd9fdf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -10,7 +10,6 @@ pub mod image_store; pub mod lsp_command; pub mod lsp_store; mod manifest_tree; -mod persistence; pub mod prettier_store; mod project_search; pub mod project_settings; diff --git a/crates/project/src/trusted_worktrees.rs b/crates/project/src/trusted_worktrees.rs index 733e8d48294b863f8bf35cdb1ea458acd59dcadb..d9f5d4a7a43d8cb8b8220f4d3de8ca35d366a8f5 100644 --- a/crates/project/src/trusted_worktrees.rs +++ b/crates/project/src/trusted_worktrees.rs @@ -68,19 +68,21 @@ use util::debug_panic; use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore}; -#[cfg(not(any(test, feature = "test-support")))] -use crate::persistence::PROJECT_DB; -#[cfg(not(any(test, feature = "test-support")))] -use util::ResultExt as _; - pub fn init( + db_trusted_paths: TrustedPaths, downstream_client: Option<(AnyProtoClient, u64)>, upstream_client: Option<(AnyProtoClient, u64)>, cx: &mut App, ) { if TrustedWorktrees::try_get_global(cx).is_none() { - let trusted_worktrees = cx.new(|cx| { - TrustedWorktreesStore::new(None, None, downstream_client, upstream_client, cx) + let trusted_worktrees = cx.new(|_| { + TrustedWorktreesStore::new( + db_trusted_paths, + None, + None, + downstream_client, + upstream_client, + ) }); cx.set_global(TrustedWorktrees(trusted_worktrees)) } @@ -126,18 +128,7 @@ pub fn track_worktree_trust( } }); } - None => { - let trusted_worktrees = cx.new(|cx| { - TrustedWorktreesStore::new( - Some(worktree_store.clone()), - remote_host, - downstream_client, - upstream_client, - cx, - ) - }); - cx.set_global(TrustedWorktrees(trusted_worktrees)) - } + None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"), } } @@ -212,9 +203,7 @@ pub struct TrustedWorktreesStore { downstream_client: Option<(AnyProtoClient, u64)>, upstream_client: Option<(AnyProtoClient, u64)>, worktree_stores: HashMap, Option>, - trusted_paths: HashMap, HashSet>, - #[cfg(not(any(test, feature = "test-support")))] - serialization_task: Task<()>, + trusted_paths: TrustedPaths, restricted: HashSet, remote_host: Option, restricted_workspaces: HashSet>, @@ -307,36 +296,16 @@ pub enum TrustedWorktreesEvent { impl EventEmitter for TrustedWorktreesStore {} +pub type TrustedPaths = HashMap, HashSet>; + impl TrustedWorktreesStore { fn new( + trusted_paths: TrustedPaths, worktree_store: Option>, remote_host: Option, downstream_client: Option<(AnyProtoClient, u64)>, upstream_client: Option<(AnyProtoClient, u64)>, - cx: &App, ) -> Self { - #[cfg(any(test, feature = "test-support"))] - let _ = cx; - - #[cfg(not(any(test, feature = "test-support")))] - let trusted_paths = if downstream_client.is_none() { - match PROJECT_DB.fetch_trusted_worktrees( - worktree_store.clone(), - remote_host.clone(), - cx, - ) { - Ok(trusted_paths) => trusted_paths, - Err(e) => { - log::error!("Failed to do initial trusted worktrees fetch: {e:#}"); - HashMap::default() - } - } - } else { - HashMap::default() - }; - #[cfg(any(test, feature = "test-support"))] - let trusted_paths = HashMap::, HashSet>::default(); - if let Some((upstream_client, upstream_project_id)) = &upstream_client { let trusted_paths = trusted_paths .iter() @@ -366,8 +335,6 @@ impl TrustedWorktreesStore { remote_host, restricted_workspaces: HashSet::default(), restricted: HashSet::default(), - #[cfg(not(any(test, feature = "test-support")))] - serialization_task: Task::ready(()), worktree_stores, } } @@ -496,41 +463,6 @@ impl TrustedWorktreesStore { trusted_paths.clone(), )); - #[cfg(not(any(test, feature = "test-support")))] - if self.downstream_client.is_none() { - let mut new_trusted_workspaces = HashSet::default(); - let new_trusted_worktrees = self - .trusted_paths - .clone() - .into_iter() - .map(|(host, paths)| { - let abs_paths = paths - .into_iter() - .flat_map(|path| match path { - PathTrust::Worktree(worktree_id) => self - .find_worktree_data(worktree_id, cx) - .map(|(abs_path, ..)| abs_path.to_path_buf()), - PathTrust::AbsPath(abs_path) => Some(abs_path), - PathTrust::Workspace => { - new_trusted_workspaces.insert(host.clone()); - None - } - }) - .collect(); - (host, abs_paths) - }) - .collect(); - // Do not persist auto trusted worktrees - if !ProjectSettings::get_global(cx).session.trust_all_worktrees { - self.serialization_task = cx.background_spawn(async move { - PROJECT_DB - .save_trusted_worktrees(new_trusted_worktrees, new_trusted_workspaces) - .await - .log_err(); - }); - } - } - if let Some((upstream_client, upstream_project_id)) = &self.upstream_client { let trusted_paths = trusted_paths .iter() @@ -578,31 +510,8 @@ impl TrustedWorktreesStore { /// Erases all trust information. /// Requires Zed's restart to take proper effect. - pub fn clear_trusted_paths(&mut self, cx: &App) -> Task<()> { - if self.downstream_client.is_none() { - self.trusted_paths.clear(); - - #[cfg(not(any(test, feature = "test-support")))] - { - let (tx, rx) = smol::channel::bounded(1); - self.serialization_task = cx.background_spawn(async move { - PROJECT_DB.clear_trusted_worktrees().await.log_err(); - tx.send(()).await.ok(); - }); - - return cx.background_spawn(async move { - rx.recv().await.ok(); - }); - } - - #[cfg(any(test, feature = "test-support"))] - { - let _ = cx; - Task::ready(()) - } - } else { - Task::ready(()) - } + pub fn clear_trusted_paths(&mut self) { + self.trusted_paths.clear(); } /// Checks whether a certain worktree is trusted (or on a larger trust level). @@ -785,6 +694,39 @@ impl TrustedWorktreesStore { } } + /// Returns a normalized representation of the trusted paths to store in the DB. + pub fn trusted_paths_for_serialization( + &mut self, + cx: &mut Context, + ) -> ( + HashSet>, + HashMap, HashSet>, + ) { + let mut new_trusted_workspaces = HashSet::default(); + let new_trusted_worktrees = self + .trusted_paths + .clone() + .into_iter() + .map(|(host, paths)| { + let abs_paths = paths + .into_iter() + .flat_map(|path| match path { + PathTrust::Worktree(worktree_id) => self + .find_worktree_data(worktree_id, cx) + .map(|(abs_path, ..)| abs_path.to_path_buf()), + PathTrust::AbsPath(abs_path) => Some(abs_path), + PathTrust::Workspace => { + new_trusted_workspaces.insert(host.clone()); + None + } + }) + .collect(); + (host, abs_paths) + }) + .collect(); + (new_trusted_workspaces, new_trusted_worktrees) + } + fn find_worktree_data( &mut self, worktree_id: WorktreeId, @@ -841,7 +783,7 @@ impl TrustedWorktreesStore { } } -pub(crate) fn find_worktree_in_store( +pub fn find_worktree_in_store( worktree_store: &WorktreeStore, abs_path: &Path, cx: &App, @@ -885,6 +827,7 @@ mod tests { cx: &mut TestAppContext, ) -> Entity { cx.update(|cx| { + init(HashMap::default(), None, None, cx); track_worktree_trust(worktree_store, None, None, None, cx); TrustedWorktrees::try_get_global(cx).expect("global should be set") }) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index f4f769474b39f182ee90cbaf146e09eab440dc15..af603998171e19d4776d47479ff81aa08d26d258 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -2,6 +2,7 @@ use crate::HeadlessProject; use crate::headless_project::HeadlessAppState; use anyhow::{Context as _, Result, anyhow}; use client::ProxySettings; +use collections::HashMap; use project::trusted_worktrees; use util::ResultExt; @@ -419,7 +420,7 @@ pub fn execute_run( log::info!("gpui app started, initializing server"); let session = start_server(listeners, log_rx, cx, is_wsl_interop); - trusted_worktrees::init(Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx); + trusted_worktrees::init(HashMap::default(), Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx); GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); git_hosting_providers::init(cx); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index f1835caf8dd84e1f729e0415b5711ffa69981d9b..dc113db68e33dc527e6b8d2cb66f644bcd83b661 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,14 +9,18 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use collections::{HashMap, IndexSet}; +use collections::{HashMap, HashSet, IndexSet}; use db::{ query, sqlez::{connection::Connection, domain::Domain}, sqlez_macros::sql, }; -use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; -use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; +use gpui::{Axis, Bounds, Entity, Task, WindowBounds, WindowId, point, size}; +use project::{ + debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}, + trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store}, + worktree_store::WorktreeStore, +}; use language::{LanguageName, Toolchain, ToolchainScope}; use project::WorktreeId; @@ -46,6 +50,11 @@ use model::{ use self::model::{DockStructure, SerializedWorkspaceLocation}; +// https://www.sqlite.org/limits.html +// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, +// > which defaults to <..> 32766 for SQLite versions after 3.32.0. +const MAX_QUERY_PLACEHOLDERS: usize = 32000; + #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); impl sqlez::bindable::StaticColumnCount for SerializedAxis {} @@ -708,6 +717,14 @@ impl Domain for WorkspaceDb { ALTER TABLE remote_connections ADD COLUMN name TEXT; ALTER TABLE remote_connections ADD COLUMN container_id TEXT; ), + sql!( + CREATE TABLE IF NOT EXISTS trusted_worktrees ( + trust_id INTEGER PRIMARY KEY AUTOINCREMENT, + absolute_path TEXT, + user_name TEXT, + host_name TEXT + ) STRICT; + ), ]; // Allow recovering from bad migration that was initially shipped to nightly @@ -1796,6 +1813,141 @@ impl WorkspaceDb { Ok(()) }).await } + + pub(crate) async fn save_trusted_worktrees( + &self, + trusted_worktrees: HashMap, HashSet>, + trusted_workspaces: HashSet>, + ) -> anyhow::Result<()> { + use anyhow::Context as _; + use db::sqlez::statement::Statement; + use itertools::Itertools as _; + + DB.clear_trusted_worktrees() + .await + .context("clearing previous trust state")?; + + let trusted_worktrees = trusted_worktrees + .into_iter() + .flat_map(|(host, abs_paths)| { + abs_paths + .into_iter() + .map(move |abs_path| (Some(abs_path), host.clone())) + }) + .chain(trusted_workspaces.into_iter().map(|host| (None, host))) + .collect::>(); + let mut first_worktree; + let mut last_worktree = 0_usize; + for (count, placeholders) in std::iter::once("(?, ?, ?)") + .cycle() + .take(trusted_worktrees.len()) + .chunks(MAX_QUERY_PLACEHOLDERS / 3) + .into_iter() + .map(|chunk| { + let mut count = 0; + let placeholders = chunk + .inspect(|_| { + count += 1; + }) + .join(", "); + (count, placeholders) + }) + .collect::>() + { + first_worktree = last_worktree; + last_worktree = last_worktree + count; + let query = format!( + r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name) +VALUES {placeholders};"# + ); + + let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec(); + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = 1; + for (abs_path, host) in trusted_worktrees { + let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy()); + next_index = statement.bind( + &abs_path.as_ref().map(|abs_path| abs_path.as_ref()), + next_index, + )?; + next_index = statement.bind( + &host + .as_ref() + .and_then(|host| Some(host.user_name.as_ref()?.as_str())), + next_index, + )?; + next_index = statement.bind( + &host.as_ref().map(|host| host.host_identifier.as_str()), + next_index, + )?; + } + statement.exec() + }) + .await + .context("inserting new trusted state")?; + } + Ok(()) + } + + pub fn fetch_trusted_worktrees( + &self, + worktree_store: Option>, + host: Option, + cx: &App, + ) -> Result, HashSet>> { + let trusted_worktrees = DB.trusted_worktrees()?; + Ok(trusted_worktrees + .into_iter() + .map(|(abs_path, user_name, host_name)| { + let db_host = match (user_name, host_name) { + (_, None) => None, + (None, Some(host_name)) => Some(RemoteHostLocation { + user_name: None, + host_identifier: SharedString::new(host_name), + }), + (Some(user_name), Some(host_name)) => Some(RemoteHostLocation { + user_name: Some(SharedString::new(user_name)), + host_identifier: SharedString::new(host_name), + }), + }; + + match abs_path { + Some(abs_path) => { + if db_host != host { + (db_host, PathTrust::AbsPath(abs_path)) + } else if let Some(worktree_store) = &worktree_store { + find_worktree_in_store(worktree_store.read(cx), &abs_path, cx) + .map(PathTrust::Worktree) + .map(|trusted_worktree| (host.clone(), trusted_worktree)) + .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path))) + } else { + (db_host, PathTrust::AbsPath(abs_path)) + } + } + None => (db_host, PathTrust::Workspace), + } + }) + .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| { + acc.entry(remote_host) + .or_insert_with(HashSet::default) + .insert(path_trust); + acc + })) + } + + query! { + fn trusted_worktrees() -> Result, Option, Option)>> { + SELECT absolute_path, user_name, host_name + FROM trusted_worktrees + } + } + + query! { + pub async fn clear_trusted_worktrees() -> Result<()> { + DELETE FROM trusted_worktrees + } + } } pub fn delete_unloaded_items( diff --git a/crates/workspace/src/security_modal.rs b/crates/workspace/src/security_modal.rs index f2a94ad81661a2572f35d1d746b04b31fa24f00c..44242a3588017aabf117300310dd10a4a28b292f 100644 --- a/crates/workspace/src/security_modal.rs +++ b/crates/workspace/src/security_modal.rs @@ -131,14 +131,14 @@ impl Render for SecurityModal { None => match &restricted_path.host { Some(remote_host) => match &remote_host.user_name { Some(user_name) => format!( - "Empty project ({}@{})", + "Workspace trust ({}@{})", user_name, remote_host.host_identifier ), None => { - format!("Empty project ({})", remote_host.host_identifier) + format!("Workspace trust ({})", remote_host.host_identifier) } }, - None => "Empty project".to_string(), + None => "Workspace trust".to_string(), }, }; h_flex() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 34593f5bc8f6af3b9cbac87e8fbff50d3f954a95..00412cfb75fce58b19a697e283f77c5a57ebb683 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -80,7 +80,7 @@ use project::{ debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, project_settings::ProjectSettings, toolchain_store::ToolchainStoreEvent, - trusted_worktrees::TrustedWorktrees, + trusted_worktrees::{TrustedWorktrees, TrustedWorktreesEvent}, }; use remote::{ RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, @@ -1184,6 +1184,7 @@ pub struct Workspace { _observe_current_user: Task>, _schedule_serialize_workspace: Option>, _schedule_serialize_ssh_paths: Option>, + _schedule_serialize_worktree_trust: Task<()>, pane_history_timestamp: Arc, bounds: Bounds, pub centered_layout: bool, @@ -1229,16 +1230,43 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Self { - cx.observe_global::(|_, cx| { - if ProjectSettings::get_global(cx).session.trust_all_worktrees { - if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { - trusted_worktrees.update(cx, |trusted_worktrees, cx| { - trusted_worktrees.auto_trust_all(cx); - }) + if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { + cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| { + if let TrustedWorktreesEvent::Trusted(..) = e { + let (new_trusted_workspaces, new_trusted_worktrees) = worktrees_store + .update(cx, |worktrees_store, cx| { + worktrees_store.trusted_paths_for_serialization(cx) + }); + // Do not persist auto trusted worktrees + if !ProjectSettings::get_global(cx).session.trust_all_worktrees { + let timeout = cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME); + workspace._schedule_serialize_worktree_trust = + cx.background_spawn(async move { + timeout.await; + persistence::DB + .save_trusted_worktrees( + new_trusted_worktrees, + new_trusted_workspaces, + ) + .await + .log_err(); + }); + } } - } - }) - .detach(); + }) + .detach(); + + cx.observe_global::(|_, cx| { + if ProjectSettings::get_global(cx).session.trust_all_worktrees { + if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { + trusted_worktrees.update(cx, |trusted_worktrees, cx| { + trusted_worktrees.auto_trust_all(cx); + }) + } + } + }) + .detach(); + } cx.subscribe_in(&project, window, move |this, _, event, window, cx| { match event { @@ -1250,11 +1278,25 @@ impl Workspace { this.collaborator_left(*peer_id, window, cx); } - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { - this.update_window_title(window, cx); - this.serialize_workspace(window, cx); - // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. - this.update_history(cx); + project::Event::WorktreeUpdatedEntries(worktree_id, _) => { + if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { + trusted_worktrees.update(cx, |trusted_worktrees, cx| { + trusted_worktrees.can_trust(*worktree_id, cx); + }); + } + } + + project::Event::WorktreeRemoved(_) => { + this.update_worktree_data(window, cx); + } + + project::Event::WorktreeAdded(worktree_id) => { + if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { + trusted_worktrees.update(cx, |trusted_worktrees, cx| { + trusted_worktrees.can_trust(*worktree_id, cx); + }); + } + this.update_worktree_data(window, cx); } project::Event::DisconnectedFromHost => { @@ -1540,6 +1582,7 @@ impl Workspace { _apply_leader_updates, _schedule_serialize_workspace: None, _schedule_serialize_ssh_paths: None, + _schedule_serialize_worktree_trust: Task::ready(()), leader_updates_tx, _subscriptions: subscriptions, pane_history_timestamp, @@ -5970,12 +6013,14 @@ impl Workspace { .on_action( cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| { if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { - let clear_task = trusted_worktrees.update(cx, |trusted_worktrees, cx| { - trusted_worktrees.clear_trusted_paths(cx) + trusted_worktrees.update(cx, |trusted_worktrees, _| { + trusted_worktrees.clear_trusted_paths() }); + let clear_task = persistence::DB.clear_trusted_worktrees(); cx.spawn(async move |_, cx| { - clear_task.await; - cx.update(|cx| reload(cx)).ok(); + if clear_task.await.log_err().is_some() { + cx.update(|cx| reload(cx)).ok(); + } }) .detach(); } @@ -6496,6 +6541,13 @@ impl Workspace { } } } + + fn update_worktree_data(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) { + self.update_window_title(window, cx); + self.serialize_workspace(window, cx); + // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. + self.update_history(cx); + } } fn leader_border_for_pane( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 353baba02cca9b0060a647f438fa8be4e81e9142..c8137a71c0f2a8524f6310d7cd711978ed833d1a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -407,7 +407,14 @@ pub fn main() { }); app.run(move |cx| { - trusted_worktrees::init(None, None, cx); + let trusted_paths = match workspace::WORKSPACE_DB.fetch_trusted_worktrees(None, None, cx) { + Ok(trusted_paths) => trusted_paths, + Err(e) => { + log::error!("Failed to do initial trusted worktrees fetch: {e:#}"); + HashMap::default() + } + }; + trusted_worktrees::init(trusted_paths, None, None, cx); menu::init(); zed_actions::init();