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