1//! A module, responsible for managing the trust logic in Zed.
2//!
3//! It deals with multiple hosts, distinguished by [`RemoteHostLocation`].
4//! Each [`crate::Project`] and `HeadlessProject` should call [`init_global`], if wants to establish the trust mechanism.
5//! This will set up a [`gpui::Global`] with [`TrustedWorktrees`] entity that will persist, restore and allow querying for worktree trust.
6//! It's also possible to subscribe on [`TrustedWorktreesEvent`] events of this entity to track trust changes dynamically.
7//!
8//! The implementation can synchronize trust information with the remote hosts: currently, WSL and SSH.
9//! Docker and Collab remotes do not employ trust mechanism, as manage that themselves.
10//!
11//! Unless `trust_all_worktrees` auto trust is enabled, does not trust anything that was not persisted before.
12//! When dealing with "restricted" and other related concepts in the API, it means all explicitly restricted, after any of the [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_global`] calls.
13//!
14//! Zed does not consider invisible, `worktree.is_visible() == false` worktrees in Zed, as those are programmatically created inside Zed for internal needs, e.g. a tmp dir for `keymap_editor.rs` needs.
15//!
16//!
17//! Path rust hierarchy.
18//!
19//! Zed has multiple layers of trust, based on the requests and [`PathTrust`] enum variants.
20//! From the least to the most trusted level:
21//!
22//! * "single file worktree"
23//!
24//! After opening an empty Zed it's possible to open just a file, same as after opening a directory in Zed it's possible to open a file outside of this directory.
25//! Usual scenario for both cases is opening Zed's settings.json file via `zed: open settings file` command: that starts a language server for a new file open, which originates from a newly created, single file worktree.
26//!
27//! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.
28//! Each single file worktree requires a separate trust permission, unless a more global level is trusted.
29//!
30//! * "directory worktree"
31//!
32//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
33//! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted.
34//!
35//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.
36//!
37//! * "path override"
38//!
39//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
40//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
41
42use client::ProjectId;
43use collections::{HashMap, HashSet};
44use gpui::{
45 App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
46};
47use remote::RemoteConnectionOptions;
48use rpc::{AnyProtoClient, proto};
49use settings::{Settings as _, WorktreeId};
50use std::{
51 path::{Path, PathBuf},
52 sync::Arc,
53};
54use util::debug_panic;
55
56use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
57
58pub fn init(
59 db_trusted_paths: DbTrustedPaths,
60 downstream_client: Option<(AnyProtoClient, ProjectId)>,
61 upstream_client: Option<(AnyProtoClient, ProjectId)>,
62 cx: &mut App,
63) {
64 if TrustedWorktrees::try_get_global(cx).is_none() {
65 let trusted_worktrees = cx.new(|_| {
66 TrustedWorktreesStore::new(db_trusted_paths, downstream_client, upstream_client)
67 });
68 cx.set_global(TrustedWorktrees(trusted_worktrees))
69 }
70}
71
72/// An initialization call to set up trust global for a particular project (remote or local).
73pub fn track_worktree_trust(
74 worktree_store: Entity<WorktreeStore>,
75 remote_host: Option<RemoteHostLocation>,
76 downstream_client: Option<(AnyProtoClient, ProjectId)>,
77 upstream_client: Option<(AnyProtoClient, ProjectId)>,
78 cx: &mut App,
79) {
80 match TrustedWorktrees::try_get_global(cx) {
81 Some(trusted_worktrees) => {
82 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
83 if let Some(downstream_client) = downstream_client {
84 trusted_worktrees.downstream_clients.push(downstream_client);
85 }
86 if let Some(upstream_client) = upstream_client.clone() {
87 trusted_worktrees.upstream_clients.push(upstream_client);
88 }
89 trusted_worktrees.add_worktree_store(worktree_store, remote_host, cx);
90
91 if let Some((upstream_client, upstream_project_id)) = upstream_client {
92 let trusted_paths = trusted_worktrees
93 .trusted_paths
94 .iter()
95 .flat_map(|(_, paths)| {
96 paths.iter().map(|trusted_path| trusted_path.to_proto())
97 })
98 .collect::<Vec<_>>();
99 if !trusted_paths.is_empty() {
100 upstream_client
101 .send(proto::TrustWorktrees {
102 project_id: upstream_project_id.0,
103 trusted_paths,
104 })
105 .ok();
106 }
107 }
108 });
109 }
110 None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
111 }
112}
113
114/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
115pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
116
117impl Global for TrustedWorktrees {}
118
119impl TrustedWorktrees {
120 pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
121 cx.try_global::<Self>().map(|this| this.0.clone())
122 }
123}
124
125/// A collection of worktrees that are considered trusted and not trusted.
126/// This can be used when checking for this criteria before enabling certain features.
127///
128/// Emits an event each time the worktree was checked and found not trusted,
129/// or a certain worktree had been trusted.
130#[derive(Debug)]
131pub struct TrustedWorktreesStore {
132 downstream_clients: Vec<(AnyProtoClient, ProjectId)>,
133 upstream_clients: Vec<(AnyProtoClient, ProjectId)>,
134 worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
135 db_trusted_paths: DbTrustedPaths,
136 trusted_paths: TrustedPaths,
137 restricted: HashMap<WeakEntity<WorktreeStore>, HashSet<WorktreeId>>,
138 worktree_trust_serialization: Task<()>,
139}
140
141/// An identifier of a host to split the trust questions by.
142/// Each trusted data change and event is done for a particular host.
143/// A host may contain more than one worktree or even project open concurrently.
144#[derive(Debug, PartialEq, Eq, Clone, Hash)]
145pub struct RemoteHostLocation {
146 pub user_name: Option<SharedString>,
147 pub host_identifier: SharedString,
148}
149
150impl From<RemoteConnectionOptions> for RemoteHostLocation {
151 fn from(options: RemoteConnectionOptions) -> Self {
152 let (user_name, host_name) = match options {
153 RemoteConnectionOptions::Ssh(ssh) => (
154 ssh.username.map(SharedString::new),
155 SharedString::new(ssh.host.to_string()),
156 ),
157 RemoteConnectionOptions::Wsl(wsl) => (
158 wsl.user.map(SharedString::new),
159 SharedString::new(wsl.distro_name),
160 ),
161 RemoteConnectionOptions::Docker(docker_connection_options) => (
162 Some(SharedString::new(docker_connection_options.name)),
163 SharedString::new(docker_connection_options.container_id),
164 ),
165 #[cfg(any(test, feature = "test-support"))]
166 RemoteConnectionOptions::Mock(mock) => {
167 (None, SharedString::new(format!("mock-{}", mock.id)))
168 }
169 };
170 Self {
171 user_name,
172 host_identifier: host_name,
173 }
174 }
175}
176
177/// A unit of trust consideration inside a particular host:
178/// either a familiar worktree, or a path that may influence other worktrees' trust.
179/// See module-level documentation on the trust model.
180#[derive(Debug, PartialEq, Eq, Clone, Hash)]
181pub enum PathTrust {
182 /// A worktree that is familiar to this workspace.
183 /// Either a single file or a directory worktree.
184 Worktree(WorktreeId),
185 /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
186 /// or a parent path coming out of the security modal.
187 AbsPath(PathBuf),
188}
189
190impl PathTrust {
191 fn to_proto(&self) -> proto::PathTrust {
192 match self {
193 Self::Worktree(worktree_id) => proto::PathTrust {
194 content: Some(proto::path_trust::Content::WorktreeId(
195 worktree_id.to_proto(),
196 )),
197 },
198 Self::AbsPath(path_buf) => proto::PathTrust {
199 content: Some(proto::path_trust::Content::AbsPath(
200 path_buf.to_string_lossy().to_string(),
201 )),
202 },
203 }
204 }
205
206 pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
207 Some(match proto.content? {
208 proto::path_trust::Content::WorktreeId(id) => {
209 Self::Worktree(WorktreeId::from_proto(id))
210 }
211 proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
212 })
213 }
214}
215
216/// A change of trust on a certain host.
217#[derive(Debug)]
218pub enum TrustedWorktreesEvent {
219 Trusted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
220 Restricted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
221}
222
223impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
224
225type TrustedPaths = HashMap<WeakEntity<WorktreeStore>, HashSet<PathTrust>>;
226pub type DbTrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>;
227
228impl TrustedWorktreesStore {
229 fn new(
230 db_trusted_paths: DbTrustedPaths,
231 downstream_client: Option<(AnyProtoClient, ProjectId)>,
232 upstream_client: Option<(AnyProtoClient, ProjectId)>,
233 ) -> Self {
234 if let Some((upstream_client, upstream_project_id)) = &upstream_client {
235 let trusted_paths = db_trusted_paths
236 .iter()
237 .flat_map(|(_, paths)| {
238 paths
239 .iter()
240 .cloned()
241 .map(PathTrust::AbsPath)
242 .map(|trusted_path| trusted_path.to_proto())
243 })
244 .collect::<Vec<_>>();
245 if !trusted_paths.is_empty() {
246 upstream_client
247 .send(proto::TrustWorktrees {
248 project_id: upstream_project_id.0,
249 trusted_paths,
250 })
251 .ok();
252 }
253 }
254
255 Self {
256 db_trusted_paths,
257 downstream_clients: downstream_client.into_iter().collect(),
258 upstream_clients: upstream_client.into_iter().collect(),
259 trusted_paths: HashMap::default(),
260 worktree_stores: HashMap::default(),
261 restricted: HashMap::default(),
262 worktree_trust_serialization: Task::ready(()),
263 }
264 }
265
266 /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
267 pub fn has_restricted_worktrees(
268 &self,
269 worktree_store: &Entity<WorktreeStore>,
270 cx: &App,
271 ) -> bool {
272 self.restricted
273 .get(&worktree_store.downgrade())
274 .is_some_and(|restricted_worktrees| {
275 restricted_worktrees.iter().any(|restricted_worktree| {
276 worktree_store
277 .read(cx)
278 .worktree_for_id(*restricted_worktree, cx)
279 .is_some()
280 })
281 })
282 }
283
284 /// Adds certain entities on this host to the trusted list.
285 /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
286 /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
287 pub fn trust(
288 &mut self,
289 worktree_store: &Entity<WorktreeStore>,
290 mut trusted_paths: HashSet<PathTrust>,
291 cx: &mut Context<Self>,
292 ) {
293 let weak_worktree_store = worktree_store.downgrade();
294 let mut new_trusted_single_file_worktrees = HashSet::default();
295 let mut new_trusted_other_worktrees = HashSet::default();
296 let mut new_trusted_abs_paths = HashSet::default();
297 for trusted_path in trusted_paths.iter().chain(
298 self.trusted_paths
299 .remove(&weak_worktree_store)
300 .iter()
301 .flat_map(|current_trusted| current_trusted.iter()),
302 ) {
303 match trusted_path {
304 PathTrust::Worktree(worktree_id) => {
305 if let Some(restricted_worktrees) =
306 self.restricted.get_mut(&weak_worktree_store)
307 {
308 restricted_worktrees.remove(worktree_id);
309 if restricted_worktrees.is_empty() {
310 self.restricted.remove(&weak_worktree_store);
311 }
312 };
313
314 if let Some(worktree) =
315 worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
316 {
317 if worktree.read(cx).is_single_file() {
318 new_trusted_single_file_worktrees.insert(*worktree_id);
319 } else {
320 new_trusted_other_worktrees
321 .insert((worktree.read(cx).abs_path(), *worktree_id));
322 }
323 }
324 }
325 PathTrust::AbsPath(abs_path) => {
326 debug_assert!(
327 util::paths::is_absolute(
328 &abs_path.to_string_lossy(),
329 worktree_store.read(cx).path_style()
330 ),
331 "Cannot trust non-absolute path {abs_path:?} on path style {style:?}",
332 style = worktree_store.read(cx).path_style()
333 );
334 if let Some((worktree_id, is_file)) =
335 find_worktree_in_store(worktree_store.read(cx), abs_path, cx)
336 {
337 if is_file {
338 new_trusted_single_file_worktrees.insert(worktree_id);
339 } else {
340 new_trusted_other_worktrees
341 .insert((Arc::from(abs_path.as_path()), worktree_id));
342 }
343 }
344 new_trusted_abs_paths.insert(abs_path.clone());
345 }
346 }
347 }
348
349 new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
350 new_trusted_abs_paths
351 .iter()
352 .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
353 });
354 if !new_trusted_other_worktrees.is_empty() {
355 new_trusted_single_file_worktrees.clear();
356 }
357
358 if let Some(restricted_worktrees) = self.restricted.remove(&weak_worktree_store) {
359 let new_restricted_worktrees = restricted_worktrees
360 .into_iter()
361 .filter(|restricted_worktree| {
362 let Some(worktree) = worktree_store
363 .read(cx)
364 .worktree_for_id(*restricted_worktree, cx)
365 else {
366 return false;
367 };
368 let is_file = worktree.read(cx).is_single_file();
369
370 // When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.
371 if is_file && !new_trusted_abs_paths.is_empty() {
372 trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
373 return false;
374 }
375
376 let restricted_worktree_path = worktree.read(cx).abs_path();
377 let retain = (!is_file || new_trusted_other_worktrees.is_empty())
378 && new_trusted_abs_paths.iter().all(|new_trusted_path| {
379 !restricted_worktree_path.starts_with(new_trusted_path)
380 });
381 if !retain {
382 trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
383 }
384 retain
385 })
386 .collect();
387 self.restricted
388 .insert(weak_worktree_store.clone(), new_restricted_worktrees);
389 }
390
391 {
392 let trusted_paths = self
393 .trusted_paths
394 .entry(weak_worktree_store.clone())
395 .or_default();
396 trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
397 trusted_paths.extend(
398 new_trusted_other_worktrees
399 .into_iter()
400 .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
401 );
402 trusted_paths.extend(
403 new_trusted_single_file_worktrees
404 .into_iter()
405 .map(PathTrust::Worktree),
406 );
407 }
408
409 cx.emit(TrustedWorktreesEvent::Trusted(
410 weak_worktree_store,
411 trusted_paths.clone(),
412 ));
413
414 for (upstream_client, upstream_project_id) in &self.upstream_clients {
415 let trusted_paths = trusted_paths
416 .iter()
417 .map(|trusted_path| trusted_path.to_proto())
418 .collect::<Vec<_>>();
419 if !trusted_paths.is_empty() {
420 upstream_client
421 .send(proto::TrustWorktrees {
422 project_id: upstream_project_id.0,
423 trusted_paths,
424 })
425 .ok();
426 }
427 }
428 }
429
430 /// Restricts certain entities on this host.
431 /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
432 pub fn restrict(
433 &mut self,
434 worktree_store: WeakEntity<WorktreeStore>,
435 restricted_paths: HashSet<PathTrust>,
436 cx: &mut Context<Self>,
437 ) {
438 let mut restricted = HashSet::default();
439 for restricted_path in restricted_paths {
440 match restricted_path {
441 PathTrust::Worktree(worktree_id) => {
442 self.restricted
443 .entry(worktree_store.clone())
444 .or_default()
445 .insert(worktree_id);
446 restricted.insert(PathTrust::Worktree(worktree_id));
447 }
448 PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
449 }
450 }
451
452 cx.emit(TrustedWorktreesEvent::Restricted(
453 worktree_store,
454 restricted,
455 ));
456 }
457
458 /// Erases all trust information.
459 /// Requires Zed's restart to take proper effect.
460 pub fn clear_trusted_paths(&mut self) {
461 self.trusted_paths.clear();
462 self.db_trusted_paths.clear();
463 }
464
465 /// Checks whether a certain worktree is trusted (or on a larger trust level).
466 /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
467 ///
468 /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
469 pub fn can_trust(
470 &mut self,
471 worktree_store: &Entity<WorktreeStore>,
472 worktree_id: WorktreeId,
473 cx: &mut Context<Self>,
474 ) -> bool {
475 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
476 return true;
477 }
478
479 let weak_worktree_store = worktree_store.downgrade();
480 let Some(worktree) = worktree_store.read(cx).worktree_for_id(worktree_id, cx) else {
481 return false;
482 };
483 let worktree_path = worktree.read(cx).abs_path();
484 // Zed opened an "internal" directory: e.g. a tmp dir for `keymap_editor.rs` needs.
485 if !worktree.read(cx).is_visible() {
486 log::debug!("Skipping worktree trust checks for not visible {worktree_path:?}");
487 return true;
488 }
489
490 let is_file = worktree.read(cx).is_single_file();
491 if self
492 .restricted
493 .get(&weak_worktree_store)
494 .is_some_and(|restricted_worktrees| restricted_worktrees.contains(&worktree_id))
495 {
496 return false;
497 }
498
499 if self
500 .trusted_paths
501 .get(&weak_worktree_store)
502 .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
503 {
504 return true;
505 }
506
507 // * Single files are auto-approved when something else (not a single file) was approved on this host already.
508 // * If parent path is trusted already, this worktree is stusted also.
509 //
510 // See module documentation for details on trust level.
511 if let Some(trusted_paths) = self.trusted_paths.get(&weak_worktree_store) {
512 let auto_trusted = worktree_store.read_with(cx, |worktree_store, cx| {
513 trusted_paths.iter().any(|trusted_path| match trusted_path {
514 PathTrust::Worktree(worktree_id) => worktree_store
515 .worktree_for_id(*worktree_id, cx)
516 .is_some_and(|worktree| {
517 let worktree = worktree.read(cx);
518 worktree_path.starts_with(&worktree.abs_path())
519 || (is_file && !worktree.is_single_file())
520 }),
521 PathTrust::AbsPath(trusted_path) => {
522 is_file || worktree_path.starts_with(trusted_path)
523 }
524 })
525 });
526 if auto_trusted {
527 return true;
528 }
529 }
530
531 self.restricted
532 .entry(weak_worktree_store.clone())
533 .or_default()
534 .insert(worktree_id);
535 log::info!("Worktree {worktree_path:?} is not trusted");
536 cx.emit(TrustedWorktreesEvent::Restricted(
537 weak_worktree_store,
538 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
539 ));
540 for (downstream_client, downstream_project_id) in &self.downstream_clients {
541 downstream_client
542 .send(proto::RestrictWorktrees {
543 project_id: downstream_project_id.0,
544 worktree_ids: vec![worktree_id.to_proto()],
545 })
546 .ok();
547 }
548 for (upstream_client, upstream_project_id) in &self.upstream_clients {
549 upstream_client
550 .send(proto::RestrictWorktrees {
551 project_id: upstream_project_id.0,
552 worktree_ids: vec![worktree_id.to_proto()],
553 })
554 .ok();
555 }
556 false
557 }
558
559 /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
560 pub fn restricted_worktrees(
561 &self,
562 worktree_store: &Entity<WorktreeStore>,
563 cx: &App,
564 ) -> HashSet<(WorktreeId, Arc<Path>)> {
565 let mut single_file_paths = HashSet::default();
566
567 let other_paths = self
568 .restricted
569 .get(&worktree_store.downgrade())
570 .into_iter()
571 .flatten()
572 .filter_map(|&restricted_worktree_id| {
573 let worktree = worktree_store
574 .read(cx)
575 .worktree_for_id(restricted_worktree_id, cx)?;
576 let worktree = worktree.read(cx);
577 let abs_path = worktree.abs_path();
578 if worktree.is_single_file() {
579 single_file_paths.insert((restricted_worktree_id, abs_path));
580 None
581 } else {
582 Some((restricted_worktree_id, abs_path))
583 }
584 })
585 .collect::<HashSet<_>>();
586
587 if !other_paths.is_empty() {
588 return other_paths;
589 } else {
590 single_file_paths
591 }
592 }
593
594 /// Switches the "trust nothing" mode to "automatically trust everything".
595 /// This does not influence already persisted data, but stops adding new worktrees there.
596 pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
597 for (worktree_store, worktrees) in std::mem::take(&mut self.restricted).into_iter().fold(
598 HashMap::default(),
599 |mut acc, (remote_host, worktrees)| {
600 acc.entry(remote_host)
601 .or_insert_with(HashSet::default)
602 .extend(worktrees.into_iter().map(PathTrust::Worktree));
603 acc
604 },
605 ) {
606 if let Some(worktree_store) = worktree_store.upgrade() {
607 self.trust(&worktree_store, worktrees, cx);
608 }
609 }
610 }
611
612 pub fn schedule_serialization<S>(&mut self, cx: &mut Context<Self>, serialize: S)
613 where
614 S: FnOnce(HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>, &App) -> Task<()>
615 + 'static,
616 {
617 self.worktree_trust_serialization = serialize(self.trusted_paths_for_serialization(cx), cx);
618 }
619
620 fn trusted_paths_for_serialization(
621 &mut self,
622 cx: &mut Context<Self>,
623 ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
624 let new_trusted_paths = self
625 .trusted_paths
626 .iter()
627 .filter_map(|(worktree_store, paths)| {
628 let host = self.worktree_stores.get(&worktree_store)?.clone();
629 let abs_paths = paths
630 .iter()
631 .flat_map(|path| match path {
632 PathTrust::Worktree(worktree_id) => worktree_store
633 .upgrade()
634 .and_then(|worktree_store| {
635 worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
636 })
637 .map(|worktree| worktree.read(cx).abs_path().to_path_buf()),
638 PathTrust::AbsPath(abs_path) => Some(abs_path.clone()),
639 })
640 .collect::<HashSet<_>>();
641 Some((host, abs_paths))
642 })
643 .chain(self.db_trusted_paths.drain())
644 .fold(HashMap::default(), |mut acc, (host, paths)| {
645 acc.entry(host)
646 .or_insert_with(HashSet::default)
647 .extend(paths);
648 acc
649 });
650
651 self.db_trusted_paths = new_trusted_paths.clone();
652 new_trusted_paths
653 }
654
655 fn add_worktree_store(
656 &mut self,
657 worktree_store: Entity<WorktreeStore>,
658 remote_host: Option<RemoteHostLocation>,
659 cx: &mut Context<Self>,
660 ) {
661 self.worktree_stores
662 .retain(|worktree_store, _| worktree_store.is_upgradable());
663 let weak_worktree_store = worktree_store.downgrade();
664 self.worktree_stores
665 .insert(weak_worktree_store.clone(), remote_host.clone());
666
667 let mut new_trusted_paths = HashSet::default();
668 if let Some(db_trusted_paths) = self.db_trusted_paths.get(&remote_host) {
669 new_trusted_paths.extend(db_trusted_paths.clone().into_iter().map(PathTrust::AbsPath));
670 }
671 if let Some(trusted_paths) = self.trusted_paths.remove(&weak_worktree_store) {
672 new_trusted_paths.extend(trusted_paths);
673 }
674 if !new_trusted_paths.is_empty() {
675 self.trusted_paths.insert(
676 weak_worktree_store,
677 new_trusted_paths
678 .into_iter()
679 .map(|path_trust| match path_trust {
680 PathTrust::AbsPath(abs_path) => {
681 find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
682 .map(|(worktree_id, _)| PathTrust::Worktree(worktree_id))
683 .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
684 }
685 other => other,
686 })
687 .collect(),
688 );
689 }
690 }
691}
692
693fn find_worktree_in_store(
694 worktree_store: &WorktreeStore,
695 abs_path: &Path,
696 cx: &App,
697) -> Option<(WorktreeId, bool)> {
698 let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
699 if path_in_worktree.is_empty() {
700 Some((worktree.read(cx).id(), worktree.read(cx).is_single_file()))
701 } else {
702 None
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use std::{cell::RefCell, path::PathBuf, rc::Rc};
709
710 use collections::HashSet;
711 use gpui::TestAppContext;
712 use serde_json::json;
713 use settings::SettingsStore;
714 use util::path;
715
716 use crate::{FakeFs, Project};
717
718 use super::*;
719
720 fn init_test(cx: &mut TestAppContext) {
721 cx.update(|cx| {
722 if cx.try_global::<SettingsStore>().is_none() {
723 let settings_store = SettingsStore::test(cx);
724 cx.set_global(settings_store);
725 }
726 if cx.try_global::<TrustedWorktrees>().is_some() {
727 cx.remove_global::<TrustedWorktrees>();
728 }
729 });
730 }
731
732 fn init_trust_global(
733 worktree_store: Entity<WorktreeStore>,
734 cx: &mut TestAppContext,
735 ) -> Entity<TrustedWorktreesStore> {
736 cx.update(|cx| {
737 init(HashMap::default(), None, None, cx);
738 track_worktree_trust(worktree_store, None, None, None, cx);
739 TrustedWorktrees::try_get_global(cx).expect("global should be set")
740 })
741 }
742
743 #[gpui::test]
744 async fn test_single_worktree_trust(cx: &mut TestAppContext) {
745 init_test(cx);
746
747 let fs = FakeFs::new(cx.executor());
748 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
749 .await;
750
751 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
752 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
753 let worktree_id = worktree_store.read_with(cx, |store, cx| {
754 store.worktrees().next().unwrap().read(cx).id()
755 });
756
757 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
758
759 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
760 cx.update({
761 let events = events.clone();
762 |cx| {
763 cx.subscribe(&trusted_worktrees, move |_, event, _| {
764 events.borrow_mut().push(match event {
765 TrustedWorktreesEvent::Trusted(host, paths) => {
766 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
767 }
768 TrustedWorktreesEvent::Restricted(host, paths) => {
769 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
770 }
771 });
772 })
773 }
774 })
775 .detach();
776
777 let can_trust = trusted_worktrees.update(cx, |store, cx| {
778 store.can_trust(&worktree_store, worktree_id, cx)
779 });
780 assert!(!can_trust, "worktree should be restricted by default");
781
782 {
783 let events = events.borrow();
784 assert_eq!(events.len(), 1);
785 match &events[0] {
786 TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
787 assert_eq!(event_worktree_store, &worktree_store.downgrade());
788 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
789 }
790 _ => panic!("expected Restricted event"),
791 }
792 }
793
794 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
795 store.has_restricted_worktrees(&worktree_store, cx)
796 });
797 assert!(has_restricted, "should have restricted worktrees");
798
799 let restricted = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
800 trusted_worktrees.restricted_worktrees(&worktree_store, cx)
801 });
802 assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
803
804 events.borrow_mut().clear();
805
806 let can_trust_again = trusted_worktrees.update(cx, |store, cx| {
807 store.can_trust(&worktree_store, worktree_id, cx)
808 });
809 assert!(!can_trust_again, "worktree should still be restricted");
810 assert!(
811 events.borrow().is_empty(),
812 "no duplicate Restricted event on repeated can_trust"
813 );
814
815 trusted_worktrees.update(cx, |store, cx| {
816 store.trust(
817 &worktree_store,
818 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
819 cx,
820 );
821 });
822
823 {
824 let events = events.borrow();
825 assert_eq!(events.len(), 1);
826 match &events[0] {
827 TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
828 assert_eq!(event_worktree_store, &worktree_store.downgrade());
829 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
830 }
831 _ => panic!("expected Trusted event"),
832 }
833 }
834
835 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
836 store.can_trust(&worktree_store, worktree_id, cx)
837 });
838 assert!(can_trust_after, "worktree should be trusted after trust()");
839
840 let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
841 store.has_restricted_worktrees(&worktree_store, cx)
842 });
843 assert!(
844 !has_restricted_after,
845 "should have no restricted worktrees after trust"
846 );
847
848 let restricted_after = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
849 trusted_worktrees.restricted_worktrees(&worktree_store, cx)
850 });
851 assert!(
852 restricted_after.is_empty(),
853 "restricted set should be empty"
854 );
855 }
856
857 #[gpui::test]
858 async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
859 init_test(cx);
860
861 let fs = FakeFs::new(cx.executor());
862 fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
863 .await;
864
865 let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
866 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
867 let worktree_id = worktree_store.read_with(cx, |store, cx| {
868 let worktree = store.worktrees().next().unwrap();
869 let worktree = worktree.read(cx);
870 assert!(worktree.is_single_file(), "expected single-file worktree");
871 worktree.id()
872 });
873
874 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
875
876 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
877 cx.update({
878 let events = events.clone();
879 |cx| {
880 cx.subscribe(&trusted_worktrees, move |_, event, _| {
881 events.borrow_mut().push(match event {
882 TrustedWorktreesEvent::Trusted(host, paths) => {
883 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
884 }
885 TrustedWorktreesEvent::Restricted(host, paths) => {
886 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
887 }
888 });
889 })
890 }
891 })
892 .detach();
893
894 let can_trust = trusted_worktrees.update(cx, |store, cx| {
895 store.can_trust(&worktree_store, worktree_id, cx)
896 });
897 assert!(
898 !can_trust,
899 "single-file worktree should be restricted by default"
900 );
901
902 {
903 let events = events.borrow();
904 assert_eq!(events.len(), 1);
905 match &events[0] {
906 TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
907 assert_eq!(event_worktree_store, &worktree_store.downgrade());
908 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
909 }
910 _ => panic!("expected Restricted event"),
911 }
912 }
913
914 events.borrow_mut().clear();
915
916 trusted_worktrees.update(cx, |store, cx| {
917 store.trust(
918 &worktree_store,
919 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
920 cx,
921 );
922 });
923
924 {
925 let events = events.borrow();
926 assert_eq!(events.len(), 1);
927 match &events[0] {
928 TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
929 assert_eq!(event_worktree_store, &worktree_store.downgrade());
930 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
931 }
932 _ => panic!("expected Trusted event"),
933 }
934 }
935
936 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
937 store.can_trust(&worktree_store, worktree_id, cx)
938 });
939 assert!(
940 can_trust_after,
941 "single-file worktree should be trusted after trust()"
942 );
943 }
944
945 #[gpui::test]
946 async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
947 init_test(cx);
948
949 let fs = FakeFs::new(cx.executor());
950 fs.insert_tree(
951 path!("/root"),
952 json!({
953 "a.rs": "fn a() {}",
954 "b.rs": "fn b() {}",
955 "c.rs": "fn c() {}"
956 }),
957 )
958 .await;
959
960 let project = Project::test(
961 fs,
962 [
963 path!("/root/a.rs").as_ref(),
964 path!("/root/b.rs").as_ref(),
965 path!("/root/c.rs").as_ref(),
966 ],
967 cx,
968 )
969 .await;
970 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
971 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
972 store
973 .worktrees()
974 .map(|worktree| {
975 let worktree = worktree.read(cx);
976 assert!(worktree.is_single_file());
977 worktree.id()
978 })
979 .collect()
980 });
981 assert_eq!(worktree_ids.len(), 3);
982
983 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
984
985 for &worktree_id in &worktree_ids {
986 let can_trust = trusted_worktrees.update(cx, |store, cx| {
987 store.can_trust(&worktree_store, worktree_id, cx)
988 });
989 assert!(
990 !can_trust,
991 "worktree {worktree_id:?} should be restricted initially"
992 );
993 }
994
995 trusted_worktrees.update(cx, |store, cx| {
996 store.trust(
997 &worktree_store,
998 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
999 cx,
1000 );
1001 });
1002
1003 let can_trust_0 = trusted_worktrees.update(cx, |store, cx| {
1004 store.can_trust(&worktree_store, worktree_ids[0], cx)
1005 });
1006 let can_trust_1 = trusted_worktrees.update(cx, |store, cx| {
1007 store.can_trust(&worktree_store, worktree_ids[1], cx)
1008 });
1009 let can_trust_2 = trusted_worktrees.update(cx, |store, cx| {
1010 store.can_trust(&worktree_store, worktree_ids[2], cx)
1011 });
1012
1013 assert!(!can_trust_0, "worktree 0 should still be restricted");
1014 assert!(can_trust_1, "worktree 1 should be trusted");
1015 assert!(!can_trust_2, "worktree 2 should still be restricted");
1016 }
1017
1018 #[gpui::test]
1019 async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
1020 init_test(cx);
1021
1022 let fs = FakeFs::new(cx.executor());
1023 fs.insert_tree(
1024 path!("/projects"),
1025 json!({
1026 "project_a": { "main.rs": "fn main() {}" },
1027 "project_b": { "lib.rs": "pub fn lib() {}" }
1028 }),
1029 )
1030 .await;
1031
1032 let project = Project::test(
1033 fs,
1034 [
1035 path!("/projects/project_a").as_ref(),
1036 path!("/projects/project_b").as_ref(),
1037 ],
1038 cx,
1039 )
1040 .await;
1041 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1042 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1043 store
1044 .worktrees()
1045 .map(|worktree| {
1046 let worktree = worktree.read(cx);
1047 assert!(!worktree.is_single_file());
1048 worktree.id()
1049 })
1050 .collect()
1051 });
1052 assert_eq!(worktree_ids.len(), 2);
1053
1054 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1055
1056 let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1057 store.can_trust(&worktree_store, worktree_ids[0], cx)
1058 });
1059 let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1060 store.can_trust(&worktree_store, worktree_ids[1], cx)
1061 });
1062 assert!(!can_trust_a, "project_a should be restricted initially");
1063 assert!(!can_trust_b, "project_b should be restricted initially");
1064
1065 trusted_worktrees.update(cx, |store, cx| {
1066 store.trust(
1067 &worktree_store,
1068 HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1069 cx,
1070 );
1071 });
1072
1073 let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1074 store.can_trust(&worktree_store, worktree_ids[0], cx)
1075 });
1076 let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1077 store.can_trust(&worktree_store, worktree_ids[1], cx)
1078 });
1079 assert!(can_trust_a, "project_a should be trusted after trust()");
1080 assert!(!can_trust_b, "project_b should still be restricted");
1081
1082 trusted_worktrees.update(cx, |store, cx| {
1083 store.trust(
1084 &worktree_store,
1085 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1086 cx,
1087 );
1088 });
1089
1090 let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1091 store.can_trust(&worktree_store, worktree_ids[0], cx)
1092 });
1093 let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1094 store.can_trust(&worktree_store, worktree_ids[1], cx)
1095 });
1096 assert!(can_trust_a, "project_a should remain trusted");
1097 assert!(can_trust_b, "project_b should now be trusted");
1098 }
1099
1100 #[gpui::test]
1101 async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1102 init_test(cx);
1103
1104 let fs = FakeFs::new(cx.executor());
1105 fs.insert_tree(
1106 path!("/"),
1107 json!({
1108 "project": { "main.rs": "fn main() {}" },
1109 "standalone.rs": "fn standalone() {}"
1110 }),
1111 )
1112 .await;
1113
1114 let project = Project::test(
1115 fs,
1116 [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1117 cx,
1118 )
1119 .await;
1120 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1121 let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1122 let worktrees: Vec<_> = store.worktrees().collect();
1123 assert_eq!(worktrees.len(), 2);
1124 let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1125 (&worktrees[1], &worktrees[0])
1126 } else {
1127 (&worktrees[0], &worktrees[1])
1128 };
1129 assert!(!dir_worktree.read(cx).is_single_file());
1130 assert!(file_worktree.read(cx).is_single_file());
1131 (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1132 });
1133
1134 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1135
1136 let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1137 store.can_trust(&worktree_store, file_worktree_id, cx)
1138 });
1139 assert!(
1140 !can_trust_file,
1141 "single-file worktree should be restricted initially"
1142 );
1143
1144 let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1145 store.can_trust(&worktree_store, dir_worktree_id, cx)
1146 });
1147 assert!(
1148 !can_trust_directory,
1149 "directory worktree should be restricted initially"
1150 );
1151
1152 trusted_worktrees.update(cx, |store, cx| {
1153 store.trust(
1154 &worktree_store,
1155 HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1156 cx,
1157 );
1158 });
1159
1160 let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1161 store.can_trust(&worktree_store, dir_worktree_id, cx)
1162 });
1163 let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1164 store.can_trust(&worktree_store, file_worktree_id, cx)
1165 });
1166 assert!(can_trust_dir, "directory worktree should be trusted");
1167 assert!(
1168 can_trust_file_after,
1169 "single-file worktree should be trusted after directory worktree trust"
1170 );
1171 }
1172
1173 #[gpui::test]
1174 async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
1175 init_test(cx);
1176
1177 let fs = FakeFs::new(cx.executor());
1178 fs.insert_tree(
1179 path!("/"),
1180 json!({
1181 "project": { "main.rs": "fn main() {}" },
1182 "standalone.rs": "fn standalone() {}"
1183 }),
1184 )
1185 .await;
1186
1187 let project = Project::test(
1188 fs,
1189 [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1190 cx,
1191 )
1192 .await;
1193 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1194 let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1195 let worktrees: Vec<_> = store.worktrees().collect();
1196 assert_eq!(worktrees.len(), 2);
1197 let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1198 (&worktrees[1], &worktrees[0])
1199 } else {
1200 (&worktrees[0], &worktrees[1])
1201 };
1202 assert!(!dir_worktree.read(cx).is_single_file());
1203 assert!(file_worktree.read(cx).is_single_file());
1204 (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1205 });
1206
1207 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1208
1209 let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1210 store.can_trust(&worktree_store, file_worktree_id, cx)
1211 });
1212 assert!(
1213 !can_trust_file,
1214 "single-file worktree should be restricted initially"
1215 );
1216
1217 let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1218 store.can_trust(&worktree_store, dir_worktree_id, cx)
1219 });
1220 assert!(
1221 !can_trust_directory,
1222 "directory worktree should be restricted initially"
1223 );
1224
1225 trusted_worktrees.update(cx, |store, cx| {
1226 store.trust(
1227 &worktree_store,
1228 HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
1229 cx,
1230 );
1231 });
1232
1233 let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1234 store.can_trust(&worktree_store, dir_worktree_id, cx)
1235 });
1236 let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1237 store.can_trust(&worktree_store, file_worktree_id, cx)
1238 });
1239 assert!(
1240 can_trust_dir,
1241 "directory worktree should be trusted after its parent is trusted"
1242 );
1243 assert!(
1244 can_trust_file_after,
1245 "single-file worktree should be trusted after directory worktree trust via its parent directory trust"
1246 );
1247 }
1248
1249 #[gpui::test]
1250 async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1251 init_test(cx);
1252
1253 let fs = FakeFs::new(cx.executor());
1254 fs.insert_tree(
1255 path!("/root"),
1256 json!({
1257 "project_a": { "main.rs": "fn main() {}" },
1258 "project_b": { "lib.rs": "pub fn lib() {}" }
1259 }),
1260 )
1261 .await;
1262
1263 let project = Project::test(
1264 fs,
1265 [
1266 path!("/root/project_a").as_ref(),
1267 path!("/root/project_b").as_ref(),
1268 ],
1269 cx,
1270 )
1271 .await;
1272 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1273 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1274 store
1275 .worktrees()
1276 .map(|worktree| worktree.read(cx).id())
1277 .collect()
1278 });
1279 assert_eq!(worktree_ids.len(), 2);
1280
1281 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1282
1283 for &worktree_id in &worktree_ids {
1284 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1285 store.can_trust(&worktree_store, worktree_id, cx)
1286 });
1287 assert!(!can_trust, "worktree should be restricted initially");
1288 }
1289
1290 trusted_worktrees.update(cx, |store, cx| {
1291 store.trust(
1292 &worktree_store,
1293 HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
1294 cx,
1295 );
1296 });
1297
1298 for &worktree_id in &worktree_ids {
1299 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1300 store.can_trust(&worktree_store, worktree_id, cx)
1301 });
1302 assert!(
1303 can_trust,
1304 "worktree should be trusted after parent path trust"
1305 );
1306 }
1307 }
1308
1309 #[gpui::test]
1310 async fn test_auto_trust_all(cx: &mut TestAppContext) {
1311 init_test(cx);
1312
1313 let fs = FakeFs::new(cx.executor());
1314 fs.insert_tree(
1315 path!("/"),
1316 json!({
1317 "project_a": { "main.rs": "fn main() {}" },
1318 "project_b": { "lib.rs": "pub fn lib() {}" },
1319 "single.rs": "fn single() {}"
1320 }),
1321 )
1322 .await;
1323
1324 let project = Project::test(
1325 fs,
1326 [
1327 path!("/project_a").as_ref(),
1328 path!("/project_b").as_ref(),
1329 path!("/single.rs").as_ref(),
1330 ],
1331 cx,
1332 )
1333 .await;
1334 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1335 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1336 store
1337 .worktrees()
1338 .map(|worktree| worktree.read(cx).id())
1339 .collect()
1340 });
1341 assert_eq!(worktree_ids.len(), 3);
1342
1343 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1344
1345 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1346 cx.update({
1347 let events = events.clone();
1348 |cx| {
1349 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1350 events.borrow_mut().push(match event {
1351 TrustedWorktreesEvent::Trusted(host, paths) => {
1352 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1353 }
1354 TrustedWorktreesEvent::Restricted(host, paths) => {
1355 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1356 }
1357 });
1358 })
1359 }
1360 })
1361 .detach();
1362
1363 for &worktree_id in &worktree_ids {
1364 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1365 store.can_trust(&worktree_store, worktree_id, cx)
1366 });
1367 assert!(!can_trust, "worktree should be restricted initially");
1368 }
1369
1370 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1371 store.has_restricted_worktrees(&worktree_store, cx)
1372 });
1373 assert!(has_restricted, "should have restricted worktrees");
1374
1375 events.borrow_mut().clear();
1376
1377 trusted_worktrees.update(cx, |store, cx| {
1378 store.auto_trust_all(cx);
1379 });
1380
1381 for &worktree_id in &worktree_ids {
1382 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1383 store.can_trust(&worktree_store, worktree_id, cx)
1384 });
1385 assert!(
1386 can_trust,
1387 "worktree {worktree_id:?} should be trusted after auto_trust_all"
1388 );
1389 }
1390
1391 let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1392 store.has_restricted_worktrees(&worktree_store, cx)
1393 });
1394 assert!(
1395 !has_restricted_after,
1396 "should have no restricted worktrees after auto_trust_all"
1397 );
1398
1399 let trusted_event_count = events
1400 .borrow()
1401 .iter()
1402 .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1403 .count();
1404 assert!(
1405 trusted_event_count > 0,
1406 "should have emitted Trusted events"
1407 );
1408 }
1409
1410 #[gpui::test]
1411 async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1412 init_test(cx);
1413
1414 let fs = FakeFs::new(cx.executor());
1415 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1416 .await;
1417
1418 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1419 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1420 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1421 store.worktrees().next().unwrap().read(cx).id()
1422 });
1423
1424 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1425
1426 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1427 cx.update({
1428 let events = events.clone();
1429 |cx| {
1430 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1431 events.borrow_mut().push(match event {
1432 TrustedWorktreesEvent::Trusted(host, paths) => {
1433 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1434 }
1435 TrustedWorktreesEvent::Restricted(host, paths) => {
1436 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1437 }
1438 });
1439 })
1440 }
1441 })
1442 .detach();
1443
1444 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1445 store.can_trust(&worktree_store, worktree_id, cx)
1446 });
1447 assert!(!can_trust, "should be restricted initially");
1448 assert_eq!(events.borrow().len(), 1);
1449 events.borrow_mut().clear();
1450
1451 trusted_worktrees.update(cx, |store, cx| {
1452 store.trust(
1453 &worktree_store,
1454 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1455 cx,
1456 );
1457 });
1458 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1459 store.can_trust(&worktree_store, worktree_id, cx)
1460 });
1461 assert!(can_trust, "should be trusted after trust()");
1462 assert_eq!(events.borrow().len(), 1);
1463 assert!(matches!(
1464 &events.borrow()[0],
1465 TrustedWorktreesEvent::Trusted(..)
1466 ));
1467 events.borrow_mut().clear();
1468
1469 trusted_worktrees.update(cx, |store, cx| {
1470 store.restrict(
1471 worktree_store.downgrade(),
1472 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1473 cx,
1474 );
1475 });
1476 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1477 store.can_trust(&worktree_store, worktree_id, cx)
1478 });
1479 assert!(!can_trust, "should be restricted after restrict()");
1480 assert_eq!(events.borrow().len(), 1);
1481 assert!(matches!(
1482 &events.borrow()[0],
1483 TrustedWorktreesEvent::Restricted(..)
1484 ));
1485
1486 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1487 store.has_restricted_worktrees(&worktree_store, cx)
1488 });
1489 assert!(has_restricted);
1490 events.borrow_mut().clear();
1491
1492 trusted_worktrees.update(cx, |store, cx| {
1493 store.trust(
1494 &worktree_store,
1495 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1496 cx,
1497 );
1498 });
1499 let can_trust = trusted_worktrees.update(cx, |store, cx| {
1500 store.can_trust(&worktree_store, worktree_id, cx)
1501 });
1502 assert!(can_trust, "should be trusted again after second trust()");
1503 assert_eq!(events.borrow().len(), 1);
1504 assert!(matches!(
1505 &events.borrow()[0],
1506 TrustedWorktreesEvent::Trusted(..)
1507 ));
1508
1509 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1510 store.has_restricted_worktrees(&worktree_store, cx)
1511 });
1512 assert!(!has_restricted);
1513 }
1514
1515 #[gpui::test]
1516 async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1517 init_test(cx);
1518
1519 let fs = FakeFs::new(cx.executor());
1520 fs.insert_tree(
1521 path!("/"),
1522 json!({
1523 "local_project": { "main.rs": "fn main() {}" },
1524 "remote_project": { "lib.rs": "pub fn lib() {}" }
1525 }),
1526 )
1527 .await;
1528
1529 let project = Project::test(
1530 fs,
1531 [
1532 path!("/local_project").as_ref(),
1533 path!("/remote_project").as_ref(),
1534 ],
1535 cx,
1536 )
1537 .await;
1538 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1539 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1540 store
1541 .worktrees()
1542 .map(|worktree| worktree.read(cx).id())
1543 .collect()
1544 });
1545 assert_eq!(worktree_ids.len(), 2);
1546 let local_worktree = worktree_ids[0];
1547 let _remote_worktree = worktree_ids[1];
1548
1549 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1550
1551 let can_trust_local = trusted_worktrees.update(cx, |store, cx| {
1552 store.can_trust(&worktree_store, local_worktree, cx)
1553 });
1554 assert!(!can_trust_local, "local worktree restricted on host_a");
1555
1556 trusted_worktrees.update(cx, |store, cx| {
1557 store.trust(
1558 &worktree_store,
1559 HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1560 cx,
1561 );
1562 });
1563
1564 let can_trust_local_after = trusted_worktrees.update(cx, |store, cx| {
1565 store.can_trust(&worktree_store, local_worktree, cx)
1566 });
1567 assert!(
1568 can_trust_local_after,
1569 "local worktree should be trusted on local host"
1570 );
1571 }
1572
1573 #[gpui::test]
1574 async fn test_invisible_worktree_stores_do_not_affect_trust(cx: &mut TestAppContext) {
1575 init_test(cx);
1576
1577 let fs = FakeFs::new(cx.executor());
1578 fs.insert_tree(
1579 path!("/"),
1580 json!({
1581 "visible": { "main.rs": "fn main() {}" },
1582 "other": { "a.rs": "fn other() {}" },
1583 "invisible": { "b.rs": "fn invisible() {}" }
1584 }),
1585 )
1586 .await;
1587
1588 let project = Project::test(fs, [path!("/visible").as_ref()], cx).await;
1589 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1590 let visible_worktree_id = worktree_store.read_with(cx, |store, cx| {
1591 store
1592 .worktrees()
1593 .find(|worktree| worktree.read(cx).root_dir().unwrap().ends_with("visible"))
1594 .expect("visible worktree")
1595 .read(cx)
1596 .id()
1597 });
1598 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1599
1600 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1601 cx.update({
1602 let events = events.clone();
1603 |cx| {
1604 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1605 events.borrow_mut().push(match event {
1606 TrustedWorktreesEvent::Trusted(host, paths) => {
1607 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1608 }
1609 TrustedWorktreesEvent::Restricted(host, paths) => {
1610 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1611 }
1612 });
1613 })
1614 }
1615 })
1616 .detach();
1617
1618 assert!(
1619 !trusted_worktrees.update(cx, |store, cx| {
1620 store.can_trust(&worktree_store, visible_worktree_id, cx)
1621 }),
1622 "visible worktree should be restricted initially"
1623 );
1624 assert_eq!(
1625 HashSet::from_iter([(visible_worktree_id)]),
1626 trusted_worktrees.read_with(cx, |store, _| {
1627 store
1628 .restricted
1629 .get(&worktree_store.downgrade())
1630 .unwrap()
1631 .clone()
1632 }),
1633 "only visible worktree should be restricted",
1634 );
1635
1636 let (new_visible_worktree, new_invisible_worktree) =
1637 worktree_store.update(cx, |worktree_store, cx| {
1638 let new_visible_worktree = worktree_store.create_worktree("/other", true, cx);
1639 let new_invisible_worktree =
1640 worktree_store.create_worktree("/invisible", false, cx);
1641 (new_visible_worktree, new_invisible_worktree)
1642 });
1643 let (new_visible_worktree, new_invisible_worktree) = (
1644 new_visible_worktree.await.unwrap(),
1645 new_invisible_worktree.await.unwrap(),
1646 );
1647
1648 let new_visible_worktree_id =
1649 new_visible_worktree.read_with(cx, |new_visible_worktree, _| new_visible_worktree.id());
1650 assert!(
1651 !trusted_worktrees.update(cx, |store, cx| {
1652 store.can_trust(&worktree_store, new_visible_worktree_id, cx)
1653 }),
1654 "new visible worktree should be restricted initially",
1655 );
1656 assert!(
1657 trusted_worktrees.update(cx, |store, cx| {
1658 store.can_trust(&worktree_store, new_invisible_worktree.read(cx).id(), cx)
1659 }),
1660 "invisible worktree should be skipped",
1661 );
1662 assert_eq!(
1663 HashSet::from_iter([visible_worktree_id, new_visible_worktree_id]),
1664 trusted_worktrees.read_with(cx, |store, _| {
1665 store
1666 .restricted
1667 .get(&worktree_store.downgrade())
1668 .unwrap()
1669 .clone()
1670 }),
1671 "only visible worktrees should be restricted"
1672 );
1673 }
1674}