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