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