mod ignore;mod worktree_settings;#[cfg(test)]mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};use anyhow::{Context as _, Result, anyhow};use clock::ReplicaId;use collections::{HashMap, HashSet, VecDeque};use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};use futures::{ FutureExt as _, Stream, StreamExt, channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, task::Poll,};use fuzzy::CharBag;use git::{ COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, repository::RepoPath, status::GitSummary,};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, local_vscode_folder_relative_path};use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch,};use rpc::{ AnyProtoClient, proto::{self, FromProto, ToProto, split_worktree_update},};pub use settings::WorktreeId;use settings::{Settings, SettingsLocation, SettingsStore};use smallvec::{SmallVec, smallvec};use smol::channel::{self, Sender};use std::{ any::Any, borrow::Borrow as _, cmp::Ordering, collections::hash_map, convert::TryFrom, ffi::OsStr, fmt, future::Future, mem::{self}, ops::{Deref, DerefMut}, path::{Component, Path, PathBuf}, pin::Pin, sync::{ Arc, atomic::{AtomicUsize, Ordering::SeqCst}, }, time::{Duration, Instant},};use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};use text::{LineEnding, Rope};use util::{ ResultExt, paths::{PathMatcher, SanitizedPath, home_dir},};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>,} impl fmt::Debug for LoadedBinaryFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LoadedBinaryFile") .field("file", &self.file) .field("content_bytes", &self.content.len()) .finish() }} 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>>, /// 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,} /// 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 {path:?} against {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) } } } pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> { match self { WorkDirectory::InProject { relative_path } => relative_path.join(path).into(), WorkDirectory::AboveProject { location_in_repo, .. } => { if &path.0 == location_in_repo { // Single-file worktree return location_in_repo .file_name() .map(Path::new) .unwrap_or(Path::new("")) .into(); } let mut location_in_repo = &**location_in_repo; let mut parents = PathBuf::new(); loop { if let Ok(segment) = path.strip_prefix(location_in_repo) { return parents.join(segment).into(); } location_in_repo = location_in_repo.parent().unwrap_or(Path::new("")); parents.push(Component::ParentDir); } } } } pub fn display_name(&self) -> String { match self { WorkDirectory::InProject { relative_path } => relative_path.display().to_string(), WorkDirectory::AboveProject { absolute_path, location_in_repo, } => { let num_of_dots = location_in_repo.components().count(); "../".repeat(num_of_dots) + &absolute_path .file_name() .map(|s| s.to_string_lossy()) .unwrap_or_default() + "/" } } }} impl Default for WorkDirectory { fn default() -> Self { Self::InProject { relative_path: Arc::from(Path::new("")), } }} #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]pub struct WorkDirectoryEntry(ProjectEntryId); impl Deref for WorkDirectoryEntry { type Target = ProjectEntryId; fn deref(&self) -> &Self::Target { &self.0 }} impl From<ProjectEntryId> for WorkDirectoryEntry { fn from(value: ProjectEntryId) -> Self { WorkDirectoryEntry(value) }} #[derive(Debug, Clone)]pub struct LocalSnapshot { snapshot: Snapshot, /// All of the gitignore files in the worktree, indexed by their relative path. /// The boolean indicates whether the gitignore needs to be updated. ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>, /// All of the git repositories in the worktree, indexed by the project entry /// id of their parent directory. git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, /// The file handle of the root dir /// (so we can find it after it's been moved) root_file_handle: Option<Arc<dyn fs::FileHandle>>,} struct BackgroundScannerState { snapshot: LocalSnapshot, scanned_dirs: HashSet<ProjectEntryId>, path_prefixes_to_scan: HashSet<Arc<Path>>, paths_to_scan: HashSet<Arc<Path>>, /// The ids of all of the entries that were removed from the snapshot /// as part of the current update. These entry ids may be re-used /// if the same inode is discovered at a new path, or if the given /// path is re-created after being deleted. removed_entries: HashMap<u64, Entry>, changed_paths: Vec<Arc<Path>>, prev_snapshot: Snapshot,} #[derive(Debug, Clone)]struct LocalRepositoryEntry { work_directory_id: ProjectEntryId, work_directory: WorkDirectory, work_directory_abs_path: Arc<Path>, git_dir_scan_id: usize, /// Absolute path to the original .git entry that caused us to create this repository. /// /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere /// (whose path we then store in `repository_dir_abs_path`). dot_git_abs_path: Arc<Path>, /// Absolute path to the "commondir" for this repository. /// /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path, /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory /// from which the submodule/worktree was derived. common_dir_abs_path: Arc<Path>, /// Absolute path to the directory holding the repository's state. /// /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the /// commondir like `/project/.git/modules/foo`. repository_dir_abs_path: Arc<Path>,} impl sum_tree::Item for LocalRepositoryEntry { type Summary = PathSummary<&'static ()>; fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path_key().0, item_summary: &(), } }} impl KeyedItem for LocalRepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { self.work_directory.path_key() }} impl Deref for LocalRepositoryEntry { type Target = WorkDirectory; fn deref(&self) -> &Self::Target { &self.work_directory }} impl Deref for LocalSnapshot { type Target = Snapshot; fn deref(&self) -> &Self::Target { &self.snapshot }} impl DerefMut for LocalSnapshot { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.snapshot }} #[derive(Debug)]enum ScanState { Started, Updated { snapshot: LocalSnapshot, changes: UpdatedEntriesSet, barrier: SmallVec<[barrier::Sender; 1]>, scanning: bool, }, RootUpdated { new_path: Option<SanitizedPath>, },} struct UpdateObservationState { snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>, resume_updates: watch::Sender<()>, _maintain_remote_snapshot: Task<Option<()>>,} #[derive(Clone)]pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet), DeletedEntry(ProjectEntryId),} const EMPTY_PATH: &str = ""; impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncApp, ) -> Result<Entity<Self>> { let abs_path = path.into(); let metadata = fs .metadata(&abs_path) .await .context("failed to stat worktree path")?; let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" ); true });Lines 1–500 of 5568. Load more
mod ignore; mod worktree_settings; #[cfg(test)] mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; use futures::{ FutureExt as _, Stream, StreamExt, channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, task::Poll, }; use fuzzy::CharBag; use git::{ COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, repository::RepoPath, status::GitSummary, }; 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, local_vscode_folder_relative_path}; use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch, }; use rpc::{ AnyProtoClient, proto::{self, FromProto, ToProto, split_worktree_update}, }; pub use settings::WorktreeId; use settings::{Settings, SettingsLocation, SettingsStore}; use smallvec::{SmallVec, smallvec}; use smol::channel::{self, Sender}; use std::{ any::Any, borrow::Borrow as _, cmp::Ordering, collections::hash_map, convert::TryFrom, ffi::OsStr, fmt, future::Future, mem::{self}, ops::{Deref, DerefMut}, path::{Component, Path, PathBuf}, pin::Pin, sync::{ Arc, atomic::{AtomicUsize, Ordering::SeqCst}, }, time::{Duration, Instant}, }; use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ ResultExt, paths::{PathMatcher, SanitizedPath, home_dir}, }; 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>, } impl fmt::Debug for LoadedBinaryFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LoadedBinaryFile") .field("file", &self.file) .field("content_bytes", &self.content.len()) .finish() } } 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>>, /// 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, } /// 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 {path:?} against {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) } } } pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> { match self { WorkDirectory::InProject { relative_path } => relative_path.join(path).into(), WorkDirectory::AboveProject { location_in_repo, .. } => { if &path.0 == location_in_repo { // Single-file worktree return location_in_repo .file_name() .map(Path::new) .unwrap_or(Path::new("")) .into(); } let mut location_in_repo = &**location_in_repo; let mut parents = PathBuf::new(); loop { if let Ok(segment) = path.strip_prefix(location_in_repo) { return parents.join(segment).into(); } location_in_repo = location_in_repo.parent().unwrap_or(Path::new("")); parents.push(Component::ParentDir); } } } } pub fn display_name(&self) -> String { match self { WorkDirectory::InProject { relative_path } => relative_path.display().to_string(), WorkDirectory::AboveProject { absolute_path, location_in_repo, } => { let num_of_dots = location_in_repo.components().count(); "../".repeat(num_of_dots) + &absolute_path .file_name() .map(|s| s.to_string_lossy()) .unwrap_or_default() + "/" } } } } impl Default for WorkDirectory { fn default() -> Self { Self::InProject { relative_path: Arc::from(Path::new("")), } } } #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); impl Deref for WorkDirectoryEntry { type Target = ProjectEntryId; fn deref(&self) -> &Self::Target { &self.0 } } impl From<ProjectEntryId> for WorkDirectoryEntry { fn from(value: ProjectEntryId) -> Self { WorkDirectoryEntry(value) } } #[derive(Debug, Clone)] pub struct LocalSnapshot { snapshot: Snapshot, /// All of the gitignore files in the worktree, indexed by their relative path. /// The boolean indicates whether the gitignore needs to be updated. ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>, /// All of the git repositories in the worktree, indexed by the project entry /// id of their parent directory. git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, /// The file handle of the root dir /// (so we can find it after it's been moved) root_file_handle: Option<Arc<dyn fs::FileHandle>>, } struct BackgroundScannerState { snapshot: LocalSnapshot, scanned_dirs: HashSet<ProjectEntryId>, path_prefixes_to_scan: HashSet<Arc<Path>>, paths_to_scan: HashSet<Arc<Path>>, /// The ids of all of the entries that were removed from the snapshot /// as part of the current update. These entry ids may be re-used /// if the same inode is discovered at a new path, or if the given /// path is re-created after being deleted. removed_entries: HashMap<u64, Entry>, changed_paths: Vec<Arc<Path>>, prev_snapshot: Snapshot, } #[derive(Debug, Clone)] struct LocalRepositoryEntry { work_directory_id: ProjectEntryId, work_directory: WorkDirectory, work_directory_abs_path: Arc<Path>, git_dir_scan_id: usize, /// Absolute path to the original .git entry that caused us to create this repository. /// /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere /// (whose path we then store in `repository_dir_abs_path`). dot_git_abs_path: Arc<Path>, /// Absolute path to the "commondir" for this repository. /// /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path, /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory /// from which the submodule/worktree was derived. common_dir_abs_path: Arc<Path>, /// Absolute path to the directory holding the repository's state. /// /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the /// commondir like `/project/.git/modules/foo`. repository_dir_abs_path: Arc<Path>, } impl sum_tree::Item for LocalRepositoryEntry { type Summary = PathSummary<&'static ()>; fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path_key().0, item_summary: &(), } } } impl KeyedItem for LocalRepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { self.work_directory.path_key() } } impl Deref for LocalRepositoryEntry { type Target = WorkDirectory; fn deref(&self) -> &Self::Target { &self.work_directory } } impl Deref for LocalSnapshot { type Target = Snapshot; fn deref(&self) -> &Self::Target { &self.snapshot } } impl DerefMut for LocalSnapshot { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.snapshot } } #[derive(Debug)] enum ScanState { Started, Updated { snapshot: LocalSnapshot, changes: UpdatedEntriesSet, barrier: SmallVec<[barrier::Sender; 1]>, scanning: bool, }, RootUpdated { new_path: Option<SanitizedPath>, }, } struct UpdateObservationState { snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>, resume_updates: watch::Sender<()>, _maintain_remote_snapshot: Task<Option<()>>, } #[derive(Clone)] pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet), DeletedEntry(ProjectEntryId), } const EMPTY_PATH: &str = ""; impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncApp, ) -> Result<Entity<Self>> { let abs_path = path.into(); let metadata = fs .metadata(&abs_path) .await .context("failed to stat worktree path")?; let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" ); true });