Fix worktree trust handling of multiple projects on the same remote host (#45834) (cherry-pick to preview) (#45839)

Cherry-pick of #45834 to preview

----
Closes https://github.com/zed-industries/zed/issues/45630

Remote host location alone is not enough to distinguish between remote
worktrees: different remote projects open in different windows will have
the same remote host location and _will_ have the same `WorktreeId`.

Thus, require an associated `WorktreeStore` with all
`WorktreeId`-related trust questions, and store those IDs based on the
store key.

Release Notes:

- Fixed worktree trust handling of multiple projects on the same remote
host

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
zed-zippy[bot]
2025-12-29 20:33:18 +00:00
committed by GitHub
parent d4541ec586
commit 6af77ec0d9
14 changed files with 487 additions and 399 deletions

View File

@@ -855,8 +855,6 @@ async fn test_slow_adapter_startup_retries(
#[gpui::test]
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
use project::trusted_worktrees::RemoteHostLocation;
cx_a.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
@@ -991,23 +989,19 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
});
assert_eq!(worktree_ids.len(), 2);
let remote_host = project_a.read_with(cx_a, |project, cx| {
project
.remote_connection_options(cx)
.map(RemoteHostLocation::from)
});
let trusted_worktrees =
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(!can_trust_a, "project_a should be restricted initially");
assert!(!can_trust_b, "project_b should be restricted initially");
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
store.has_restricted_worktrees(&worktree_store, cx)
});
@@ -1054,8 +1048,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
trusted_worktrees.update(cx_a, |store, cx| {
store.trust(
&worktree_store,
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
remote_host.clone(),
cx,
);
});
@@ -1080,25 +1074,29 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
"inlay hints should be queried after trust approval"
);
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(can_trust_a, "project_a should be trusted after trust()");
assert!(!can_trust_b, "project_b should still be restricted");
trusted_worktrees.update(cx_a, |store, cx| {
store.trust(
&worktree_store,
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
remote_host.clone(),
cx,
);
});
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(can_trust_a, "project_a should remain trusted");
assert!(can_trust_b, "project_b should now be trusted");

View File

@@ -29419,11 +29419,14 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
.map(|wt| wt.read(cx).id())
.expect("should have a worktree")
});
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let trusted_worktrees =
cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
let can_trust = trusted_worktrees.update(cx, |store, cx| {
store.can_trust(&worktree_store, worktree_id, cx)
});
assert!(!can_trust, "worktree should be restricted initially");
let buffer_before_approval = project
@@ -29469,8 +29472,8 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
trusted_worktrees.update(cx, |store, cx| {
store.trust(
&worktree_store,
std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
None,
cx,
);
});
@@ -29497,7 +29500,8 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
"inlay hints should be queried after trust approval"
);
let can_trust_after =
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
store.can_trust(&worktree_store, worktree_id, cx)
});
assert!(can_trust_after, "worktree should be trusted after trust()");
}

View File

