Move DB away from the project (#45036) (cherry-pick to preview) (#45156)

zed-zippy[bot] and Kirill Bulatov created

Cherry-pick of #45036 to preview

----
Follow-up of https://github.com/zed-industries/zed/pull/44887

This fixes remote server builds.

Additionally:

* slightly rewords workspace trust text in the security modal
* eagerly ask for worktree trust on open

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/collab/src/tests/remote_editing_collaboration_tests.rs |   2 
crates/editor/src/editor_tests.rs                             |   1 
crates/project/Cargo.toml                                     |   1 
crates/project/src/persistence.rs                             | 355 ----
crates/project/src/project.rs                                 |   1 
crates/project/src/trusted_worktrees.rs                       | 159 -
crates/remote_server/src/unix.rs                              |   3 
crates/workspace/src/persistence.rs                           | 158 ++
crates/workspace/src/security_modal.rs                        |   6 
crates/workspace/src/workspace.rs                             |  90 +
crates/zed/src/main.rs                                        |   9 
11 files changed, 295 insertions(+), 490 deletions(-)

Detailed changes

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;

crates/editor/src/editor_tests.rs 🔗

@@ -29341,6 +29341,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| {

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

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<Option<RemoteHostLocation>, HashSet<PathBuf>>,
-        trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
-    ) -> 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::<Vec<_>>();
-        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::<Vec<_>>()
-        {
-            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<Entity<WorktreeStore>>,
-        host: Option<RemoteHostLocation>,
-        cx: &App,
-    ) -> anyhow::Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
-        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<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
-            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::<SettingsStore>().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<Option<RemoteHostLocation>, HashSet<PathBuf>> =
-            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::<SettingsStore>().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::<SettingsStore>().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<Option<RemoteHostLocation>, HashSet<PathBuf>> =
-            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::<SettingsStore>().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(());
 }

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;

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<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
-    trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    serialization_task: Task<()>,
+    trusted_paths: TrustedPaths,
     restricted: HashSet<WorktreeId>,
     remote_host: Option<RemoteHostLocation>,
     restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
@@ -307,36 +296,16 @@ pub enum TrustedWorktreesEvent {
 
 impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 
+pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
+
 impl TrustedWorktreesStore {
     fn new(
+        trusted_paths: TrustedPaths,
         worktree_store: Option<Entity<WorktreeStore>>,
         remote_host: Option<RemoteHostLocation>,
         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::<Option<RemoteHostLocation>, HashSet<PathTrust>>::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<Self>,
+    ) -> (
+        HashSet<Option<RemoteHostLocation>>,
+        HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
+    ) {
+        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<TrustedWorktreesStore> {
         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")
         })

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);

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<Option<RemoteHostLocation>, HashSet<PathBuf>>,
+        trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
+    ) -> 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::<Vec<_>>();
+        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::<Vec<_>>()
+        {
+            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<Entity<WorktreeStore>>,
+        host: Option<RemoteHostLocation>,
+        cx: &App,
+    ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
+        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<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
+            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(

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()

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,
@@ -1188,6 +1188,7 @@ pub struct Workspace {
     _observe_current_user: Task<Result<()>>,
     _schedule_serialize_workspace: Option<Task<()>>,
     _schedule_serialize_ssh_paths: Option<Task<()>>,
+    _schedule_serialize_worktree_trust: Task<()>,
     pane_history_timestamp: Arc<AtomicUsize>,
     bounds: Bounds<Pixels>,
     pub centered_layout: bool,
@@ -1233,16 +1234,43 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        cx.observe_global::<SettingsStore>(|_, 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::<SettingsStore>(|_, 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 {
@@ -1254,11 +1282,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 => {
@@ -1544,6 +1586,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,
@@ -5982,12 +6025,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();
                     }
@@ -6508,6 +6553,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(

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();