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