@@ -13,7 +13,7 @@ use picker::{Picker, PickerDelegate, PickerEditorPosition};
use project::{
DirectoryLister,
git_store::Repository,
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
trusted_worktrees::{PathTrust, TrustedWorktrees},
};
use recent_projects::{RemoteConnectionModal, connect};
use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
@@ -271,16 +271,18 @@ impl WorktreeListDelegate {
if let Some((parent_worktree, _)) =
project.read(cx).find_worktree(repo_path, cx)
{
let worktree_store = project.read(cx).worktree_store();
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
if trusted_worktrees.can_trust(parent_worktree.read(cx).id(), cx) {
if trusted_worktrees.can_trust(
&worktree_store,
parent_worktree.read(cx).id(),
cx,
) {
trusted_worktrees.trust(
&worktree_store,
HashSet::from_iter([PathTrust::AbsPath(
new_worktree_path.clone(),
)]),
project
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from),
cx,
);
}

View File

@@ -386,7 +386,7 @@ impl LocalLspStore {
let untrusted_worktree_task =
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
let can_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust(worktree_id, cx)
trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
});
if can_trust {
self.restricted_worktrees_tasks.remove(&worktree_id);

View File

@@ -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,7 +1297,7 @@ impl Project {
worktree_store.clone(),
Some(RemoteHostLocation::from(connection_options)),
None,
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
Some((remote_proto.clone(), ProjectId(REMOTE_SERVER_PROJECT_ID))),
cx,
);
}
@@ -4814,7 +4816,7 @@ impl Project {
let worktree_id = WorktreeId::from_proto(envelope.payload.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)
trusted_worktrees.can_trust(&project.worktree_store, worktree_id, cx)
});
}
if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
@@ -4853,18 +4855,14 @@ impl Project {
.update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
let remote_host = this
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from);
trusted_worktrees.trust(
&this.read(cx).worktree_store(),
envelope
.payload
.trusted_paths
.into_iter()
.filter_map(|proto_path| PathTrust::from_proto(proto_path))
.collect(),
remote_host,
cx,
);
})?;
@@ -4880,6 +4878,7 @@ impl Project {
.update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
let worktree_store = this.read(cx).worktree_store().downgrade();
let restricted_paths = envelope
.payload
.worktree_ids
@@ -4887,11 +4886,7 @@ impl Project {
.map(WorktreeId::from_proto)
.map(PathTrust::Worktree)
.collect::<HashSet<_>>();
let remote_host = this
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from);
trusted_worktrees.restrict(restricted_paths, remote_host, cx);
trusted_worktrees.restrict(worktree_store, restricted_paths, cx);
})?;
Ok(proto::Ack {})
}

View File

@@ -1046,7 +1046,7 @@ impl SettingsObserver {
if *can_trust_worktree.get_or_init(|| {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust(worktree_id, cx)
trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
})
} else {
true

File diff suppressed because it is too large Load Diff

View File

@@ -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,
);
@@ -611,22 +612,23 @@ impl HeadlessProject {
}
pub async fn handle_trust_worktrees(
_: Entity<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::TrustWorktrees>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let trusted_worktrees = cx
.update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?;
let worktree_store = this.read_with(&cx, |project, _| project.worktree_store.clone())?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
trusted_worktrees.trust(
&worktree_store,
envelope
.payload
.trusted_paths
.into_iter()
.filter_map(PathTrust::from_proto)
.collect(),
None,
cx,
);
})?;
@@ -634,13 +636,15 @@ impl HeadlessProject {
}
pub async fn handle_restrict_worktrees(
_: Entity<Self>,
this: Entity<Self>,
envelope: TypedEnvelope<proto::RestrictWorktrees>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let trusted_worktrees = cx
.update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?;
let worktree_store =
this.read_with(&cx, |project, _| project.worktree_store.downgrade())?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
let restricted_paths = envelope
.payload
@@ -649,7 +653,7 @@ impl HeadlessProject {
.map(WorktreeId::from_proto)
.map(PathTrust::Worktree)
.collect::<HashSet<_>>();
trusted_worktrees.restrict(restricted_paths, None, cx);
trusted_worktrees.restrict(worktree_store, restricted_paths, cx);
})?;
Ok(proto::Ack {})
}

View File

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

View File

