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