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