@@ -20,7 +20,7 @@ use std::{
time::Duration,
};
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct AnyProtoClient(Arc<State>);
type RequestIds = Arc<
@@ -45,6 +45,15 @@ struct State {
request_ids: RequestIds,
}
impl std::fmt::Debug for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("State")
.field("next_lsp_request_id", &self.next_lsp_request_id)
.field("request_ids", &self.request_ids)
.finish_non_exhaustive()
}
}
pub trait ProtoClient: Send + Sync {
fn request(
&self,

View File

@@ -15,11 +15,10 @@ use db::{
sqlez::{connection::Connection, domain::Domain},
sqlez_macros::sql,
};
use gpui::{Axis, Bounds, Entity, Task, WindowBounds, WindowId, point, size};
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
use project::{
debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store},
worktree_store::WorktreeStore,
trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
};
use language::{LanguageName, Toolchain, ToolchainScope};
@@ -1888,18 +1887,12 @@ VALUES {placeholders};"#
Ok(())
}
pub fn fetch_trusted_worktrees(
&self,
worktree_store: Option<Entity<WorktreeStore>>,
host: Option<RemoteHostLocation>,
cx: &App,
) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
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 {
user_name: None,
host_identifier: SharedString::new(host_name),
@@ -1908,24 +1901,14 @@ VALUES {placeholders};"#
user_name: Some(SharedString::new(user_name)),
host_identifier: SharedString::new(host_name),
}),
_ => None,
};
let abs_path = abs_path?;
Some(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))
})
Some((db_host, abs_path?))
})
.fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
.fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
acc.entry(remote_host)
.or_insert_with(HashSet::default)
.insert(path_trust);
.insert(abs_path);
acc
}))
}

View File

@@ -267,7 +267,9 @@ impl SecurityModal {
}
fn trust_and_dismiss(&mut self, cx: &mut Context<Self>) {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
if let Some((trusted_worktrees, worktree_store)) =
TrustedWorktrees::try_get_global(cx).zip(self.worktree_store.upgrade())
{
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
let mut paths_to_trust = self
.restricted_paths
@@ -288,7 +290,7 @@ impl SecurityModal {
},
));
}
trusted_worktrees.trust(paths_to_trust, self.remote_host.clone(), cx);
trusted_worktrees.trust(&worktree_store, paths_to_trust, cx);
});
}
@@ -305,7 +307,7 @@ impl SecurityModal {
if let Some(worktree_store) = self.worktree_store.upgrade() {
let new_restricted_worktrees = trusted_worktrees
.read(cx)
.restricted_worktrees(worktree_store.read(cx), cx)
.restricted_worktrees(&worktree_store, cx)
.into_iter()
.filter_map(|(worktree_id, abs_path)| {
let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;

View File

@@ -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,
@@ -1188,7 +1188,6 @@ 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,
@@ -1235,23 +1234,26 @@ impl Workspace {
cx: &mut Context<Self>,
) -> Self {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| {
cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
if let TrustedWorktreesEvent::Trusted(..) = e {
// Do not persist auto trusted worktrees
if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
let new_trusted_worktrees =
worktrees_store.update(cx, |worktrees_store, cx| {
worktrees_store.trusted_paths_for_serialization(cx)
});
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)
.await
.log_err();
});
worktrees_store.update(cx, |worktrees_store, cx| {
worktrees_store.schedule_serialization(
cx,
|new_trusted_worktrees, cx| {
let timeout =
cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
cx.background_spawn(async move {
timeout.await;
persistence::DB
.save_trusted_worktrees(new_trusted_worktrees)
.await
.log_err();
})
},
)
});
}
}
})
@@ -1282,7 +1284,11 @@ impl Workspace {
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);
trusted_worktrees.can_trust(
&this.project().read(cx).worktree_store(),
*worktree_id,
cx,
);
});
}
}
@@ -1294,7 +1300,11 @@ impl Workspace {
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);
trusted_worktrees.can_trust(
&this.project().read(cx).worktree_store(),
*worktree_id,
cx,
);
});
}
this.update_worktree_data(window, cx);
@@ -1583,7 +1593,6 @@ 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,
@@ -6542,7 +6551,9 @@ impl Workspace {
.unwrap_or(false);
if has_restricted_worktrees {
let project = self.project().read(cx);
let remote_host = project.remote_connection_options(cx);
let remote_host = 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)

View File

@@ -406,14 +406,14 @@ pub fn main() {
});
app.run(move |cx| {
let trusted_paths = match workspace::WORKSPACE_DB.fetch_trusted_worktrees(None, None, cx) {
let db_trusted_paths = match workspace::WORKSPACE_DB.fetch_trusted_worktrees() {
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);
trusted_worktrees::init(db_trusted_paths, None, None, cx);
menu::init();
zed_actions::init();