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