From 67491632cbce955038e907360c35027e094e4028 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 10:02:58 -0700 Subject: [PATCH] WIP: Track live entry status in repository co-authored-by: petros --- crates/fs/src/repository.rs | 152 +++++++++++++++++++------------- crates/project/src/worktree.rs | 95 +++++++++++++++----- crates/sum_tree/src/tree_map.rs | 4 +- 3 files changed, 165 insertions(+), 86 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 14e7e75a3d..626fbf9e12 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,11 +1,14 @@ use anyhow::Result; use collections::HashMap; +use git2::Status; use parking_lot::Mutex; -use sum_tree::TreeMap; use std::{ + ffi::OsStr, + os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, - sync::Arc, ffi::OsStr, os::unix::prelude::OsStrExt, + sync::Arc, }; +use sum_tree::TreeMap; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -19,6 +22,8 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; fn statuses(&self) -> Option>; + + fn file_status(&self, path: &RepoPath) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -70,72 +75,22 @@ impl GitRepository for LibGitRepository { let mut map = TreeMap::default(); - for status in statuses.iter() { + for status in statuses + .iter() + .filter(|status| !status.status().contains(git2::Status::IGNORED)) + { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - let status_data = status.status(); - - let status = if status_data.contains(git2::Status::CONFLICTED) { - GitStatus::Conflict - } else if status_data.intersects(git2::Status::INDEX_MODIFIED - | git2::Status::WT_MODIFIED - | git2::Status::INDEX_RENAMED - | git2::Status::WT_RENAMED) { - GitStatus::Modified - } else if status_data.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { - GitStatus::Added - } else { - GitStatus::Untracked - }; - - map.insert(path, status) + map.insert(path, status.status().into()) } Some(map) } -} -#[derive(Debug, Clone, Default)] -pub enum GitStatus { - Added, - Modified, - Conflict, - #[default] - Untracked, -} + fn file_status(&self, path: &RepoPath) -> Option { + let status = self.status_file(path).log_err()?; -#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(PathBuf); - -impl From<&Path> for RepoPath { - fn from(value: &Path) -> Self { - RepoPath(value.to_path_buf()) - } -} - -impl From for RepoPath { - fn from(value: PathBuf) -> Self { - RepoPath(value) - } -} - -impl Default for RepoPath { - fn default() -> Self { - RepoPath(PathBuf::new()) - } -} - -impl AsRef for RepoPath { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl std::ops::Deref for RepoPath { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 + Some(status.into()) } } @@ -170,7 +125,11 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option>{ + fn statuses(&self) -> Option> { + todo!() + } + + fn file_status(&self, _: &RepoPath) -> Option { todo!() } } @@ -203,3 +162,74 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { _ => Ok(()), } } + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum GitStatus { + Added, + Modified, + Conflict, + #[default] + Untracked, +} + +impl From for GitStatus { + fn from(value: Status) -> Self { + if value.contains(git2::Status::CONFLICTED) { + GitStatus::Conflict + } else if value.intersects( + git2::Status::INDEX_MODIFIED + | git2::Status::WT_MODIFIED + | git2::Status::INDEX_RENAMED + | git2::Status::WT_RENAMED, + ) { + GitStatus::Modified + } else if value.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { + GitStatus::Added + } else { + GitStatus::Untracked + } + } +} + +#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] +pub struct RepoPath(PathBuf); + +impl RepoPath { + fn new(path: PathBuf) -> Self { + debug_assert!(path.is_relative(), "Repo paths must be relative"); + + RepoPath(path) + } +} + +impl From<&Path> for RepoPath { + fn from(value: &Path) -> Self { + RepoPath::new(value.to_path_buf()) + } +} + +impl From for RepoPath { + fn from(value: PathBuf) -> Self { + RepoPath::new(value) + } +} + +impl Default for RepoPath { + fn default() -> Self { + RepoPath(PathBuf::new()) + } +} + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl std::ops::Deref for RepoPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e236d18efd..e43ab9257b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; -use fs::{repository::{GitRepository, RepoPath, GitStatus}, Fs, LineEnding}; +use fs::{ + repository::{GitRepository, GitStatus, RepoPath}, + Fs, LineEnding, +}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -121,7 +124,7 @@ pub struct Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - // pub(crate) statuses: TreeMap + pub(crate) statuses: TreeMap, } impl RepositoryEntry { @@ -169,7 +172,6 @@ impl AsRef for RepositoryWorkDirectory { } } - #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); @@ -219,6 +221,7 @@ pub struct LocalSnapshot { #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { pub(crate) scan_id: usize, + pub(crate) full_scan_id: usize, pub(crate) repo_ptr: Arc>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file @@ -1412,6 +1415,8 @@ impl Snapshot { let repository = RepositoryEntry { work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), branch: repository.branch.map(Into::into), + // TODO: status + statuses: Default::default(), }; if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { self.repository_entries @@ -1572,6 +1577,10 @@ impl LocalSnapshot { current_candidate.map(|entry| entry.to_owned()) } + pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { + self.git_repositories.get(&repo.work_directory.0) + } + pub(crate) fn repo_for_metadata( &self, path: &Path, @@ -1842,6 +1851,7 @@ impl LocalSnapshot { RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), + statuses: repo_lock.statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -1850,6 +1860,7 @@ impl LocalSnapshot { work_dir_id, LocalRepositoryEntry { scan_id, + full_scan_id: scan_id, repo_ptr: repo, git_dir_path: parent_path.clone(), }, @@ -2825,26 +2836,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - let scan_id = snapshot.scan_id; - - let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); - if let Some((entry_id, repo)) = repo_with_path_in_dotgit { - let work_dir = snapshot - .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; - - let repo = repo.lock(); - repo.reload_index(); - let branch = repo.branch_name(); - - snapshot.git_repositories.update(&entry_id, |entry| { - entry.scan_id = scan_id; - }); - - snapshot - .repository_entries - .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - } + self.reload_repo_for_path(&path, &mut snapshot); if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -2872,6 +2864,63 @@ impl BackgroundScanner { Some(event_paths) } + fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + let scan_id = snapshot.scan_id; + + if path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + + let work_dir = snapshot + .entry_for_id(entry_id) + .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; + + let repo = repo.lock(); + repo.reload_index(); + let branch = repo.branch_name(); + let statuses = repo.statuses().unwrap_or_default(); + + snapshot.git_repositories.update(&entry_id, |entry| { + entry.scan_id = scan_id; + entry.full_scan_id = scan_id; + }); + + snapshot.repository_entries.update(&work_dir, |entry| { + entry.branch = branch.map(Into::into); + entry.statuses = statuses; + }); + } else if let Some(repo) = snapshot.repo_for(&path) { + let status = { + let local_repo = snapshot.get_local_repo(&repo)?; + // Short circuit if we've already scanned everything + if local_repo.full_scan_id == scan_id { + return None; + } + + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + let git_ptr = local_repo.repo_ptr.lock(); + git_ptr.file_status(&repo_path)? + }; + + if status != GitStatus::Untracked { + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory; + + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); + + snapshot + .repository_entries + .update(&work_dir, |entry| entry.statuses.insert(repo_path, status)); + } + } + + Some(()) + } + async fn update_ignore_statuses(&self) { use futures::FutureExt as _; diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 1b97cbec9f..ab37d2577a 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,13 +2,13 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, V: Clone + Debug; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct MapEntry { key: K, value: V,