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),
 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                let retain = (!is_file || new_trusted_other_worktrees.is_empty())
 341                    && new_trusted_abs_paths.iter().all(|new_trusted_path| {
 342                        !restricted_worktree_path.starts_with(new_trusted_path)
 343                    });
 344                if !retain {
 345                    trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 346                }
 347                retain
 348            })
 349            .collect();
 350
 351        {
 352            let trusted_paths = self.trusted_paths.entry(remote_host.clone()).or_default();
 353            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
 354            trusted_paths.extend(
 355                new_trusted_other_worktrees
 356                    .into_iter()
 357                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
 358            );
 359            trusted_paths.extend(
 360                new_trusted_single_file_worktrees
 361                    .into_iter()
 362                    .map(PathTrust::Worktree),
 363            );
 364        }
 365
 366        cx.emit(TrustedWorktreesEvent::Trusted(
 367            remote_host,
 368            trusted_paths.clone(),
 369        ));
 370
 371        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 372            let trusted_paths = trusted_paths
 373                .iter()
 374                .map(|trusted_path| trusted_path.to_proto())
 375                .collect::<Vec<_>>();
 376            if !trusted_paths.is_empty() {
 377                upstream_client
 378                    .send(proto::TrustWorktrees {
 379                        project_id: *upstream_project_id,
 380                        trusted_paths,
 381                    })
 382                    .ok();
 383            }
 384        }
 385    }
 386
 387    /// Restricts certain entities on this host.
 388    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
 389    pub fn restrict(
 390        &mut self,
 391        restricted_paths: HashSet<PathTrust>,
 392        remote_host: Option<RemoteHostLocation>,
 393        cx: &mut Context<Self>,
 394    ) {
 395        for restricted_path in restricted_paths {
 396            match restricted_path {
 397                PathTrust::Worktree(worktree_id) => {
 398                    self.restricted.insert(worktree_id);
 399                    cx.emit(TrustedWorktreesEvent::Restricted(
 400                        remote_host.clone(),
 401                        HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 402                    ));
 403                }
 404                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
 405            }
 406        }
 407    }
 408
 409    /// Erases all trust information.
 410    /// Requires Zed's restart to take proper effect.
 411    pub fn clear_trusted_paths(&mut self) {
 412        self.trusted_paths.clear();
 413    }
 414
 415    /// Checks whether a certain worktree is trusted (or on a larger trust level).
 416    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
 417    ///
 418    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 419    pub fn can_trust(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) -> bool {
 420        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 421            return true;
 422        }
 423        if self.restricted.contains(&worktree_id) {
 424            return false;
 425        }
 426
 427        let Some((worktree_path, is_file, remote_host)) = self.find_worktree_data(worktree_id, cx)
 428        else {
 429            return false;
 430        };
 431
 432        if self
 433            .trusted_paths
 434            .get(&remote_host)
 435            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
 436        {
 437            return true;
 438        }
 439
 440        // See module documentation for details on trust level.
 441        if is_file && self.trusted_paths.contains_key(&remote_host) {
 442            return true;
 443        }
 444
 445        let parent_path_trusted =
 446            self.trusted_paths
 447                .get(&remote_host)
 448                .is_some_and(|trusted_paths| {
 449                    trusted_paths.iter().any(|trusted_path| {
 450                        let PathTrust::AbsPath(trusted_path) = trusted_path else {
 451                            return false;
 452                        };
 453                        worktree_path.starts_with(trusted_path)
 454                    })
 455                });
 456        if parent_path_trusted {
 457            return true;
 458        }
 459
 460        self.restricted.insert(worktree_id);
 461        cx.emit(TrustedWorktreesEvent::Restricted(
 462            remote_host,
 463            HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 464        ));
 465        if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
 466            downstream_client
 467                .send(proto::RestrictWorktrees {
 468                    project_id: *downstream_project_id,
 469                    worktree_ids: vec![worktree_id.to_proto()],
 470                })
 471                .ok();
 472        }
 473        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 474            upstream_client
 475                .send(proto::RestrictWorktrees {
 476                    project_id: *upstream_project_id,
 477                    worktree_ids: vec![worktree_id.to_proto()],
 478                })
 479                .ok();
 480        }
 481        false
 482    }
 483
 484    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
 485    pub fn restricted_worktrees(
 486        &self,
 487        worktree_store: &WorktreeStore,
 488        cx: &App,
 489    ) -> HashSet<(WorktreeId, Arc<Path>)> {
 490        let mut single_file_paths = HashSet::default();
 491        let other_paths = self
 492            .restricted
 493            .iter()
 494            .filter_map(|&restricted_worktree_id| {
 495                let worktree = worktree_store.worktree_for_id(restricted_worktree_id, cx)?;
 496                let worktree = worktree.read(cx);
 497                let abs_path = worktree.abs_path();
 498                if worktree.is_single_file() {
 499                    single_file_paths.insert((restricted_worktree_id, abs_path));
 500                    None
 501                } else {
 502                    Some((restricted_worktree_id, abs_path))
 503                }
 504            })
 505            .collect::<HashSet<_>>();
 506
 507        if !other_paths.is_empty() {
 508            return other_paths;
 509        } else {
 510            single_file_paths
 511        }
 512    }
 513
 514    /// Switches the "trust nothing" mode to "automatically trust everything".
 515    /// This does not influence already persisted data, but stops adding new worktrees there.
 516    pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
 517        for (remote_host, worktrees) in std::mem::take(&mut self.restricted)
 518            .into_iter()
 519            .flat_map(|restricted_worktree| {
 520                let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?;
 521                Some((restricted_worktree, host))
 522            })
 523            .fold(HashMap::default(), |mut acc, (worktree_id, remote_host)| {
 524                acc.entry(remote_host)
 525                    .or_insert_with(HashSet::default)
 526                    .insert(PathTrust::Worktree(worktree_id));
 527                acc
 528            })
 529        {
 530            self.trust(worktrees, remote_host, cx);
 531        }
 532    }
 533
 534    /// Returns a normalized representation of the trusted paths to store in the DB.
 535    pub fn trusted_paths_for_serialization(
 536        &mut self,
 537        cx: &mut Context<Self>,
 538    ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
 539        let new_trusted_worktrees = self
 540            .trusted_paths
 541            .clone()
 542            .into_iter()
 543            .map(|(host, paths)| {
 544                let abs_paths = paths
 545                    .into_iter()
 546                    .flat_map(|path| match path {
 547                        PathTrust::Worktree(worktree_id) => self
 548                            .find_worktree_data(worktree_id, cx)
 549                            .map(|(abs_path, ..)| abs_path.to_path_buf()),
 550                        PathTrust::AbsPath(abs_path) => Some(abs_path),
 551                    })
 552                    .collect();
 553                (host, abs_paths)
 554            })
 555            .collect();
 556        new_trusted_worktrees
 557    }
 558
 559    fn find_worktree_data(
 560        &mut self,
 561        worktree_id: WorktreeId,
 562        cx: &mut Context<Self>,
 563    ) -> Option<(Arc<Path>, bool, Option<RemoteHostLocation>)> {
 564        let mut worktree_data = None;
 565        self.worktree_stores.retain(
 566            |worktree_store, remote_host| match worktree_store.upgrade() {
 567                Some(worktree_store) => {
 568                    if worktree_data.is_none() {
 569                        if let Some(worktree) =
 570                            worktree_store.read(cx).worktree_for_id(worktree_id, cx)
 571                        {
 572                            worktree_data = Some((
 573                                worktree.read(cx).abs_path(),
 574                                worktree.read(cx).is_single_file(),
 575                                remote_host.clone(),
 576                            ));
 577                        }
 578                    }
 579                    true
 580                }
 581                None => false,
 582            },
 583        );
 584        worktree_data
 585    }
 586
 587    fn add_worktree_store(
 588        &mut self,
 589        worktree_store: Entity<WorktreeStore>,
 590        remote_host: Option<RemoteHostLocation>,
 591        cx: &mut Context<Self>,
 592    ) {
 593        self.worktree_stores
 594            .insert(worktree_store.downgrade(), remote_host.clone());
 595
 596        if let Some(trusted_paths) = self.trusted_paths.remove(&remote_host) {
 597            self.trusted_paths.insert(
 598                remote_host.clone(),
 599                trusted_paths
 600                    .into_iter()
 601                    .map(|path_trust| match path_trust {
 602                        PathTrust::AbsPath(abs_path) => {
 603                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
 604                                .map(PathTrust::Worktree)
 605                                .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
 606                        }
 607                        other => other,
 608                    })
 609                    .collect(),
 610            );
 611        }
 612    }
 613}
 614
 615pub fn find_worktree_in_store(
 616    worktree_store: &WorktreeStore,
 617    abs_path: &Path,
 618    cx: &App,
 619) -> Option<WorktreeId> {
 620    let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
 621    if path_in_worktree.is_empty() {
 622        Some(worktree.read(cx).id())
 623    } else {
 624        None
 625    }
 626}
 627
 628#[cfg(test)]
 629mod tests {
 630    use std::{cell::RefCell, path::PathBuf, rc::Rc};
 631
 632    use collections::HashSet;
 633    use gpui::TestAppContext;
 634    use serde_json::json;
 635    use settings::SettingsStore;
 636    use util::path;
 637
 638    use crate::{FakeFs, Project};
 639
 640    use super::*;
 641
 642    fn init_test(cx: &mut TestAppContext) {
 643        cx.update(|cx| {
 644            if cx.try_global::<SettingsStore>().is_none() {
 645                let settings_store = SettingsStore::test(cx);
 646                cx.set_global(settings_store);
 647            }
 648            if cx.try_global::<TrustedWorktrees>().is_some() {
 649                cx.remove_global::<TrustedWorktrees>();
 650            }
 651        });
 652    }
 653
 654    fn init_trust_global(
 655        worktree_store: Entity<WorktreeStore>,
 656        cx: &mut TestAppContext,
 657    ) -> Entity<TrustedWorktreesStore> {
 658        cx.update(|cx| {
 659            init(HashMap::default(), None, None, cx);
 660            track_worktree_trust(worktree_store, None, None, None, cx);
 661            TrustedWorktrees::try_get_global(cx).expect("global should be set")
 662        })
 663    }
 664
 665    #[gpui::test]
 666    async fn test_single_worktree_trust(cx: &mut TestAppContext) {
 667        init_test(cx);
 668
 669        let fs = FakeFs::new(cx.executor());
 670        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
 671            .await;
 672
 673        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 674        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 675        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 676            store.worktrees().next().unwrap().read(cx).id()
 677        });
 678
 679        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 680
 681        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 682        cx.update({
 683            let events = events.clone();
 684            |cx| {
 685                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 686                    events.borrow_mut().push(match event {
 687                        TrustedWorktreesEvent::Trusted(host, paths) => {
 688                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 689                        }
 690                        TrustedWorktreesEvent::Restricted(host, paths) => {
 691                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 692                        }
 693                    });
 694                })
 695            }
 696        })
 697        .detach();
 698
 699        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 700        assert!(!can_trust, "worktree should be restricted by default");
 701
 702        {
 703            let events = events.borrow();
 704            assert_eq!(events.len(), 1);
 705            match &events[0] {
 706                TrustedWorktreesEvent::Restricted(host, paths) => {
 707                    assert!(host.is_none());
 708                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 709                }
 710                _ => panic!("expected Restricted event"),
 711            }
 712        }
 713
 714        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
 715            store.has_restricted_worktrees(&worktree_store, cx)
 716        });
 717        assert!(has_restricted, "should have restricted worktrees");
 718
 719        let restricted = worktree_store.read_with(cx, |ws, cx| {
 720            trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
 721        });
 722        assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
 723
 724        events.borrow_mut().clear();
 725
 726        let can_trust_again =
 727            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 728        assert!(!can_trust_again, "worktree should still be restricted");
 729        assert!(
 730            events.borrow().is_empty(),
 731            "no duplicate Restricted event on repeated can_trust"
 732        );
 733
 734        trusted_worktrees.update(cx, |store, cx| {
 735            store.trust(
 736                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 737                None,
 738                cx,
 739            );
 740        });
 741
 742        {
 743            let events = events.borrow();
 744            assert_eq!(events.len(), 1);
 745            match &events[0] {
 746                TrustedWorktreesEvent::Trusted(host, paths) => {
 747                    assert!(host.is_none());
 748                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 749                }
 750                _ => panic!("expected Trusted event"),
 751            }
 752        }
 753
 754        let can_trust_after =
 755            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 756        assert!(can_trust_after, "worktree should be trusted after trust()");
 757
 758        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
 759            store.has_restricted_worktrees(&worktree_store, cx)
 760        });
 761        assert!(
 762            !has_restricted_after,
 763            "should have no restricted worktrees after trust"
 764        );
 765
 766        let restricted_after = worktree_store.read_with(cx, |ws, cx| {
 767            trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
 768        });
 769        assert!(
 770            restricted_after.is_empty(),
 771            "restricted set should be empty"
 772        );
 773    }
 774
 775    #[gpui::test]
 776    async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
 777        init_test(cx);
 778
 779        let fs = FakeFs::new(cx.executor());
 780        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
 781            .await;
 782
 783        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
 784        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 785        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 786            let worktree = store.worktrees().next().unwrap();
 787            let worktree = worktree.read(cx);
 788            assert!(worktree.is_single_file(), "expected single-file worktree");
 789            worktree.id()
 790        });
 791
 792        let trusted_worktrees = init_trust_global(worktree_store, cx);
 793
 794        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 795        cx.update({
 796            let events = events.clone();
 797            |cx| {
 798                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 799                    events.borrow_mut().push(match event {
 800                        TrustedWorktreesEvent::Trusted(host, paths) => {
 801                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 802                        }
 803                        TrustedWorktreesEvent::Restricted(host, paths) => {
 804                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 805                        }
 806                    });
 807                })
 808            }
 809        })
 810        .detach();
 811
 812        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 813        assert!(
 814            !can_trust,
 815            "single-file worktree should be restricted by default"
 816        );
 817
 818        {
 819            let events = events.borrow();
 820            assert_eq!(events.len(), 1);
 821            match &events[0] {
 822                TrustedWorktreesEvent::Restricted(host, paths) => {
 823                    assert!(host.is_none());
 824                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 825                }
 826                _ => panic!("expected Restricted event"),
 827            }
 828        }
 829
 830        events.borrow_mut().clear();
 831
 832        trusted_worktrees.update(cx, |store, cx| {
 833            store.trust(
 834                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 835                None,
 836                cx,
 837            );
 838        });
 839
 840        {
 841            let events = events.borrow();
 842            assert_eq!(events.len(), 1);
 843            match &events[0] {
 844                TrustedWorktreesEvent::Trusted(host, paths) => {
 845                    assert!(host.is_none());
 846                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 847                }
 848                _ => panic!("expected Trusted event"),
 849            }
 850        }
 851
 852        let can_trust_after =
 853            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 854        assert!(
 855            can_trust_after,
 856            "single-file worktree should be trusted after trust()"
 857        );
 858    }
 859
 860    #[gpui::test]
 861    async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
 862        init_test(cx);
 863
 864        let fs = FakeFs::new(cx.executor());
 865        fs.insert_tree(
 866            path!("/root"),
 867            json!({
 868                "a.rs": "fn a() {}",
 869                "b.rs": "fn b() {}",
 870                "c.rs": "fn c() {}"
 871            }),
 872        )
 873        .await;
 874
 875        let project = Project::test(
 876            fs,
 877            [
 878                path!("/root/a.rs").as_ref(),
 879                path!("/root/b.rs").as_ref(),
 880                path!("/root/c.rs").as_ref(),
 881            ],
 882            cx,
 883        )
 884        .await;
 885        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 886        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
 887            store
 888                .worktrees()
 889                .map(|worktree| {
 890                    let worktree = worktree.read(cx);
 891                    assert!(worktree.is_single_file());
 892                    worktree.id()
 893                })
 894                .collect()
 895        });
 896        assert_eq!(worktree_ids.len(), 3);
 897
 898        let trusted_worktrees = init_trust_global(worktree_store, cx);
 899
 900        for &worktree_id in &worktree_ids {
 901            let can_trust =
 902                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 903            assert!(
 904                !can_trust,
 905                "worktree {worktree_id:?} should be restricted initially"
 906            );
 907        }
 908
 909        trusted_worktrees.update(cx, |store, cx| {
 910            store.trust(
 911                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
 912                None,
 913                cx,
 914            );
 915        });
 916
 917        let can_trust_0 =
 918            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
 919        let can_trust_1 =
 920            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
 921        let can_trust_2 =
 922            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[2], cx));
 923
 924        assert!(!can_trust_0, "worktree 0 should still be restricted");
 925        assert!(can_trust_1, "worktree 1 should be trusted");
 926        assert!(!can_trust_2, "worktree 2 should still be restricted");
 927    }
 928
 929    #[gpui::test]
 930    async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
 931        init_test(cx);
 932
 933        let fs = FakeFs::new(cx.executor());
 934        fs.insert_tree(
 935            path!("/projects"),
 936            json!({
 937                "project_a": { "main.rs": "fn main() {}" },
 938                "project_b": { "lib.rs": "pub fn lib() {}" }
 939            }),
 940        )
 941        .await;
 942
 943        let project = Project::test(
 944            fs,
 945            [
 946                path!("/projects/project_a").as_ref(),
 947                path!("/projects/project_b").as_ref(),
 948            ],
 949            cx,
 950        )
 951        .await;
 952        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 953        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
 954            store
 955                .worktrees()
 956                .map(|worktree| {
 957                    let worktree = worktree.read(cx);
 958                    assert!(!worktree.is_single_file());
 959                    worktree.id()
 960                })
 961                .collect()
 962        });
 963        assert_eq!(worktree_ids.len(), 2);
 964
 965        let trusted_worktrees = init_trust_global(worktree_store, cx);
 966
 967        let can_trust_a =
 968            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
 969        let can_trust_b =
 970            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
 971        assert!(!can_trust_a, "project_a should be restricted initially");
 972        assert!(!can_trust_b, "project_b should be restricted initially");
 973
 974        trusted_worktrees.update(cx, |store, cx| {
 975            store.trust(
 976                HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
 977                None,
 978                cx,
 979            );
 980        });
 981
 982        let can_trust_a =
 983            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
 984        let can_trust_b =
 985            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
 986        assert!(can_trust_a, "project_a should be trusted after trust()");
 987        assert!(!can_trust_b, "project_b should still be restricted");
 988
 989        trusted_worktrees.update(cx, |store, cx| {
 990            store.trust(
 991                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
 992                None,
 993                cx,
 994            );
 995        });
 996
 997        let can_trust_a =
 998            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
 999        let can_trust_b =
