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