trusted_worktrees.rs

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