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