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