1000            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1001        assert!(can_trust_a, "project_a should remain trusted");
1002        assert!(can_trust_b, "project_b should now be trusted");
1003    }
1004
1005    #[gpui::test]
1006    async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1007        init_test(cx);
1008
1009        let fs = FakeFs::new(cx.executor());
1010        fs.insert_tree(
1011            path!("/"),
1012            json!({
1013                "project": { "main.rs": "fn main() {}" },
1014                "standalone.rs": "fn standalone() {}"
1015            }),
1016        )
1017        .await;
1018
1019        let project = Project::test(
1020            fs,
1021            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1022            cx,
1023        )
1024        .await;
1025        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1026        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1027            let worktrees: Vec<_> = store.worktrees().collect();
1028            assert_eq!(worktrees.len(), 2);
1029            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1030                (&worktrees[1], &worktrees[0])
1031            } else {
1032                (&worktrees[0], &worktrees[1])
1033            };
1034            assert!(!dir_worktree.read(cx).is_single_file());
1035            assert!(file_worktree.read(cx).is_single_file());
1036            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1037        });
1038
1039        let trusted_worktrees = init_trust_global(worktree_store, cx);
1040
1041        let can_trust_file =
1042            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1043        assert!(
1044            !can_trust_file,
1045            "single-file worktree should be restricted initially"
1046        );
1047
1048        trusted_worktrees.update(cx, |store, cx| {
1049            store.trust(
1050                HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1051                None,
1052                cx,
1053            );
1054        });
1055
1056        let can_trust_dir =
1057            trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
1058        let can_trust_file_after =
1059            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1060        assert!(can_trust_dir, "directory worktree should be trusted");
1061        assert!(
1062            can_trust_file_after,
1063            "single-file worktree should be trusted after directory worktree trust"
1064        );
1065    }
1066
1067    #[gpui::test]
1068    async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1069        init_test(cx);
1070
1071        let fs = FakeFs::new(cx.executor());
1072        fs.insert_tree(
1073            path!("/root"),
1074            json!({
1075                "project_a": { "main.rs": "fn main() {}" },
1076                "project_b": { "lib.rs": "pub fn lib() {}" }
1077            }),
1078        )
1079        .await;
1080
1081        let project = Project::test(
1082            fs,
1083            [
1084                path!("/root/project_a").as_ref(),
1085                path!("/root/project_b").as_ref(),
1086            ],
1087            cx,
1088        )
1089        .await;
1090        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1091        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1092            store
1093                .worktrees()
1094                .map(|worktree| worktree.read(cx).id())
1095                .collect()
1096        });
1097        assert_eq!(worktree_ids.len(), 2);
1098
1099        let trusted_worktrees = init_trust_global(worktree_store, cx);
1100
1101        for &worktree_id in &worktree_ids {
1102            let can_trust =
1103                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1104            assert!(!can_trust, "worktree should be restricted initially");
1105        }
1106
1107        trusted_worktrees.update(cx, |store, cx| {
1108            store.trust(
1109                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
1110                None,
1111                cx,
1112            );
1113        });
1114
1115        for &worktree_id in &worktree_ids {
1116            let can_trust =
1117                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1118            assert!(
1119                can_trust,
1120                "worktree should be trusted after parent path trust"
1121            );
1122        }
1123    }
1124
1125    #[gpui::test]
1126    async fn test_auto_trust_all(cx: &mut TestAppContext) {
1127        init_test(cx);
1128
1129        let fs = FakeFs::new(cx.executor());
1130        fs.insert_tree(
1131            path!("/"),
1132            json!({
1133                "project_a": { "main.rs": "fn main() {}" },
1134                "project_b": { "lib.rs": "pub fn lib() {}" },
1135                "single.rs": "fn single() {}"
1136            }),
1137        )
1138        .await;
1139
1140        let project = Project::test(
1141            fs,
1142            [
1143                path!("/project_a").as_ref(),
1144                path!("/project_b").as_ref(),
1145                path!("/single.rs").as_ref(),
1146            ],
1147            cx,
1148        )
1149        .await;
1150        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1151        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1152            store
1153                .worktrees()
1154                .map(|worktree| worktree.read(cx).id())
1155                .collect()
1156        });
1157        assert_eq!(worktree_ids.len(), 3);
1158
1159        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1160
1161        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1162        cx.update({
1163            let events = events.clone();
1164            |cx| {
1165                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1166                    events.borrow_mut().push(match event {
1167                        TrustedWorktreesEvent::Trusted(host, paths) => {
1168                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1169                        }
1170                        TrustedWorktreesEvent::Restricted(host, paths) => {
1171                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1172                        }
1173                    });
1174                })
1175            }
1176        })
1177        .detach();
1178
1179        for &worktree_id in &worktree_ids {
1180            let can_trust =
1181                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1182            assert!(!can_trust, "worktree should be restricted initially");
1183        }
1184
1185        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1186            store.has_restricted_worktrees(&worktree_store, cx)
1187        });
1188        assert!(has_restricted, "should have restricted worktrees");
1189
1190        events.borrow_mut().clear();
1191
1192        trusted_worktrees.update(cx, |store, cx| {
1193            store.auto_trust_all(cx);
1194        });
1195
1196        for &worktree_id in &worktree_ids {
1197            let can_trust =
1198                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1199            assert!(
1200                can_trust,
1201                "worktree {worktree_id:?} should be trusted after auto_trust_all"
1202            );
1203        }
1204
1205        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1206            store.has_restricted_worktrees(&worktree_store, cx)
1207        });
1208        assert!(
1209            !has_restricted_after,
1210            "should have no restricted worktrees after auto_trust_all"
1211        );
1212
1213        let trusted_event_count = events
1214            .borrow()
1215            .iter()
1216            .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1217            .count();
1218        assert!(
1219            trusted_event_count > 0,
1220            "should have emitted Trusted events"
1221        );
1222    }
1223
1224    #[gpui::test]
1225    async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1226        init_test(cx);
1227
1228        let fs = FakeFs::new(cx.executor());
1229        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1230            .await;
1231
1232        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1233        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1234        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1235            store.worktrees().next().unwrap().read(cx).id()
1236        });
1237
1238        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1239
1240        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1241        cx.update({
1242            let events = events.clone();
1243            |cx| {
1244                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1245                    events.borrow_mut().push(match event {
1246                        TrustedWorktreesEvent::Trusted(host, paths) => {
1247                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1248                        }
1249                        TrustedWorktreesEvent::Restricted(host, paths) => {
1250                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1251                        }
1252                    });
1253                })
1254            }
1255        })
1256        .detach();
1257
1258        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1259        assert!(!can_trust, "should be restricted initially");
1260        assert_eq!(events.borrow().len(), 1);
1261        events.borrow_mut().clear();
1262
1263        trusted_worktrees.update(cx, |store, cx| {
1264            store.trust(
1265                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1266                None,
1267                cx,
1268            );
1269        });
1270        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1271        assert!(can_trust, "should be trusted after trust()");
1272        assert_eq!(events.borrow().len(), 1);
1273        assert!(matches!(
1274            &events.borrow()[0],
1275            TrustedWorktreesEvent::Trusted(..)
1276        ));
1277        events.borrow_mut().clear();
1278
1279        trusted_worktrees.update(cx, |store, cx| {
1280            store.restrict(
1281                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1282                None,
1283                cx,
1284            );
1285        });
1286        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1287        assert!(!can_trust, "should be restricted after restrict()");
1288        assert_eq!(events.borrow().len(), 1);
1289        assert!(matches!(
1290            &events.borrow()[0],
1291            TrustedWorktreesEvent::Restricted(..)
1292        ));
1293
1294        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1295            store.has_restricted_worktrees(&worktree_store, cx)
1296        });
1297        assert!(has_restricted);
1298        events.borrow_mut().clear();
1299
1300        trusted_worktrees.update(cx, |store, cx| {
1301            store.trust(
1302                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1303                None,
1304                cx,
1305            );
1306        });
1307        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1308        assert!(can_trust, "should be trusted again after second trust()");
1309        assert_eq!(events.borrow().len(), 1);
1310        assert!(matches!(
1311            &events.borrow()[0],
1312            TrustedWorktreesEvent::Trusted(..)
1313        ));
1314
1315        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1316            store.has_restricted_worktrees(&worktree_store, cx)
1317        });
1318        assert!(!has_restricted);
1319    }
1320
1321    #[gpui::test]
1322    async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1323        init_test(cx);
1324
1325        let fs = FakeFs::new(cx.executor());
1326        fs.insert_tree(
1327            path!("/"),
1328            json!({
1329                "local_project": { "main.rs": "fn main() {}" },
1330                "remote_project": { "lib.rs": "pub fn lib() {}" }
1331            }),
1332        )
1333        .await;
1334
1335        let project = Project::test(
1336            fs,
1337            [
1338                path!("/local_project").as_ref(),
1339                path!("/remote_project").as_ref(),
1340            ],
1341            cx,
1342        )
1343        .await;
1344        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1345        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1346            store
1347                .worktrees()
1348                .map(|worktree| worktree.read(cx).id())
1349                .collect()
1350        });
1351        assert_eq!(worktree_ids.len(), 2);
1352        let local_worktree = worktree_ids[0];
1353        let _remote_worktree = worktree_ids[1];
1354
1355        let trusted_worktrees = init_trust_global(worktree_store, cx);
1356
1357        let host_a: Option<RemoteHostLocation> = None;
1358
1359        let can_trust_local =
1360            trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1361        assert!(!can_trust_local, "local worktree restricted on host_a");
1362
1363        trusted_worktrees.update(cx, |store, cx| {
1364            store.trust(
1365                HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1366                host_a.clone(),
1367                cx,
1368            );
1369        });
1370
1371        let can_trust_local_after =
1372            trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1373        assert!(
1374            can_trust_local_after,
1375            "local worktree should be trusted on host_a"
1376        );
1377    }
1378}