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