@@ -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<String>;
fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>>;
+
+ fn file_status(&self, path: &RepoPath) -> Option<GitStatus>;
}
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,
-}
-#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
-pub struct RepoPath(PathBuf);
+ fn file_status(&self, path: &RepoPath) -> Option<GitStatus> {
+ let status = self.status_file(path).log_err()?;
-impl From<&Path> for RepoPath {
- fn from(value: &Path) -> Self {
- RepoPath(value.to_path_buf())
- }
-}
-
-impl From<PathBuf> for RepoPath {
- fn from(value: PathBuf) -> Self {
- RepoPath(value)
- }
-}
-
-impl Default for RepoPath {
- fn default() -> Self {
- RepoPath(PathBuf::new())
- }
-}
-
-impl AsRef<Path> 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<TreeMap<RepoPath, GitStatus>>{
+ fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>> {
+ todo!()
+ }
+
+ fn file_status(&self, _: &RepoPath) -> Option<GitStatus> {
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<Status> 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<PathBuf> for RepoPath {
+ fn from(value: PathBuf) -> Self {
+ RepoPath::new(value)
+ }
+}
+
+impl Default for RepoPath {
+ fn default() -> Self {
+ RepoPath(PathBuf::new())
+ }
+}
+
+impl AsRef<Path> 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
+ }
+}
@@ -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<Arc<str>>,
- // pub(crate) statuses: TreeMap<RepoPath, GitStatus>
+ pub(crate) statuses: TreeMap<RepoPath, GitStatus>,
}
impl RepositoryEntry {
@@ -169,7 +172,6 @@ impl AsRef<Path> 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<Mutex<dyn GitRepository>>,
/// 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 _;
@@ -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<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Debug + Default + Ord,
V: Clone + Debug;
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MapEntry<K, V> {
key: K,
value: V,