Distinguish worktree trust hosts by project ids too

Kirill Bulatov created

Change summary

crates/collab/src/tests/remote_editing_collaboration_tests.rs |  1 
crates/git_ui/src/worktree_picker.rs                          | 16 +
crates/project/src/project.rs                                 | 13 +
crates/project/src/trusted_worktrees.rs                       | 41 ++--
crates/remote_server/src/headless_project.rs                  |  3 
crates/remote_server/src/unix.rs                              |  4 
crates/workspace/src/persistence.rs                           | 36 ++-
crates/workspace/src/workspace.rs                             | 12 +
8 files changed, 81 insertions(+), 45 deletions(-)

Detailed changes

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -994,6 +994,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
     let remote_host = project_a.read_with(cx_a, |project, cx| {
         project
             .remote_connection_options(cx)
+            .map(|options| (0, options))
             .map(RemoteHostLocation::from)
     });
 

crates/git_ui/src/worktree_picker.rs 🔗

@@ -271,16 +271,24 @@ impl WorktreeListDelegate {
                     if let Some((parent_worktree, _)) =
                         project.read(cx).find_worktree(repo_path, cx)
                     {
+                        let remote_host = project.read_with(cx, |project, cx| {
+                            project
+                                .lsp_store()
+                                .read(cx)
+                                .downstream_client()
+                                .or_else(|| project.lsp_store().read(cx).upstream_client())
+                                .map(|(_, project_id)| project_id)
+                                .zip(project.remote_connection_options(cx))
+                                .map(RemoteHostLocation::from)
+                        });
+
                         trusted_worktrees.update(cx, |trusted_worktrees, cx| {
                             if trusted_worktrees.can_trust(parent_worktree.read(cx).id(), cx) {
                                 trusted_worktrees.trust(
                                     HashSet::from_iter([PathTrust::AbsPath(
                                         new_worktree_path.clone(),
                                     )]),
-                                    project
-                                        .read(cx)
-                                        .remote_connection_options(cx)
-                                        .map(RemoteHostLocation::from),
+                                    remote_host,
                                     cx,
                                 );
                             }

crates/project/src/project.rs 🔗

@@ -52,7 +52,9 @@ pub use project_search::Search;
 
 use anyhow::{Context as _, Result, anyhow};
 use buffer_store::{BufferStore, BufferStoreEvent};
-use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto};
+use client::{
+    Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto,
+};
 use clock::ReplicaId;
 
 use dap::client::DebugAdapterClient;
@@ -1295,9 +1297,12 @@ impl Project {
             if init_worktree_trust {
                 trusted_worktrees::track_worktree_trust(
                     worktree_store.clone(),
-                    Some(RemoteHostLocation::from(connection_options)),
+                    Some(RemoteHostLocation::from((
+                        REMOTE_SERVER_PROJECT_ID,
+                        connection_options,
+                    ))),
                     None,
-                    Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
+                    Some((remote_proto.clone(), ProjectId(REMOTE_SERVER_PROJECT_ID))),
                     cx,
                 );
             }
@@ -4872,6 +4877,7 @@ impl Project {
             let remote_host = this
                 .read(cx)
                 .remote_connection_options(cx)
+                .map(|options| (envelope.payload.project_id, options))
                 .map(RemoteHostLocation::from);
             trusted_worktrees.trust(
                 envelope
@@ -4906,6 +4912,7 @@ impl Project {
             let remote_host = this
                 .read(cx)
                 .remote_connection_options(cx)
+                .map(|options| (envelope.payload.project_id, options))
                 .map(RemoteHostLocation::from);
             trusted_worktrees.restrict(restricted_paths, remote_host, cx);
         })?;

crates/project/src/trusted_worktrees.rs 🔗

@@ -39,6 +39,7 @@
 //! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
 //! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
 
+use client::ProjectId;
 use collections::{HashMap, HashSet};
 use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, WeakEntity};
 use remote::RemoteConnectionOptions;
@@ -54,8 +55,8 @@ use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
 
 pub fn init(
     db_trusted_paths: TrustedPaths,
-    downstream_client: Option<(AnyProtoClient, u64)>,
-    upstream_client: Option<(AnyProtoClient, u64)>,
+    downstream_client: Option<(AnyProtoClient, ProjectId)>,
+    upstream_client: Option<(AnyProtoClient, ProjectId)>,
     cx: &mut App,
 ) {
     if TrustedWorktrees::try_get_global(cx).is_none() {
@@ -76,8 +77,8 @@ pub fn init(
 pub fn track_worktree_trust(
     worktree_store: Entity<WorktreeStore>,
     remote_host: Option<RemoteHostLocation>,
-    downstream_client: Option<(AnyProtoClient, u64)>,
-    upstream_client: Option<(AnyProtoClient, u64)>,
+    downstream_client: Option<(AnyProtoClient, ProjectId)>,
+    upstream_client: Option<(AnyProtoClient, ProjectId)>,
     cx: &mut App,
 ) {
     match TrustedWorktrees::try_get_global(cx) {
@@ -103,7 +104,7 @@ pub fn track_worktree_trust(
                         if !trusted_paths.is_empty() {
                             upstream_client
                                 .send(proto::TrustWorktrees {
-                                    project_id: *upstream_project_id,
+                                    project_id: upstream_project_id.0,
                                     trusted_paths,
                                 })
                                 .ok();
@@ -133,8 +134,8 @@ impl TrustedWorktrees {
 /// Emits an event each time the worktree was checked and found not trusted,
 /// or a certain worktree had been trusted.
 pub struct TrustedWorktreesStore {
-    downstream_client: Option<(AnyProtoClient, u64)>,
-    upstream_client: Option<(AnyProtoClient, u64)>,
+    downstream_client: Option<(AnyProtoClient, ProjectId)>,
+    upstream_client: Option<(AnyProtoClient, ProjectId)>,
     worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
     trusted_paths: TrustedPaths,
     restricted: HashSet<WorktreeId>,
@@ -147,10 +148,11 @@ pub struct TrustedWorktreesStore {
 pub struct RemoteHostLocation {
     pub user_name: Option<SharedString>,
     pub host_identifier: SharedString,
+    pub project_id: ProjectId,
 }
 
-impl From<RemoteConnectionOptions> for RemoteHostLocation {
-    fn from(options: RemoteConnectionOptions) -> Self {
+impl From<(u64, RemoteConnectionOptions)> for RemoteHostLocation {
+    fn from((project_id, options): (u64, RemoteConnectionOptions)) -> Self {
         let (user_name, host_name) = match options {
             RemoteConnectionOptions::Ssh(ssh) => (
                 ssh.username.map(SharedString::new),
@@ -165,9 +167,10 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
                 SharedString::new(docker_connection_options.container_id),
             ),
         };
-        RemoteHostLocation {
+        Self {
             user_name,
             host_identifier: host_name,
+            project_id: ProjectId(project_id),
         }
     }
 }
@@ -227,8 +230,8 @@ impl TrustedWorktreesStore {
         trusted_paths: TrustedPaths,
         worktree_store: Option<Entity<WorktreeStore>>,
         remote_host: Option<RemoteHostLocation>,
-        downstream_client: Option<(AnyProtoClient, u64)>,
-        upstream_client: Option<(AnyProtoClient, u64)>,
+        downstream_client: Option<(AnyProtoClient, ProjectId)>,
+        upstream_client: Option<(AnyProtoClient, ProjectId)>,
     ) -> Self {
         if let Some((upstream_client, upstream_project_id)) = &upstream_client {
             let trusted_paths = trusted_paths
@@ -238,7 +241,7 @@ impl TrustedWorktreesStore {
             if !trusted_paths.is_empty() {
                 upstream_client
                     .send(proto::TrustWorktrees {
-                        project_id: *upstream_project_id,
+                        project_id: upstream_project_id.0,
                         trusted_paths,
                     })
                     .ok();
@@ -383,7 +386,7 @@ impl TrustedWorktreesStore {
             if !trusted_paths.is_empty() {
                 upstream_client
                     .send(proto::TrustWorktrees {
-                        project_id: *upstream_project_id,
+                        project_id: upstream_project_id.0,
                         trusted_paths,
                     })
                     .ok();
@@ -472,7 +475,7 @@ impl TrustedWorktreesStore {
         if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
             downstream_client
                 .send(proto::RestrictWorktrees {
-                    project_id: *downstream_project_id,
+                    project_id: downstream_project_id.0,
                     worktree_ids: vec![worktree_id.to_proto()],
                 })
                 .ok();
@@ -480,7 +483,7 @@ impl TrustedWorktreesStore {
         if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
             upstream_client
                 .send(proto::RestrictWorktrees {
-                    project_id: *upstream_project_id,
+                    project_id: upstream_project_id.0,
                     worktree_ids: vec![worktree_id.to_proto()],
                 })
                 .ok();
@@ -1440,8 +1443,6 @@ mod tests {
 
         let trusted_worktrees = init_trust_global(worktree_store, cx);
 
-        let host_a: Option<RemoteHostLocation> = None;
-
         let can_trust_local =
             trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
         assert!(!can_trust_local, "local worktree restricted on host_a");
@@ -1449,7 +1450,7 @@ mod tests {
         trusted_worktrees.update(cx, |store, cx| {
             store.trust(
                 HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
-                host_a.clone(),
+                None,
                 cx,
             );
         });
@@ -1458,7 +1459,7 @@ mod tests {
             trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
         assert!(
             can_trust_local_after,
-            "local worktree should be trusted on host_a"
+            "local worktree should be trusted on local host"
         );
     }
 }

crates/remote_server/src/headless_project.rs 🔗

@@ -1,4 +1,5 @@
 use anyhow::{Context as _, Result, anyhow};
+use client::ProjectId;
 use collections::HashSet;
 use language::File;
 use lsp::LanguageServerId;
@@ -104,7 +105,7 @@ impl HeadlessProject {
             project::trusted_worktrees::track_worktree_trust(
                 worktree_store.clone(),
                 None::<RemoteHostLocation>,
-                Some((session.clone(), REMOTE_SERVER_PROJECT_ID)),
+                Some((session.clone(), ProjectId(REMOTE_SERVER_PROJECT_ID))),
                 None,
                 cx,
             );

crates/remote_server/src/unix.rs 🔗

@@ -1,7 +1,7 @@
 use crate::HeadlessProject;
 use crate::headless_project::HeadlessAppState;
 use anyhow::{Context as _, Result, anyhow};
-use client::ProxySettings;
+use client::{ProjectId, ProxySettings};
 use collections::HashMap;
 use project::trusted_worktrees;
 use util::ResultExt;
@@ -419,7 +419,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(HashMap::default(), Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
+        trusted_worktrees::init(HashMap::default(), Some((session.clone(), ProjectId(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,6 +9,7 @@ use std::{
 };
 
 use anyhow::{Context as _, Result, bail};
+use client::ProjectId;
 use collections::{HashMap, HashSet, IndexSet};
 use db::{
     kvp::KEY_VALUE_STORE,
@@ -844,6 +845,9 @@ impl Domain for WorkspaceDb {
                 host_name TEXT
             ) STRICT;
         ),
+        sql!(
+            ALTER TABLE trusted_worktrees ADD COLUMN project_id INTEGER;
+        ),
         sql!(CREATE TABLE toolchains2 (
             workspace_id INTEGER,
             worktree_root_path TEXT NOT NULL,
@@ -1977,10 +1981,10 @@ impl WorkspaceDb {
             .collect::<Vec<_>>();
         let mut first_worktree;
         let mut last_worktree = 0_usize;
-        for (count, placeholders) in std::iter::once("(?, ?, ?)")
+        for (count, placeholders) in std::iter::once("(?, ?, ?, ?)")
             .cycle()
             .take(trusted_worktrees.len())
-            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
+            .chunks(MAX_QUERY_PLACEHOLDERS / 4)
             .into_iter()
             .map(|chunk| {
                 let mut count = 0;
@@ -1996,7 +2000,7 @@ impl WorkspaceDb {
             first_worktree = last_worktree;
             last_worktree = last_worktree + count;
             let query = format!(
-                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
+                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name, project_id)
 VALUES {placeholders};"#
             );
 
@@ -2020,6 +2024,8 @@ VALUES {placeholders};"#
                         &host.as_ref().map(|host| host.host_identifier.as_str()),
                         next_index,
                     )?;
+                    next_index =
+                        statement.bind(&host.as_ref().map(|host| host.project_id.0), next_index)?;
                 }
                 statement.exec()
             })
@@ -2038,17 +2044,21 @@ VALUES {placeholders};"#
         let trusted_worktrees = DB.trusted_worktrees()?;
         Ok(trusted_worktrees
             .into_iter()
-            .filter_map(|(abs_path, user_name, host_name)| {
-                let db_host = match (user_name, host_name) {
-                    (_, None) => None,
-                    (None, Some(host_name)) => Some(RemoteHostLocation {
+            .filter_map(|(abs_path, user_name, host_name, project_id)| {
+                let db_host = match (user_name, host_name, project_id) {
+                    (None, Some(host_name), Some(project_id)) => Some(RemoteHostLocation {
                         user_name: None,
                         host_identifier: SharedString::new(host_name),
+                        project_id: ProjectId(project_id),
                     }),
-                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
-                        user_name: Some(SharedString::new(user_name)),
-                        host_identifier: SharedString::new(host_name),
-                    }),
+                    (Some(user_name), Some(host_name), Some(project_id)) => {
+                        Some(RemoteHostLocation {
+                            user_name: Some(SharedString::new(user_name)),
+                            host_identifier: SharedString::new(host_name),
+                            project_id: ProjectId(project_id),
+                        })
+                    }
+                    _ => None,
                 };
 
                 let abs_path = abs_path?;
@@ -2072,8 +2082,8 @@ VALUES {placeholders};"#
     }
 
     query! {
-        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
-            SELECT absolute_path, user_name, host_name
+        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>, Option<u64>)>> {
+            SELECT absolute_path, user_name, host_name, project_id
             FROM trusted_worktrees
         }
     }

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, TrustedWorktreesEvent},
+    trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
 };
 use remote::{
     RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
@@ -6620,7 +6620,15 @@ impl Workspace {
                 .unwrap_or(false);
             if has_restricted_worktrees {
                 let project = self.project().read(cx);
-                let remote_host = project.remote_connection_options(cx);
+                let project_id = project
+                    .lsp_store()
+                    .read(cx)
+                    .downstream_client()
+                    .or_else(|| project.lsp_store().read(cx).upstream_client())
+                    .map(|(_, project_id)| project_id);
+                let remote_host = project_id
+                    .zip(project.remote_connection_options(cx))
+                    .map(RemoteHostLocation::from);
                 let worktree_store = project.worktree_store().downgrade();
                 self.toggle_modal(window, cx, |_, cx| {
                     SecurityModal::new(worktree_store, remote_host, cx)