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