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