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