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