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::{GitRepository, RepoPath}, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, }, GitHostingProviderRegistry, COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK,};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}, 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::{Path, PathBuf}, pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, Instant},};use sum_tree::{ Bias, Cursor, 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; #[cfg(feature = "test-support")]pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);#[cfg(not(feature = "test-support"))]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(crate) statuses_by_path: SumTree<StatusEntry>, work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, pub(crate) branch: Option<Arc<str>>, pub current_merge_conflicts: TreeSet<RepoPath>,} impl Deref for RepositoryEntry { type Target = WorkDirectory; fn deref(&self) -> &Self::Target { &self.work_directory }} impl AsRef<Path> for RepositoryEntry { fn as_ref(&self) -> &Path { &self.path }} impl RepositoryEntry { pub fn branch(&self) -> Option<Arc<str>> { self.branch.clone() } 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) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), branch: self.branch.as_ref().map(|branch| branch.to_string()), 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(), } } pub fn build_update(&self, old: &Self) -> proto::RepositoryEntry { 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.to_proto()); current_old_entry = old_statuses.next(); } } } (None, Some(old_entry)) => { removed_statuses.push(old_entry.repo_path.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::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), branch: self.branch.as_ref().map(|branch| branch.to_string()), updated_statuses, removed_statuses, current_merge_conflicts: self .current_merge_conflicts .iter() .map(RepoPath::to_proto) .collect(), } }} /// 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 struct WorkDirectory { path: Arc<Path>, /// If location_in_repo is set, it means the .git folder is external /// and in a parent folder of the project root. /// In that case, the work_directory field will point to the /// project-root and location_in_repo contains the location of the /// project-root in the repository. /// /// Example: /// /// my_root_folder/ <-- repository root /// .git /// my_sub_folder_1/ /// project_root/ <-- Project root, Zed opened here /// ... /// /// For this setup, the attributes will have the following values: /// /// work_directory: pointing to "" entry /// location_in_repo: Some("my_sub_folder_1/project_root") pub(crate) location_in_repo: Option<Arc<Path>>,} impl WorkDirectory { pub fn path_key(&self) -> PathKey { PathKey(self.path.clone()) } /// 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. pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool { let path = path.as_ref(); path.starts_with(&self.path) } /// 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> { let repo_path = if let Some(location_in_repo) = &self.location_in_repo { // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. if path == Path::new("") { RepoPath(location_in_repo.clone()) } else { location_in_repo.join(path).into() } } else { path.strip_prefix(&self.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))? .into() }; Ok(repo_path) } /// This is the opposite operation to `relativize` above pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> { if let Some(location) = &self.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).ok().map(Into::into) } else { Some(self.path.join(path).into()) } }} impl Default for WorkDirectory { fn default() -> Self { Self { path: Arc::from(Path::new("")), location_in_repo: None, } }} impl Deref for WorkDirectory { type Target = Path; fn deref(&self) -> &Self::Target { self.as_ref() }} impl AsRef<Path> for WorkDirectory { fn as_ref(&self) -> &Path { self.path.as_ref() }} #[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, git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>, repository_scans: HashMap<Arc<Path>, Task<()>>,} #[derive(Debug, Clone)]pub struct LocalRepositoryEntry { pub(crate) work_directory_id: ProjectEntryId, pub(crate) work_directory: WorkDirectory, pub(crate) git_dir_scan_id: usize, pub(crate) status_scan_id: usize, pub(crate) repo_ptr: Arc<dyn GitRepository>, /// Absolute path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file pub(crate) dot_git_dir_abs_path: Arc<Path>, /// Absolute path to the .git file, if we're in a git worktree. pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>, pub current_merge_head_shas: Vec<String>,} impl sum_tree::Item for LocalRepositoryEntry { type Summary = PathSummary<Unit>; fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path.clone(), item_summary: Unit, } }} impl KeyedItem for LocalRepositoryEntry { type Key = PathKey; Lines 1–500 of 6371. Load more
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::{GitRepository, RepoPath}, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, }, GitHostingProviderRegistry, COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, }; 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}, 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::{Path, PathBuf}, pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, Instant}, }; use sum_tree::{ Bias, Cursor, 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; #[cfg(feature = "test-support")] pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); #[cfg(not(feature = "test-support"))] 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(crate) statuses_by_path: SumTree<StatusEntry>, work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, pub(crate) branch: Option<Arc<str>>, pub current_merge_conflicts: TreeSet<RepoPath>, } impl Deref for RepositoryEntry { type Target = WorkDirectory; fn deref(&self) -> &Self::Target { &self.work_directory } } impl AsRef<Path> for RepositoryEntry { fn as_ref(&self) -> &Path { &self.path } } impl RepositoryEntry { pub fn branch(&self) -> Option<Arc<str>> { self.branch.clone() } 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) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), branch: self.branch.as_ref().map(|branch| branch.to_string()), 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(), } } pub fn build_update(&self, old: &Self) -> proto::RepositoryEntry { 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.to_proto()); current_old_entry = old_statuses.next(); } } } (None, Some(old_entry)) => { removed_statuses.push(old_entry.repo_path.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::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), branch: self.branch.as_ref().map(|branch| branch.to_string()), updated_statuses, removed_statuses, current_merge_conflicts: self .current_merge_conflicts .iter() .map(RepoPath::to_proto) .collect(), } } } /// 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 struct WorkDirectory { path: Arc<Path>, /// If location_in_repo is set, it means the .git folder is external /// and in a parent folder of the project root. /// In that case, the work_directory field will point to the /// project-root and location_in_repo contains the location of the /// project-root in the repository. /// /// Example: /// /// my_root_folder/ <-- repository root /// .git /// my_sub_folder_1/ /// project_root/ <-- Project root, Zed opened here /// ... /// /// For this setup, the attributes will have the following values: /// /// work_directory: pointing to "" entry /// location_in_repo: Some("my_sub_folder_1/project_root") pub(crate) location_in_repo: Option<Arc<Path>>, } impl WorkDirectory { pub fn path_key(&self) -> PathKey { PathKey(self.path.clone()) } /// 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. pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool { let path = path.as_ref(); path.starts_with(&self.path) } /// 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> { let repo_path = if let Some(location_in_repo) = &self.location_in_repo { // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. if path == Path::new("") { RepoPath(location_in_repo.clone()) } else { location_in_repo.join(path).into() } } else { path.strip_prefix(&self.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))? .into() }; Ok(repo_path) } /// This is the opposite operation to `relativize` above pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> { if let Some(location) = &self.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).ok().map(Into::into) } else { Some(self.path.join(path).into()) } } } impl Default for WorkDirectory { fn default() -> Self { Self { path: Arc::from(Path::new("")), location_in_repo: None, } } } impl Deref for WorkDirectory { type Target = Path; fn deref(&self) -> &Self::Target { self.as_ref() } } impl AsRef<Path> for WorkDirectory { fn as_ref(&self) -> &Path { self.path.as_ref() } } #[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, git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>, repository_scans: HashMap<Arc<Path>, Task<()>>, } #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { pub(crate) work_directory_id: ProjectEntryId, pub(crate) work_directory: WorkDirectory, pub(crate) git_dir_scan_id: usize, pub(crate) status_scan_id: usize, pub(crate) repo_ptr: Arc<dyn GitRepository>, /// Absolute path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file pub(crate) dot_git_dir_abs_path: Arc<Path>, /// Absolute path to the .git file, if we're in a git worktree. pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>, pub current_merge_head_shas: Vec<String>, } impl sum_tree::Item for LocalRepositoryEntry { type Summary = PathSummary<Unit>; fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path.clone(), item_summary: Unit, } } } impl KeyedItem for LocalRepositoryEntry { type Key = PathKey;