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