diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index eb16446daf5170fddb2d7f47a79f0a64033d3226..d131b6dd41538d698171c96e9b7e0d58bfd7fe33 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -14,9 +14,10 @@ use collections::HashMap; pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate}; use fs::Fs; use futures::{ - FutureExt, StreamExt as _, + FutureExt, StreamExt, channel::{mpsc, oneshot}, future::{self, Shared}, + stream::FuturesOrdered, }; use git::{ BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH, @@ -63,8 +64,8 @@ use sum_tree::{Edit, SumTree, TreeSet}; use text::{Bias, BufferId}; use util::{ResultExt, debug_panic, post_inc}; use worktree::{ - File, PathKey, PathProgress, PathSummary, PathTarget, UpdatedGitRepositoriesSet, - UpdatedGitRepository, Worktree, + File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId, + UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree, }; pub struct GitStore { @@ -1083,27 +1084,26 @@ impl GitStore { match event { WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => { - let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); - for (relative_path, _, _) in updated_entries.iter() { - let Some((repo, repo_path)) = self.repository_and_path_for_project_path( - &(*worktree_id, relative_path.clone()).into(), - cx, - ) else { - continue; - }; - paths_by_git_repo.entry(repo).or_default().push(repo_path) - } - - for (repo, paths) in paths_by_git_repo { - repo.update(cx, |repo, cx| { - repo.paths_changed( - paths, - downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - cx, - ); - }); + if let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(*worktree_id, cx) + { + let paths_by_git_repo = + self.process_updated_entries(&worktree, updated_entries, cx); + let downstream = downstream + .as_ref() + .map(|downstream| downstream.updates_tx.clone()); + cx.spawn(async move |_, cx| { + let paths_by_git_repo = paths_by_git_repo.await; + for (repo, paths) in paths_by_git_repo { + repo.update(cx, |repo, cx| { + repo.paths_changed(paths, downstream.clone(), cx); + }) + .ok(); + } + }) + .detach(); } } WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => { @@ -2191,6 +2191,80 @@ impl GitStore { .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone())) .collect() } + + fn process_updated_entries( + &self, + worktree: &Entity, + updated_entries: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut App, + ) -> Task, Vec>> { + let mut repo_paths = self + .repositories + .values() + .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone())) + .collect::>(); + let mut entries: Vec<_> = updated_entries + .iter() + .map(|(path, _, _)| path.clone()) + .collect(); + entries.sort(); + let worktree = worktree.read(cx); + + let entries = entries + .into_iter() + .filter_map(|path| worktree.absolutize(&path).ok()) + .collect::>(); + + let executor = cx.background_executor().clone(); + cx.background_executor().spawn(async move { + repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); + let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); + let mut tasks = FuturesOrdered::new(); + for (repo_path, repo) in repo_paths.into_iter().rev() { + let entries = entries.clone(); + let task = executor.spawn(async move { + // Find all repository paths that belong to this repo + let mut ix = entries.partition_point(|path| path < &*repo_path); + if ix == entries.len() { + return None; + }; + + let mut paths = vec![]; + // All paths prefixed by a given repo will constitute a continuous range. + while let Some(path) = entries.get(ix) + && let Some(repo_path) = + RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, &path) + { + paths.push((repo_path, ix)); + ix += 1; + } + Some((repo, paths)) + }); + tasks.push_back(task); + } + + // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos. + let mut path_was_used = vec![false; entries.len()]; + let tasks = tasks.collect::>().await; + // Process tasks from the back: iterating backwards allows us to see more-specific paths first. + // We always want to assign a path to it's innermost repository. + for t in tasks { + let Some((repo, paths)) = t else { + continue; + }; + let entry = paths_by_git_repo.entry(repo).or_default(); + for (repo_path, ix) in paths { + if path_was_used[ix] { + continue; + } + path_was_used[ix] = true; + entry.push(repo_path); + } + } + + paths_by_git_repo + }) + } } impl BufferGitState { @@ -2660,8 +2734,16 @@ impl RepositorySnapshot { } pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option { + Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path) + } + + #[inline] + fn abs_path_to_repo_path_inner( + work_directory_abs_path: &Path, + abs_path: &Path, + ) -> Option { abs_path - .strip_prefix(&self.work_directory_abs_path) + .strip_prefix(&work_directory_abs_path) .map(RepoPath::from) .ok() } diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index cd173d5714863f1ed845dd9e7116dc73d214f710..777042cb02cf87c127f050a88d8504dcb181678c 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -1,6 +1,6 @@ use collections::HashMap; -use git::status::GitSummary; -use std::{ops::Deref, path::Path}; +use git::{repository::RepoPath, status::GitSummary}; +use std::{collections::BTreeMap, ops::Deref, path::Path}; use sum_tree::Cursor; use text::Bias; use worktree::{Entry, PathProgress, PathTarget, Traversal}; @@ -11,7 +11,7 @@ use super::{RepositoryId, RepositorySnapshot, StatusEntry}; pub struct GitTraversal<'a> { traversal: Traversal<'a>, current_entry_summary: Option, - repo_snapshots: &'a HashMap, + repo_root_to_snapshot: BTreeMap<&'a Path, &'a RepositorySnapshot>, repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>, } @@ -20,16 +20,46 @@ impl<'a> GitTraversal<'a> { repo_snapshots: &'a HashMap, traversal: Traversal<'a>, ) -> GitTraversal<'a> { + let repo_root_to_snapshot = repo_snapshots + .values() + .map(|snapshot| (&*snapshot.work_directory_abs_path, snapshot)) + .collect(); let mut this = GitTraversal { traversal, - repo_snapshots, current_entry_summary: None, repo_location: None, + repo_root_to_snapshot, }; this.synchronize_statuses(true); this } + fn repo_root_for_path(&self, path: &Path) -> Option<(&'a RepositorySnapshot, RepoPath)> { + // We might need to perform a range search multiple times, as there may be a nested repository inbetween + // the target and our path. E.g: + // /our_root_repo/ + // .git/ + // other_repo/ + // .git/ + // our_query.txt + let mut query = path.ancestors(); + while let Some(query) = query.next() { + let (_, snapshot) = self + .repo_root_to_snapshot + .range(Path::new("")..=query) + .last()?; + + let stripped = snapshot + .abs_path_to_repo_path(path) + .map(|repo_path| (*snapshot, repo_path)); + if stripped.is_some() { + return stripped; + } + } + + None + } + fn synchronize_statuses(&mut self, reset: bool) { self.current_entry_summary = None; @@ -42,15 +72,7 @@ impl<'a> GitTraversal<'a> { return; }; - let Some((repo, repo_path)) = self - .repo_snapshots - .values() - .filter_map(|repo_snapshot| { - let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?; - Some((repo_snapshot, repo_path)) - }) - .max_by_key(|(repo, _)| repo.work_directory_abs_path.clone()) - else { + let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else { self.repo_location = None; return; };