WIP: Refactor existing git code to use new representation.

Mikayla Maki and petros created

co-authored-by: petros <petros@zed.dev>

Change summary

crates/project/src/project.rs  |  23 +++--
crates/project/src/worktree.rs | 145 +++++++++++++++++++++++++----------
2 files changed, 117 insertions(+), 51 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -64,7 +64,7 @@ use std::{
     },
     time::{Duration, Instant, SystemTime},
 };
-use sum_tree::TreeMap;
+
 use terminals::Terminals;
 
 use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
@@ -4697,7 +4697,7 @@ impl Project {
     fn update_local_worktree_buffers_git_repos(
         &mut self,
         worktree: ModelHandle<Worktree>,
-        repos: &TreeMap<RepositoryWorkDirectory, RepositoryEntry>,
+        repos: &Vec<RepositoryEntry>,
         cx: &mut ModelContext<Self>,
     ) {
         for (_, buffer) in &self.opened_buffers {
@@ -4712,27 +4712,30 @@ impl Project {
 
                 let path = file.path().clone();
 
-                let (work_directory, repo) = match repos
+                let repo = match repos
                     .iter()
-                    .find(|(work_directory, _)| work_directory.contains(&path))
+                    .find(|entry| entry.work_directory.contains(&path))
                 {
-                    Some((work_directory, repo)) => (work_directory, repo.clone()),
+                    Some(repo) => repo.clone(),
                     None => return,
                 };
 
-                let relative_repo = match work_directory.relativize(&path) {
+                let relative_repo = match repo.work_directory.relativize(&path) {
                     Some(relative_repo) => relative_repo.to_owned(),
                     None => return,
                 };
 
                 let remote_id = self.remote_id();
                 let client = self.client.clone();
+                let diff_base_task = worktree.update(cx, move |worktree, cx| {
+                    worktree
+                        .as_local()
+                        .unwrap()
+                        .load_index_text(repo, relative_repo, cx)
+                });
 
                 cx.spawn(|_, mut cx| async move {
-                    let diff_base = cx
-                        .background()
-                        .spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
-                        .await;
+                    let diff_base = diff_base_task.await;
 
                     let buffer_id = buffer.update(&mut cx, |buffer, cx| {
                         buffer.set_diff_base(diff_base.clone(), cx);

crates/project/src/worktree.rs 🔗

@@ -123,6 +123,7 @@ pub struct RepositoryEntry {
     // Note: if .git is a file, this points to the folder indicated by the .git file
     pub(crate) git_dir_path: Arc<Path>,
     pub(crate) git_dir_entry_id: ProjectEntryId,
+    pub(crate) work_directory: RepositoryWorkDirectory,
     pub(crate) scan_id: usize,
     // TODO: pub(crate) head_ref: Arc<str>,
 }
@@ -144,8 +145,41 @@ impl RepositoryWorkDirectory {
         path.starts_with(self.0.as_ref())
     }
 
-    pub(crate) fn relativize(&self, path: &Path) -> Option<&Path> {
-        path.strip_prefix(self.0.as_ref()).ok()
+    pub(crate) fn relativize(&self, path: &Path) -> Option<RepoPath> {
+        path.strip_prefix(self.0.as_ref())
+            .ok()
+            .map(move |path| RepoPath(path.to_owned()))
+    }
+}
+
+impl Deref for RepositoryWorkDirectory {
+    type Target = Path;
+
+    fn deref(&self) -> &Self::Target {
+        self.0.as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
+pub struct RepoPath(PathBuf);
+
+impl AsRef<Path> for RepoPath {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
+    }
+}
+
+impl Deref for RepoPath {
+    type Target = PathBuf;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl AsRef<Path> for RepositoryWorkDirectory {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
     }
 }
 
@@ -628,7 +662,7 @@ impl LocalWorktree {
     ) -> Vec<RepositoryEntry> {
         fn diff<'a>(
             a: impl Iterator<Item = &'a RepositoryEntry>,
-            b: impl Iterator<Item = &'a RepositoryEntry>,
+            mut b: impl Iterator<Item = &'a RepositoryEntry>,
             updated: &mut HashMap<&'a Path, RepositoryEntry>,
         ) {
             for a_repo in a {
@@ -691,11 +725,12 @@ impl LocalWorktree {
         cx.spawn(|this, mut cx| async move {
             let text = fs.load(&abs_path).await?;
 
-            let diff_base = if let Some(repo) = snapshot.repo_for(&path) {
-                if let Ok(repo_relative) = path.strip_prefix(repo.content_path) {
+            let diff_base = if let Some((work_directory, repo)) = snapshot.repo_for_metadata(&path)
+            {
+                if let Ok(repo_relative) = path.strip_prefix(&work_directory) {
                     let repo_relative = repo_relative.to_owned();
                     cx.background()
-                        .spawn(async move { repo.repo.lock().load_index_text(&repo_relative) })
+                        .spawn(async move { repo.lock().load_index_text(&repo_relative) })
                         .await
                 } else {
                     None
@@ -1076,6 +1111,19 @@ impl LocalWorktree {
     pub fn is_shared(&self) -> bool {
         self.share.is_some()
     }
+
+    pub fn load_index_text(
+        &self,
+        repo: RepositoryEntry,
+        repo_path: RepoPath,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Option<String>> {
+        let Some(git_ptr) = self.git_repositories.get(&repo.git_dir_entry_id).map(|git_ptr| git_ptr.to_owned()) else {
+            return Task::Ready(Some(None))
+        };
+        cx.background()
+            .spawn(async move { git_ptr.lock().load_index_text(&repo_path) })
+    }
 }
 
 impl RemoteWorktree {
@@ -1418,21 +1466,22 @@ impl Snapshot {
 }
 
 impl LocalSnapshot {
-    // Gives the most specific git repository for a given path
-    pub(crate) fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
-        self.repository_entries
-            .closest(&RepositoryWorkDirectory(path.into()))
-            .map(|(_, entry)| entry.to_owned())
-    }
-
-    pub(crate) fn repo_with_dot_git_containing(
-        &mut self,
+    pub(crate) fn repo_for_metadata(
+        &self,
         path: &Path,
-    ) -> Option<&mut RepositoryEntry> {
-        // Git repositories cannot be nested, so we don't need to reverse the order
-        self.git_repositories_old
-            .iter_mut()
-            .find(|repo| repo.in_dot_git(path))
+    ) -> Option<(RepositoryWorkDirectory, Arc<Mutex<dyn GitRepository>>)> {
+        self.repository_entries
+            .iter()
+            .find(|(_, repo)| repo.in_dot_git(path))
+            .map(|(work_directory, entry)| {
+                (
+                    work_directory.to_owned(),
+                    self.git_repositories
+                        .get(&entry.git_dir_entry_id)
+                        .expect("These two data structures should be in sync")
+                        .to_owned(),
+                )
+            })
     }
 
     #[cfg(test)]
@@ -1610,10 +1659,11 @@ impl LocalSnapshot {
             if self.repository_entries.get(&key).is_none() {
                 if let Some(repo) = fs.open_repo(abs_path.as_path()) {
                     self.repository_entries.insert(
-                        key,
+                        key.clone(),
                         RepositoryEntry {
                             git_dir_path: parent_path.clone(),
                             git_dir_entry_id: parent_entry.id,
+                            work_directory: key,
                             scan_id: 0,
                         },
                     );
@@ -2599,16 +2649,10 @@ impl BackgroundScanner {
 
                     let scan_id = snapshot.scan_id;
 
-                    if let Some(repo) = snapshot.repo_with_dot_git_containing(&path) {
-                        repo.repo.lock().reload_index();
-                        repo.scan_id = scan_id;
-                    }
+                    let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path);
+                    if let Some((key, repo)) = repo_with_path_in_dotgit {
+                        repo.lock().reload_index();
 
-                    let repo_with_path_in_dotgit = snapshot
-                        .repository_entries
-                        .iter()
-                        .find_map(|(key, repo)| repo.in_dot_git(&path).then(|| key.clone()));
-                    if let Some(key) = repo_with_path_in_dotgit {
                         snapshot
                             .repository_entries
                             .update(&key, |entry| entry.scan_id = scan_id);
@@ -3123,7 +3167,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use fs::repository::FakeGitRepository;
     use fs::{FakeFs, RealFs};
     use gpui::{executor::Deterministic, TestAppContext};
     use pretty_assertions::assert_eq;
@@ -3386,23 +3429,37 @@ mod tests {
             .await;
         tree.flush_fs_events(cx).await;
 
+        fn entry(key: &RepositoryWorkDirectory, tree: &LocalWorktree) -> RepositoryEntry {
+            tree.repository_entries.get(key).unwrap().to_owned()
+        }
+
         tree.read_with(cx, |tree, _cx| {
             let tree = tree.as_local().unwrap();
 
-            assert!(tree.repo_for("c.txt".as_ref()).is_none());
+            assert!(tree.repo_for_metadata("c.txt".as_ref()).is_none());
 
-            let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
-            assert_eq!(repo.content_path.as_ref(), Path::new("dir1"));
-            assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git"));
+            let (work_directory, _repo_entry) =
+                tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap();
+            assert_eq!(work_directory.0.as_ref(), Path::new("dir1"));
+            assert_eq!(
+                entry(&work_directory, &tree).git_dir_path.as_ref(),
+                Path::new("dir1/.git")
+            );
 
-            let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
-            assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1"));
-            assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),);
+            let _repo = tree
+                .repo_for_metadata("dir1/deps/dep1/src/a.txt".as_ref())
+                .unwrap();
+            assert_eq!(work_directory.deref(), Path::new("dir1/deps/dep1"));
+            assert_eq!(
+                entry(&work_directory, &tree).git_dir_path.as_ref(),
+                Path::new("dir1/deps/dep1/.git"),
+            );
         });
 
         let original_scan_id = tree.read_with(cx, |tree, _cx| {
             let tree = tree.as_local().unwrap();
-            tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id
+            let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap();
+            entry(&key, &tree).scan_id
         });
 
         std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
@@ -3410,7 +3467,10 @@ mod tests {
 
         tree.read_with(cx, |tree, _cx| {
             let tree = tree.as_local().unwrap();
-            let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
+            let new_scan_id = {
+                let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap();
+                entry(&key, &tree).scan_id
+            };
             assert_ne!(
                 original_scan_id, new_scan_id,
                 "original {original_scan_id}, new {new_scan_id}"
@@ -3423,7 +3483,7 @@ mod tests {
         tree.read_with(cx, |tree, _cx| {
             let tree = tree.as_local().unwrap();
 
-            assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
+            assert!(tree.repo_for_metadata("dir1/src/b.txt".as_ref()).is_none());
         });
     }
 
@@ -3434,6 +3494,9 @@ mod tests {
                 scan_id,
                 git_dir_path: git_dir_path.as_ref().into(),
                 git_dir_entry_id: ProjectEntryId(0),
+                work_directory: RepositoryWorkDirectory(
+                    Path::new(&format!("don't-care-{}", scan_id)).into(),
+                ),
             }
         }