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