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