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_related_message, FromProto, ToProto, WorktreeRelatedMessage}, 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<WorktreeRelatedMessage>)>>, project_id: u64, client: AnyProtoClient, file_scan_inclusions: PathMatcher, updates_tx: Option<UnboundedSender<WorktreeRelatedMessage>>, update_observer: Option<mpsc::UnboundedSender<WorktreeRelatedMessage>>, 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>, work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, work_directory_abs_path: PathBuf, pub(crate) current_branch: Option<Branch>, pub current_merge_conflicts: TreeSet<RepoPath>,} impl RepositoryEntry { pub fn relativize(&self, path: &Path) -> Result<RepoPath> { self.work_directory.relativize(path) } pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> { self.work_directory.try_unrelativize(path) } pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> { self.work_directory.unrelativize(path) } pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool { self.work_directory.directory_contains(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, worktree_scan_id: usize, ) -> 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: worktree_scan_id as u64, } } pub fn build_update( &self, old: &Self, project_id: u64, scan_id: usize, ) -> 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: 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(), }, } } pub fn is_above_project(&self) -> bool { match self { WorkDirectory::InProject { .. } => false, WorkDirectory::AboveProject { .. } => true, } } 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()))Lines 1–500 of 6573. 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::{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_related_message, FromProto, ToProto, WorktreeRelatedMessage}, 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<WorktreeRelatedMessage>)>>, project_id: u64, client: AnyProtoClient, file_scan_inclusions: PathMatcher, updates_tx: Option<UnboundedSender<WorktreeRelatedMessage>>, update_observer: Option<mpsc::UnboundedSender<WorktreeRelatedMessage>>, 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>, work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, work_directory_abs_path: PathBuf, pub(crate) current_branch: Option<Branch>, pub current_merge_conflicts: TreeSet<RepoPath>, } impl RepositoryEntry { pub fn relativize(&self, path: &Path) -> Result<RepoPath> { self.work_directory.relativize(path) } pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> { self.work_directory.try_unrelativize(path) } pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> { self.work_directory.unrelativize(path) } pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool { self.work_directory.directory_contains(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, worktree_scan_id: usize, ) -> 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: worktree_scan_id as u64, } } pub fn build_update( &self, old: &Self, project_id: u64, scan_id: usize, ) -> 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: 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(), }, } } pub fn is_above_project(&self) -> bool { match self { WorkDirectory::InProject { .. } => false, WorkDirectory::AboveProject { .. } => true, } } 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()))