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