6401 lines · 224.0 KB · Rust
RawDownload
mod ignore;mod worktree_settings;#[cfg(test)]mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};use anyhow::{anyhow, Context as _, Result};use clock::ReplicaId;use collections::{HashMap, HashSet, VecDeque};use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher};use futures::{    channel::{        mpsc::{self, UnboundedSender},        oneshot,    },    future::join_all,    select_biased,    task::Poll,    FutureExt as _, Stream, StreamExt,};use fuzzy::CharBag;use git::{    repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},    status::{        FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,    },    GitHostingProviderRegistry, COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK,    LFS_DIR,};use gpui::{    App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,};use ignore::IgnoreStack;use language::DiskState; use parking_lot::Mutex;use paths::local_settings_folder_relative_path;use postage::{    barrier,    prelude::{Sink as _, Stream as _},    watch,};use rpc::{    proto::{self, split_worktree_update, FromProto, ToProto},    AnyProtoClient,};pub use settings::WorktreeId;use settings::{Settings, SettingsLocation, SettingsStore};use smallvec::{smallvec, SmallVec};use smol::channel::{self, Sender};use std::{    any::Any,    cmp::Ordering,    collections::hash_map,    convert::TryFrom,    ffi::OsStr,    fmt,    future::Future,    mem::{self},    ops::{Deref, DerefMut},    path::{Component, Path, PathBuf},    pin::Pin,    sync::{        atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},        Arc,    },    time::{Duration, Instant},};use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit};use text::{LineEnding, Rope};use util::{    paths::{home_dir, PathMatcher, SanitizedPath},    ResultExt,};pub use worktree_settings::WorktreeSettings; pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); /// A set of local or remote files that are being opened as part of a project./// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates./// Stores git repositories data and the diagnostics for the file(s).////// Has an absolute path, and may be set to be visible in Zed UI or not./// May correspond to a directory or a single file./// Possible examples:/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree/// * a directory opened in Zed — may be added as a visible entry to the current worktree////// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.pub enum Worktree {    Local(LocalWorktree),    Remote(RemoteWorktree),} /// An entry, created in the worktree.#[derive(Debug)]pub enum CreatedEntry {    /// Got created and indexed by the worktree, receiving a corresponding entry.    Included(Entry),    /// Got created, but not indexed due to falling under exclusion filters.    Excluded { abs_path: PathBuf },} pub struct LoadedFile {    pub file: Arc<File>,    pub text: String,} pub struct LoadedBinaryFile {    pub file: Arc<File>,    pub content: Vec<u8>,} pub struct LocalWorktree {    snapshot: LocalSnapshot,    scan_requests_tx: channel::Sender<ScanRequest>,    path_prefixes_to_scan_tx: channel::Sender<PathPrefixScanRequest>,    is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),    _background_scanner_tasks: Vec<Task<()>>,    update_observer: Option<UpdateObservationState>,    fs: Arc<dyn Fs>,    fs_case_sensitive: bool,    visible: bool,    next_entry_id: Arc<AtomicUsize>,    settings: WorktreeSettings,    share_private_files: bool,} pub struct PathPrefixScanRequest {    path: Arc<Path>,    done: SmallVec<[barrier::Sender; 1]>,} struct ScanRequest {    relative_paths: Vec<Arc<Path>>,    done: SmallVec<[barrier::Sender; 1]>,} pub struct RemoteWorktree {    snapshot: Snapshot,    background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,    project_id: u64,    client: AnyProtoClient,    file_scan_inclusions: PathMatcher,    updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,    update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,    snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,    replica_id: ReplicaId,    visible: bool,    disconnected: bool,} #[derive(Clone)]pub struct Snapshot {    id: WorktreeId,    abs_path: SanitizedPath,    root_name: String,    root_char_bag: CharBag,    entries_by_path: SumTree<Entry>,    entries_by_id: SumTree<PathEntry>,    always_included_entries: Vec<Arc<Path>>,    repositories: SumTree<RepositoryEntry>,     /// A number that increases every time the worktree begins scanning    /// a set of paths from the filesystem. This scanning could be caused    /// by some operation performed on the worktree, such as reading or    /// writing a file, or by an event reported by the filesystem.    scan_id: usize,     /// The latest scan id that has completed, and whose preceding scans    /// have all completed. The current `scan_id` could be more than one    /// greater than the `completed_scan_id` if operations are performed    /// on the worktree while it is processing a file-system event.    completed_scan_id: usize,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct RepositoryEntry {    /// The git status entries for this repository.    /// Note that the paths on this repository are relative to the git work directory.    /// If the .git folder is external to Zed, these paths will be relative to that folder,    /// and this data structure might reference files external to this worktree.    ///    /// For example:    ///    ///     my_root_folder/          <-- repository root    ///       .git    ///       my_sub_folder_1/    ///         project_root/        <-- Project root, Zed opened here    ///           changed_file_1     <-- File with changes, in worktree    ///       my_sub_folder_2/    ///         changed_file_2       <-- File with changes, out of worktree    ///           ...    ///    /// With this setup, this field would contain 2 entries, like so:    ///     - my_sub_folder_1/project_root/changed_file_1    ///     - my_sub_folder_2/changed_file_2    pub statuses_by_path: SumTree<StatusEntry>,    pub work_directory_id: ProjectEntryId,    pub work_directory_abs_path: PathBuf,    pub worktree_scan_id: usize,    pub current_branch: Option<Branch>,    pub current_merge_conflicts: TreeSet<RepoPath>,} impl RepositoryEntry {    pub fn relativize_abs_path(&self, abs_path: &Path) -> Option<RepoPath> {        Some(            abs_path                .strip_prefix(&self.work_directory_abs_path)                .ok()?                .into(),        )    }     pub fn directory_contains_abs_path(&self, abs_path: impl AsRef<Path>) -> bool {        abs_path.as_ref().starts_with(&self.work_directory_abs_path)    }     pub fn branch(&self) -> Option<&Branch> {        self.current_branch.as_ref()    }     pub fn work_directory_id(&self) -> ProjectEntryId {        self.work_directory_id    }     pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {        self.statuses_by_path.iter().cloned()    }     pub fn status_len(&self) -> usize {        self.statuses_by_path.summary().item_summary.count    }     pub fn status_summary(&self) -> GitSummary {        self.statuses_by_path.summary().item_summary    }     pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {        self.statuses_by_path            .get(&PathKey(path.0.clone()), &())            .cloned()    }     pub fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {        proto::UpdateRepository {            branch_summary: self.current_branch.as_ref().map(branch_to_proto),            updated_statuses: self                .statuses_by_path                .iter()                .map(|entry| entry.to_proto())                .collect(),            removed_statuses: Default::default(),            current_merge_conflicts: self                .current_merge_conflicts                .iter()                .map(|repo_path| repo_path.to_proto())                .collect(),            project_id,            // This is semantically wrong---we want to move to having separate IDs for repositories.            // But for the moment, RepositoryEntry isn't set up to provide that at this level, so we            // shim it using the work directory's project entry ID. The pair of this + project ID will            // be globally unique.            id: self.work_directory_id().to_proto(),            abs_path: self.work_directory_abs_path.as_path().to_proto(),            entry_ids: vec![self.work_directory_id().to_proto()],            // This is also semantically wrong, and should be replaced once we separate git repo updates            // from worktree scans.            scan_id: self.worktree_scan_id as u64,        }    }     pub fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {        let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();        let mut removed_statuses: Vec<String> = Vec::new();         let mut new_statuses = self.statuses_by_path.iter().peekable();        let mut old_statuses = old.statuses_by_path.iter().peekable();         let mut current_new_entry = new_statuses.next();        let mut current_old_entry = old_statuses.next();        loop {            match (current_new_entry, current_old_entry) {                (Some(new_entry), Some(old_entry)) => {                    match new_entry.repo_path.cmp(&old_entry.repo_path) {                        Ordering::Less => {                            updated_statuses.push(new_entry.to_proto());                            current_new_entry = new_statuses.next();                        }                        Ordering::Equal => {                            if new_entry.status != old_entry.status {                                updated_statuses.push(new_entry.to_proto());                            }                            current_old_entry = old_statuses.next();                            current_new_entry = new_statuses.next();                        }                        Ordering::Greater => {                            removed_statuses.push(old_entry.repo_path.as_ref().to_proto());                            current_old_entry = old_statuses.next();                        }                    }                }                (None, Some(old_entry)) => {                    removed_statuses.push(old_entry.repo_path.as_ref().to_proto());                    current_old_entry = old_statuses.next();                }                (Some(new_entry), None) => {                    updated_statuses.push(new_entry.to_proto());                    current_new_entry = new_statuses.next();                }                (None, None) => break,            }        }         proto::UpdateRepository {            branch_summary: self.current_branch.as_ref().map(branch_to_proto),            updated_statuses,            removed_statuses,            current_merge_conflicts: self                .current_merge_conflicts                .iter()                .map(|path| path.as_ref().to_proto())                .collect(),            project_id,            id: self.work_directory_id.to_proto(),            abs_path: self.work_directory_abs_path.as_path().to_proto(),            entry_ids: vec![self.work_directory_id.to_proto()],            scan_id: self.worktree_scan_id as u64,        }    }} pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {    proto::Branch {        is_head: branch.is_head,        name: branch.name.to_string(),        unix_timestamp: branch            .most_recent_commit            .as_ref()            .map(|commit| commit.commit_timestamp as u64),        upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {            ref_name: upstream.ref_name.to_string(),            tracking: upstream                .tracking                .status()                .map(|upstream| proto::UpstreamTracking {                    ahead: upstream.ahead as u64,                    behind: upstream.behind as u64,                }),        }),        most_recent_commit: branch            .most_recent_commit            .as_ref()            .map(|commit| proto::CommitSummary {                sha: commit.sha.to_string(),                subject: commit.subject.to_string(),                commit_timestamp: commit.commit_timestamp,            }),    }} pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {    git::repository::Branch {        is_head: proto.is_head,        name: proto.name.clone().into(),        upstream: proto            .upstream            .as_ref()            .map(|upstream| git::repository::Upstream {                ref_name: upstream.ref_name.to_string().into(),                tracking: upstream                    .tracking                    .as_ref()                    .map(|tracking| {                        git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {                            ahead: tracking.ahead as u32,                            behind: tracking.behind as u32,                        })                    })                    .unwrap_or(git::repository::UpstreamTracking::Gone),            }),        most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {            git::repository::CommitSummary {                sha: commit.sha.to_string().into(),                subject: commit.subject.to_string().into(),                commit_timestamp: commit.commit_timestamp,                has_parent: true,            }        }),    }} /// This path corresponds to the 'content path' of a repository in relation/// to Zed's project root./// In the majority of the cases, this is the folder that contains the .git folder./// But if a sub-folder of a git repository is opened, this corresponds to the/// project root and the .git folder is located in a parent directory.#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]pub enum WorkDirectory {    InProject {        relative_path: Arc<Path>,    },    AboveProject {        absolute_path: Arc<Path>,        location_in_repo: Arc<Path>,    },} impl WorkDirectory {    #[cfg(test)]    fn in_project(path: &str) -> Self {        let path = Path::new(path);        Self::InProject {            relative_path: path.into(),        }    }     //#[cfg(test)]    //fn canonicalize(&self) -> Self {    //    match self {    //        WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {    //            relative_path: relative_path.clone(),    //        },    //        WorkDirectory::AboveProject {    //            absolute_path,    //            location_in_repo,    //        } => WorkDirectory::AboveProject {    //            absolute_path: absolute_path.canonicalize().unwrap().into(),    //            location_in_repo: location_in_repo.clone(),    //        },    //    }    //}     fn path_key(&self) -> PathKey {        match self {            WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),            WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),        }    }     /// Returns true if the given path is a child of the work directory.    ///    /// Note that the path may not be a member of this repository, if there    /// is a repository in a directory between these two paths    /// external .git folder in a parent folder of the project root.    #[track_caller]    pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {        let path = path.as_ref();        debug_assert!(path.is_relative());        match self {            WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),            WorkDirectory::AboveProject { .. } => true,        }    }     /// relativize returns the given project path relative to the root folder of the    /// repository.    /// If the root of the repository (and its .git folder) are located in a parent folder    /// of the project root folder, then the returned RepoPath is relative to the root    /// of the repository and not a valid path inside the project.    pub fn relativize(&self, path: &Path) -> Result<RepoPath> {        // path is assumed to be relative to worktree root.        debug_assert!(path.is_relative());        match self {            WorkDirectory::InProject { relative_path } => Ok(path                .strip_prefix(relative_path)                .map_err(|_| {                    anyhow!(                        "could not relativize {:?} against {:?}",                        path,                        relative_path                    )                })?                .into()),            WorkDirectory::AboveProject {                location_in_repo, ..            } => {                // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.                if path == Path::new("") {                    Ok(RepoPath(location_in_repo.clone()))                } else {                    Ok(location_in_repo.join(path).into())                }            }        }    }     /// This is the opposite operation to `relativize` above    pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {        match self {            WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),            WorkDirectory::AboveProject {                location_in_repo, ..            } => {                // If we fail to strip the prefix, that means this status entry is                // external to this worktree, and we definitely won't have an entry_id                path.strip_prefix(location_in_repo).ok().map(Into::into)            }        }

Lines 1–500 of 6401.