Add git status to the file system abstraction

Mikayla Maki and petros created

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

Change summary

Cargo.lock                     |  1 
crates/fs/Cargo.toml           |  1 
crates/fs/src/repository.rs    | 82 +++++++++++++++++++++++++++++++++++
crates/project/src/worktree.rs | 37 ++++-----------
4 files changed, 94 insertions(+), 27 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2350,6 +2350,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "smol",
+ "sum_tree",
  "tempfile",
  "util",
 ]

crates/fs/Cargo.toml 🔗

@@ -13,6 +13,7 @@ gpui = { path = "../gpui" }
 lsp = { path = "../lsp" }
 rope = { path = "../rope" }
 util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
 anyhow.workspace = true
 async-trait.workspace = true
 futures.workspace = true

crates/fs/src/repository.rs 🔗

@@ -1,9 +1,10 @@
 use anyhow::Result;
 use collections::HashMap;
 use parking_lot::Mutex;
+use sum_tree::TreeMap;
 use std::{
     path::{Component, Path, PathBuf},
-    sync::Arc,
+    sync::Arc, ffi::OsStr, os::unix::prelude::OsStrExt,
 };
 use util::ResultExt;
 
@@ -16,6 +17,8 @@ pub trait GitRepository: Send {
     fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
 
     fn branch_name(&self) -> Option<String>;
+
+    fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>>;
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -61,6 +64,79 @@ impl GitRepository for LibGitRepository {
         let branch = String::from_utf8_lossy(head.shorthand_bytes());
         Some(branch.to_string())
     }
+
+    fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>> {
+        let statuses = self.statuses(None).log_err()?;
+
+        let mut map = TreeMap::default();
+
+        for status in statuses.iter() {
+            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)
+        }
+
+        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);
+
+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
+    }
 }
 
 #[derive(Debug, Clone, Default)]
@@ -93,6 +169,10 @@ impl GitRepository for FakeGitRepository {
         let state = self.state.lock();
         state.branch_name.clone()
     }
+
+    fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>>{
+        todo!()
+    }
 }
 
 fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {

crates/project/src/worktree.rs 🔗

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client};
 use clock::ReplicaId;
 use collections::{HashMap, VecDeque};
-use fs::{repository::GitRepository, Fs, LineEnding};
+use fs::{repository::{GitRepository, RepoPath, GitStatus}, Fs, LineEnding};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
@@ -117,10 +117,11 @@ pub struct Snapshot {
     completed_scan_id: usize,
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct RepositoryEntry {
     pub(crate) work_directory: WorkDirectoryEntry,
     pub(crate) branch: Option<Arc<str>>,
+    // pub(crate) statuses: TreeMap<RepoPath, GitStatus>
 }
 
 impl RepositoryEntry {
@@ -162,6 +163,13 @@ impl Default for RepositoryWorkDirectory {
     }
 }
 
+impl AsRef<Path> for RepositoryWorkDirectory {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
+    }
+}
+
+
 #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
 pub struct WorkDirectoryEntry(ProjectEntryId);
 
@@ -178,7 +186,7 @@ impl WorkDirectoryEntry {
         worktree.entry_for_id(self.0).and_then(|entry| {
             path.strip_prefix(&entry.path)
                 .ok()
-                .map(move |path| RepoPath(path.to_owned()))
+                .map(move |path| path.into())
         })
     }
 }
@@ -197,29 +205,6 @@ impl<'a> From<ProjectEntryId> for WorkDirectoryEntry {
     }
 }
 
-#[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()
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct LocalSnapshot {
     ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,