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