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