From e7f32ce1d10f75a5c18734b127ca54be605d3f10 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 28 Dec 2025 03:12:00 +0200 Subject: [PATCH] Distinguish worktree trust hosts by project ids too --- .../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(-) diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 5342b0bbd4b11afb24ccbaa6d4bf17df036cec76..f346ff98654b69b6c10d2f2de1894b9ca9ddd595 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/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) }); diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index fef5e16c80ddd26ae6dd0b2a5c0ad1d8e5b21b2c..0569410d3b3d787db07adf37e6c33a83c228243f 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/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, ); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9b8e0790f829e8707e42d2a9581785f4c36685dd..b7d218bce6ca0fc25ee1fee65b3ae8529a0cd7f2 100644 --- a/crates/project/src/project.rs +++ b/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); })?; diff --git a/crates/project/src/trusted_worktrees.rs b/crates/project/src/trusted_worktrees.rs index 2b7e73d1f23eefd5a568800c86b19f8f509faba7..0cbace77e7add7af6e410f67bc67ed21b7dd8680 100644 --- a/crates/project/src/trusted_worktrees.rs +++ b/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, remote_host: Option, - 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, Option>, trusted_paths: TrustedPaths, restricted: HashSet, @@ -147,10 +148,11 @@ pub struct TrustedWorktreesStore { pub struct RemoteHostLocation { pub user_name: Option, pub host_identifier: SharedString, + pub project_id: ProjectId, } -impl From 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 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>, remote_host: Option, - 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 = 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" ); } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index c83cc6aa34402a082fe104d64a8cb47f460704b8..e9c7d95bcf1c3f83b3fd772be06a183c29044b42 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/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::, - Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), + Some((session.clone(), ProjectId(REMOTE_SERVER_PROJECT_ID))), None, cx, ); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 449b8491ece2494dacf8bfb1fa89aeeb8f6a81ac..a81859a685e1ce34eccc0a0d4809b9914743f79f 100644 --- a/crates/remote_server/src/unix.rs +++ b/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); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 8d20339ec952020416e4b8d5846bf44f5f8e9b98..92634a2ea3f1820158a89ce2a39a76d7090591fa 100644 --- a/crates/workspace/src/persistence.rs +++ b/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::>(); 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, Option, Option)>> { - SELECT absolute_path, user_name, host_name + fn trusted_worktrees() -> Result, Option, Option, Option)>> { + SELECT absolute_path, user_name, host_name, project_id FROM trusted_worktrees } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eb0debc929e9bc17a5d64a7c650165446d687e4e..f5a3b0fc1e80031d04940ae5f5a1000b4315d59d 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, 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)