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