Introduce primitives in `GitStore` to support reviewing assistant diffs (#27576)

Antonio Scandurra created

Release Notes:

- N/A

Change summary

crates/assistant2/src/message_editor.rs |   2 
crates/fs/src/fake_git_repo.rs          |  42 ++
crates/git/src/repository.rs            | 382 ++++++++++++++++++++++++--
crates/git_ui/src/git_panel.rs          |   2 
crates/git_ui/src/project_diff.rs       |   2 
crates/project/src/git_store.rs         | 286 +++++++++++++++++++
crates/worktree/src/worktree.rs         |   5 
7 files changed, 662 insertions(+), 59 deletions(-)

Detailed changes

crates/assistant2/src/message_editor.rs 🔗

@@ -317,7 +317,7 @@ impl Render for MessageEditor {
 
         let project = self.thread.read(cx).project();
         let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
-            repository.read(cx).status().count()
+            repository.read(cx).cached_status().count()
         } else {
             0
         };

crates/fs/src/fake_git_repo.rs 🔗

@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
 use git::{
     blame::Blame,
     repository::{
-        AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
-        Remote, RepoPath, ResetMode,
+        AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
+        PushOptions, Remote, RepoPath, ResetMode,
     },
     status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
 };
@@ -81,7 +81,15 @@ impl FakeGitRepository {
 impl GitRepository for FakeGitRepository {
     fn reload_index(&self) {}
 
-    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
+    fn load_index_text(
+        &self,
+        index: Option<GitIndex>,
+        path: RepoPath,
+    ) -> BoxFuture<Option<String>> {
+        if index.is_some() {
+            unimplemented!();
+        }
+
         async {
             self.with_state_async(false, move |state| {
                 state
@@ -171,7 +179,15 @@ impl GitRepository for FakeGitRepository {
         self.path()
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>> {
+    fn status(
+        &self,
+        index: Option<GitIndex>,
+        path_prefixes: &[RepoPath],
+    ) -> BoxFuture<'static, Result<GitStatus>> {
+        if index.is_some() {
+            unimplemented!();
+        }
+
         let status = self.status_blocking(path_prefixes);
         async move { status }.boxed()
     }
@@ -414,7 +430,7 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 
-    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
+    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
         unimplemented!()
     }
 
@@ -433,4 +449,20 @@ impl GitRepository for FakeGitRepository {
     fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
+
+    fn diff_checkpoints(
+        &self,
+        _base_checkpoint: GitRepositoryCheckpoint,
+        _target_checkpoint: GitRepositoryCheckpoint,
+    ) -> BoxFuture<Result<String>> {
+        unimplemented!()
+    }
+
+    fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
+        unimplemented!()
+    }
+
+    fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
+        unimplemented!()
+    }
 }

crates/git/src/repository.rs 🔗

@@ -12,7 +12,6 @@ use schemars::JsonSchema;
 use serde::Deserialize;
 use std::borrow::{Borrow, Cow};
 use std::ffi::{OsStr, OsString};
-use std::future;
 use std::path::Component;
 use std::process::{ExitStatus, Stdio};
 use std::sync::LazyLock;
@@ -21,6 +20,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+use std::{future, mem};
 use sum_tree::MapSeekTarget;
 use thiserror::Error;
 use util::command::{new_smol_command, new_std_command};
@@ -161,7 +161,8 @@ pub trait GitRepository: Send + Sync {
     /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
     ///
     /// Also returns `None` for symlinks.
-    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
+    fn load_index_text(&self, index: Option<GitIndex>, path: RepoPath)
+        -> BoxFuture<Option<String>>;
 
     /// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
     ///
@@ -183,7 +184,11 @@ pub trait GitRepository: Send + Sync {
 
     fn merge_head_shas(&self) -> Vec<String>;
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>>;
+    fn status(
+        &self,
+        index: Option<GitIndex>,
+        path_prefixes: &[RepoPath],
+    ) -> BoxFuture<'static, Result<GitStatus>>;
     fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
@@ -286,7 +291,7 @@ pub trait GitRepository: Send + Sync {
     fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
 
     /// Creates a checkpoint for the repository.
-    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>>;
+    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
 
     /// Resets to a previously-created checkpoint.
     fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
@@ -300,6 +305,19 @@ pub trait GitRepository: Send + Sync {
 
     /// Deletes a previously-created checkpoint.
     fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
+
+    /// Computes a diff between two checkpoints.
+    fn diff_checkpoints(
+        &self,
+        base_checkpoint: GitRepositoryCheckpoint,
+        target_checkpoint: GitRepositoryCheckpoint,
+    ) -> BoxFuture<Result<String>>;
+
+    /// Creates a new index for the repository.
+    fn create_index(&self) -> BoxFuture<Result<GitIndex>>;
+
+    /// Applies a diff to the repository's index.
+    fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>>;
 }
 
 pub enum DiffType {
@@ -356,8 +374,10 @@ pub struct GitRepositoryCheckpoint {
     commit_sha: Oid,
 }
 
-// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
-const GIT_MODE_SYMLINK: u32 = 0o120000;
+#[derive(Copy, Clone, Debug)]
+pub struct GitIndex {
+    id: Uuid,
+}
 
 impl GitRepository for RealGitRepository {
     fn reload_index(&self) {
@@ -464,31 +484,82 @@ impl GitRepository for RealGitRepository {
         .boxed()
     }
 
-    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
-        let repo = self.repository.clone();
+    fn load_index_text(
+        &self,
+        index: Option<GitIndex>,
+        path: RepoPath,
+    ) -> BoxFuture<Option<String>> {
+        let working_directory = self.working_directory();
+        let git_binary_path = self.git_binary_path.clone();
+        let executor = self.executor.clone();
         self.executor
             .spawn(async move {
-                fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
-                    // This check is required because index.get_path() unwraps internally :(
-                    check_path_to_repo_path_errors(path)?;
+                match check_path_to_repo_path_errors(&path) {
+                    Ok(_) => {}
+                    Err(err) => {
+                        log::error!("Error with repo path: {:?}", err);
+                        return None;
+                    }
+                }
 
-                    let mut index = repo.index()?;
-                    index.read(false)?;
+                let working_directory = match working_directory {
+                    Ok(dir) => dir,
+                    Err(err) => {
+                        log::error!("Error getting working directory: {:?}", err);
+                        return None;
+                    }
+                };
 
-                    const STAGE_NORMAL: i32 = 0;
-                    let oid = match index.get_path(path, STAGE_NORMAL) {
-                        Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
-                        _ => return Ok(None),
-                    };
+                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+                let text = git
+                    .with_option_index(index, async |git| {
+                        // First check if the file is a symlink using ls-files
+                        let ls_files_output = git
+                            .run(&[
+                                OsStr::new("ls-files"),
+                                OsStr::new("--stage"),
+                                path.to_unix_style().as_ref(),
+                            ])
+                            .await
+                            .context("error running ls-files")?;
+
+                        // Parse ls-files output to check if it's a symlink
+                        // Format is: "100644 <sha> 0 <filename>" where 100644 is the mode
+                        if ls_files_output.is_empty() {
+                            return Ok(None); // File not in index
+                        }
 
-                    let content = repo.find_blob(oid)?.content().to_owned();
-                    Ok(Some(String::from_utf8(content)?))
-                }
-                match logic(&repo.lock(), &path) {
-                    Ok(value) => return value,
-                    Err(err) => log::error!("Error loading index text: {:?}", err),
+                        let parts: Vec<&str> = ls_files_output.split_whitespace().collect();
+                        if parts.len() < 2 {
+                            return Err(anyhow!(
+                                "unexpected ls-files output format: {}",
+                                ls_files_output
+                            ));
+                        }
+
+                        // Check if it's a symlink (120000 mode)
+                        if parts[0] == "120000" {
+                            return Ok(None);
+                        }
+
+                        let sha = parts[1];
+
+                        // Now get the content
+                        Ok(Some(
+                            git.run_raw(&["cat-file", "blob", sha])
+                                .await
+                                .context("error getting blob content")?,
+                        ))
+                    })
+                    .await;
+
+                match text {
+                    Ok(text) => text,
+                    Err(error) => {
+                        log::error!("Error getting text: {}", error);
+                        None
+                    }
                 }
-                None
             })
             .boxed()
     }
@@ -607,16 +678,36 @@ impl GitRepository for RealGitRepository {
         shas
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>> {
+    fn status(
+        &self,
+        index: Option<GitIndex>,
+        path_prefixes: &[RepoPath],
+    ) -> BoxFuture<'static, Result<GitStatus>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
         let executor = self.executor.clone();
-        let args = git_status_args(path_prefixes);
+        let mut args = vec![
+            OsString::from("--no-optional-locks"),
+            OsString::from("status"),
+            OsString::from("--porcelain=v1"),
+            OsString::from("--untracked-files=all"),
+            OsString::from("--no-renames"),
+            OsString::from("-z"),
+        ];
+        args.extend(path_prefixes.iter().map(|path_prefix| {
+            if path_prefix.0.as_ref() == Path::new("") {
+                Path::new(".").into()
+            } else {
+                path_prefix.as_os_str().into()
+            }
+        }));
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let git = GitBinary::new(git_binary_path, working_directory, executor);
-                git.run(&args).await?.parse()
+                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+                git.with_option_index(index, async |git| git.run(&args).await)
+                    .await?
+                    .parse()
             })
             .boxed()
     }
@@ -1071,7 +1162,7 @@ impl GitRepository for RealGitRepository {
             .boxed()
     }
 
-    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
+    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
         let executor = self.executor.clone();
@@ -1203,6 +1294,66 @@ impl GitRepository for RealGitRepository {
             })
             .boxed()
     }
+
+    fn diff_checkpoints(
+        &self,
+        base_checkpoint: GitRepositoryCheckpoint,
+        target_checkpoint: GitRepositoryCheckpoint,
+    ) -> BoxFuture<Result<String>> {
+        let working_directory = self.working_directory();
+        let git_binary_path = self.git_binary_path.clone();
+
+        let executor = self.executor.clone();
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                let git = GitBinary::new(git_binary_path, working_directory, executor);
+                git.run(&[
+                    "diff",
+                    "--find-renames",
+                    "--patch",
+                    &base_checkpoint.ref_name,
+                    &target_checkpoint.ref_name,
+                ])
+                .await
+            })
+            .boxed()
+    }
+
+    fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
+        let working_directory = self.working_directory();
+        let git_binary_path = self.git_binary_path.clone();
+
+        let executor = self.executor.clone();
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+                let index = GitIndex { id: Uuid::new_v4() };
+                git.with_index(index, async move |git| git.run(&["add", "--all"]).await)
+                    .await?;
+                Ok(index)
+            })
+            .boxed()
+    }
+
+    fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>> {
+        let working_directory = self.working_directory();
+        let git_binary_path = self.git_binary_path.clone();
+
+        let executor = self.executor.clone();
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+                git.with_index(index, async move |git| {
+                    git.run_with_stdin(&["apply", "--cached", "-"], diff).await
+                })
+                .await?;
+                Ok(())
+            })
+            .boxed()
+    }
 }
 
 fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
@@ -1256,7 +1407,7 @@ impl GitBinary {
         &mut self,
         f: impl AsyncFnOnce(&Self) -> Result<R>,
     ) -> Result<R> {
-        let index_file_path = self.working_directory.join(".git/index.tmp");
+        let index_file_path = self.path_for_index(GitIndex { id: Uuid::new_v4() });
 
         let delete_temp_index = util::defer({
             let index_file_path = index_file_path.clone();
@@ -1281,20 +1432,52 @@ impl GitBinary {
         Ok(result)
     }
 
+    pub async fn with_index<R>(
+        &mut self,
+        index: GitIndex,
+        f: impl AsyncFnOnce(&Self) -> Result<R>,
+    ) -> Result<R> {
+        self.with_option_index(Some(index), f).await
+    }
+
+    pub async fn with_option_index<R>(
+        &mut self,
+        index: Option<GitIndex>,
+        f: impl AsyncFnOnce(&Self) -> Result<R>,
+    ) -> Result<R> {
+        let new_index_path = index.map(|index| self.path_for_index(index));
+        let old_index_path = mem::replace(&mut self.index_file_path, new_index_path);
+        let result = f(self).await;
+        self.index_file_path = old_index_path;
+        result
+    }
+
+    fn path_for_index(&self, index: GitIndex) -> PathBuf {
+        self.working_directory
+            .join(".git")
+            .join(format!("index-{}.tmp", index.id))
+    }
+
     pub async fn run<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
     where
         S: AsRef<OsStr>,
     {
-        let mut command = new_smol_command(&self.git_binary_path);
-        command.current_dir(&self.working_directory);
-        command.args(args);
-        if let Some(index_file_path) = self.index_file_path.as_ref() {
-            command.env("GIT_INDEX_FILE", index_file_path);
+        let mut stdout = self.run_raw(args).await?;
+        if stdout.chars().last() == Some('\n') {
+            stdout.pop();
         }
-        command.envs(&self.envs);
+        Ok(stdout)
+    }
+
+    /// Returns the result of the command without trimming the trailing newline.
+    pub async fn run_raw<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
+    where
+        S: AsRef<OsStr>,
+    {
+        let mut command = self.build_command(args);
         let output = command.output().await?;
         if output.status.success() {
-            anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
+            Ok(String::from_utf8(output.stdout)?)
         } else {
             Err(anyhow!(GitBinaryCommandError {
                 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
@@ -1302,6 +1485,40 @@ impl GitBinary {
             }))
         }
     }
+
+    pub async fn run_with_stdin(&self, args: &[&str], stdin: String) -> Result<String> {
+        let mut command = self.build_command(args);
+        command.stdin(Stdio::piped());
+        let mut child = command.spawn()?;
+
+        let mut child_stdin = child.stdin.take().context("failed to write to stdin")?;
+        child_stdin.write_all(stdin.as_bytes()).await?;
+        drop(child_stdin);
+
+        let output = child.output().await?;
+        if output.status.success() {
+            Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
+        } else {
+            Err(anyhow!(GitBinaryCommandError {
+                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
+                status: output.status,
+            }))
+        }
+    }
+
+    fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
+    where
+        S: AsRef<OsStr>,
+    {
+        let mut command = new_smol_command(&self.git_binary_path);
+        command.current_dir(&self.working_directory);
+        command.args(args);
+        if let Some(index_file_path) = self.index_file_path.as_ref() {
+            command.env("GIT_INDEX_FILE", index_file_path);
+        }
+        command.envs(&self.envs);
+        command
+    }
 }
 
 #[derive(Error, Debug)]
@@ -1570,8 +1787,9 @@ fn checkpoint_author_envs() -> HashMap<String, String> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::status::FileStatus;
+    use crate::status::{FileStatus, StatusCode, TrackedStatus};
     use gpui::TestAppContext;
+    use unindent::Unindent;
 
     #[gpui::test]
     async fn test_checkpoint_basic(cx: &mut TestAppContext) {
@@ -1751,7 +1969,7 @@ mod tests {
             "content2"
         );
         assert_eq!(
-            repo.status(&[]).await.unwrap().entries.as_ref(),
+            repo.status(None, &[]).await.unwrap().entries.as_ref(),
             &[
                 (RepoPath::from_str("new_file1"), FileStatus::Untracked),
                 (RepoPath::from_str("new_file2"), FileStatus::Untracked)
@@ -1790,6 +2008,90 @@ mod tests {
             .unwrap());
     }
 
+    #[gpui::test]
+    async fn test_secondary_indices(cx: &mut TestAppContext) {
+        cx.executor().allow_parking();
+
+        let repo_dir = tempfile::tempdir().unwrap();
+        git2::Repository::init(repo_dir.path()).unwrap();
+        let repo =
+            RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
+        let index = repo.create_index().await.unwrap();
+        smol::fs::write(repo_dir.path().join("file1"), "file1\n")
+            .await
+            .unwrap();
+        smol::fs::write(repo_dir.path().join("file2"), "file2\n")
+            .await
+            .unwrap();
+        let diff = r#"
+            diff --git a/file2 b/file2
+            new file mode 100644
+            index 0000000..cbc4e2e
+            --- /dev/null
+            +++ b/file2
+            @@ -0,0 +1 @@
+            +file2
+        "#
+        .unindent();
+        repo.apply_diff(index, diff.to_string()).await.unwrap();
+
+        assert_eq!(
+            repo.status(Some(index), &[])
+                .await
+                .unwrap()
+                .entries
+                .as_ref(),
+            vec![
+                (RepoPath::from_str("file1"), FileStatus::Untracked),
+                (
+                    RepoPath::from_str("file2"),
+                    FileStatus::index(StatusCode::Added)
+                )
+            ]
+        );
+        assert_eq!(
+            repo.load_index_text(Some(index), RepoPath::from_str("file1"))
+                .await,
+            None
+        );
+        assert_eq!(
+            repo.load_index_text(Some(index), RepoPath::from_str("file2"))
+                .await,
+            Some("file2\n".to_string())
+        );
+
+        smol::fs::write(repo_dir.path().join("file2"), "file2-changed\n")
+            .await
+            .unwrap();
+        assert_eq!(
+            repo.status(Some(index), &[])
+                .await
+                .unwrap()
+                .entries
+                .as_ref(),
+            vec![
+                (RepoPath::from_str("file1"), FileStatus::Untracked),
+                (
+                    RepoPath::from_str("file2"),
+                    FileStatus::Tracked(TrackedStatus {
+                        worktree_status: StatusCode::Modified,
+                        index_status: StatusCode::Added,
+                    })
+                )
+            ]
+        );
+        assert_eq!(
+            repo.load_index_text(Some(index), RepoPath::from_str("file1"))
+                .await,
+            None
+        );
+        assert_eq!(
+            repo.load_index_text(Some(index), RepoPath::from_str("file2"))
+                .await,
+            Some("file2\n".to_string())
+        );
+    }
+
     #[test]
     fn test_branches_parsing() {
         // suppress "help: octal escapes are not supported, `\0` is always null"

crates/git_ui/src/git_panel.rs 🔗

@@ -2259,7 +2259,7 @@ impl GitPanel {
 
         let repo = repo.read(cx);
 
-        for entry in repo.status() {
+        for entry in repo.cached_status() {
             let is_conflict = repo.has_conflict(&entry.repo_path);
             let is_new = entry.status.is_created();
             let staging = entry.status.staging();

crates/git_ui/src/project_diff.rs 🔗

@@ -339,7 +339,7 @@ impl ProjectDiff {
 
         let mut result = vec![];
         repo.update(cx, |repo, cx| {
-            for entry in repo.status() {
+            for entry in repo.cached_status() {
                 if !entry.status.has_changes() {
                     continue;
                 }

crates/project/src/git_store.rs 🔗

@@ -20,10 +20,10 @@ use git::{
     blame::Blame,
     parse_git_remote_url,
     repository::{
-        Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
-        Remote, RemoteCommandOutput, RepoPath, ResetMode,
+        Branch, CommitDetails, DiffType, GitIndex, GitRepository, GitRepositoryCheckpoint,
+        PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
     },
-    status::FileStatus,
+    status::{FileStatus, GitStatus},
     BuildPermalinkParams, GitHostingProviderRegistry,
 };
 use gpui::{
@@ -146,6 +146,22 @@ pub struct GitStoreCheckpoint {
     checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
 }
 
+#[derive(Clone, Debug)]
+pub struct GitStoreDiff {
+    diffs_by_work_dir_abs_path: HashMap<PathBuf, String>,
+}
+
+#[derive(Clone, Debug)]
+pub struct GitStoreIndex {
+    indices_by_work_dir_abs_path: HashMap<PathBuf, GitIndex>,
+}
+
+#[derive(Default)]
+pub struct GitStoreStatus {
+    #[allow(dead_code)]
+    statuses_by_work_dir_abs_path: HashMap<PathBuf, GitStatus>,
+}
+
 pub struct Repository {
     pub repository_entry: RepositoryEntry,
     pub merge_message: Option<String>,
@@ -651,8 +667,8 @@ impl GitStore {
             .collect::<HashMap<_, _>>();
 
         let mut tasks = Vec::new();
-        for (dot_git_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
-            if let Some(repository) = repositories_by_work_dir_abs_path.get(&dot_git_abs_path) {
+        for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
+            if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
                 let restore = repository.read(cx).restore_checkpoint(checkpoint);
                 tasks.push(async move { restore.await? });
             }
@@ -685,12 +701,13 @@ impl GitStore {
             .collect::<HashMap<_, _>>();
 
         let mut tasks = Vec::new();
-        for (dot_git_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
+        for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
             if let Some(right_checkpoint) = right
                 .checkpoints_by_work_dir_abs_path
-                .remove(&dot_git_abs_path)
+                .remove(&work_dir_abs_path)
             {
-                if let Some(repository) = repositories_by_work_dir_abs_path.get(&dot_git_abs_path) {
+                if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
+                {
                     let compare = repository
                         .read(cx)
                         .compare_checkpoints(left_checkpoint, right_checkpoint);
@@ -738,6 +755,113 @@ impl GitStore {
         })
     }
 
+    pub fn diff_checkpoints(
+        &self,
+        base_checkpoint: GitStoreCheckpoint,
+        target_checkpoint: GitStoreCheckpoint,
+        cx: &App,
+    ) -> Task<Result<GitStoreDiff>> {
+        let repositories_by_work_dir_abs_path = self
+            .repositories
+            .values()
+            .map(|repo| {
+                (
+                    repo.read(cx)
+                        .repository_entry
+                        .work_directory_abs_path
+                        .clone(),
+                    repo,
+                )
+            })
+            .collect::<HashMap<_, _>>();
+
+        let mut tasks = Vec::new();
+        for (work_dir_abs_path, base_checkpoint) in base_checkpoint.checkpoints_by_work_dir_abs_path
+        {
+            if let Some(target_checkpoint) = target_checkpoint
+                .checkpoints_by_work_dir_abs_path
+                .get(&work_dir_abs_path)
+                .cloned()
+            {
+                if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
+                {
+                    let diff = repository
+                        .read(cx)
+                        .diff_checkpoints(base_checkpoint, target_checkpoint);
+                    tasks.push(async move {
+                        let diff = diff.await??;
+                        anyhow::Ok((work_dir_abs_path, diff))
+                    });
+                }
+            }
+        }
+
+        cx.background_spawn(async move {
+            let diffs_by_path = future::try_join_all(tasks).await?;
+            Ok(GitStoreDiff {
+                diffs_by_work_dir_abs_path: diffs_by_path.into_iter().collect(),
+            })
+        })
+    }
+
+    pub fn create_index(&self, cx: &App) -> Task<Result<GitStoreIndex>> {
+        let mut indices = Vec::new();
+        for repository in self.repositories.values() {
+            let repository = repository.read(cx);
+            let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
+            let index = repository.create_index().map(|index| index?);
+            indices.push(async move {
+                let index = index.await?;
+                anyhow::Ok((work_dir_abs_path, index))
+            });
+        }
+
+        cx.background_executor().spawn(async move {
+            let indices = future::try_join_all(indices).await?;
+            Ok(GitStoreIndex {
+                indices_by_work_dir_abs_path: indices.into_iter().collect(),
+            })
+        })
+    }
+
+    pub fn apply_diff(
+        &self,
+        mut index: GitStoreIndex,
+        diff: GitStoreDiff,
+        cx: &App,
+    ) -> Task<Result<()>> {
+        let repositories_by_work_dir_abs_path = self
+            .repositories
+            .values()
+            .map(|repo| {
+                (
+                    repo.read(cx)
+                        .repository_entry
+                        .work_directory_abs_path
+                        .clone(),
+                    repo,
+                )
+            })
+            .collect::<HashMap<_, _>>();
+
+        let mut tasks = Vec::new();
+        for (work_dir_abs_path, diff) in diff.diffs_by_work_dir_abs_path {
+            if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
+                if let Some(branch) = index
+                    .indices_by_work_dir_abs_path
+                    .remove(&work_dir_abs_path)
+                {
+                    let apply = repository.read(cx).apply_diff(branch, diff);
+                    tasks.push(async move { apply.await? });
+                }
+            }
+        }
+        cx.background_spawn(async move {
+            future::try_join_all(tasks).await?;
+            Ok(())
+        })
+    }
+
     /// Blames a buffer.
     pub fn blame_buffer(
         &self,
@@ -1282,7 +1406,7 @@ impl GitStore {
                         let index_text = if current_index_text.is_some() {
                             local_repo
                                 .repo()
-                                .load_index_text(relative_path.clone())
+                                .load_index_text(None, relative_path.clone())
                                 .await
                         } else {
                             None
@@ -1397,6 +1521,87 @@ impl GitStore {
         Some(status.status)
     }
 
+    pub fn status(&self, index: Option<GitStoreIndex>, cx: &App) -> Task<Result<GitStoreStatus>> {
+        let repositories_by_work_dir_abs_path = self
+            .repositories
+            .values()
+            .map(|repo| {
+                (
+                    repo.read(cx)
+                        .repository_entry
+                        .work_directory_abs_path
+                        .clone(),
+                    repo,
+                )
+            })
+            .collect::<HashMap<_, _>>();
+
+        let mut tasks = Vec::new();
+
+        if let Some(index) = index {
+            // When we have an index, just check the repositories that are part of it
+            for (work_dir_abs_path, git_index) in index.indices_by_work_dir_abs_path {
+                if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
+                {
+                    let status = repository.read(cx).status(Some(git_index));
+                    tasks.push(
+                        async move {
+                            let status = status.await??;
+                            anyhow::Ok((work_dir_abs_path, status))
+                        }
+                        .boxed(),
+                    );
+                }
+            }
+        } else {
+            // Otherwise, check all repositories
+            for repository in self.repositories.values() {
+                let repository = repository.read(cx);
+                let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
+                let status = repository.status(None);
+                tasks.push(
+                    async move {
+                        let status = status.await??;
+                        anyhow::Ok((work_dir_abs_path, status))
+                    }
+                    .boxed(),
+                );
+            }
+        }
+
+        cx.background_executor().spawn(async move {
+            let statuses = future::try_join_all(tasks).await?;
+            Ok(GitStoreStatus {
+                statuses_by_work_dir_abs_path: statuses.into_iter().collect(),
+            })
+        })
+    }
+
+    pub fn load_index_text(
+        &self,
+        index: Option<GitStoreIndex>,
+        buffer: &Entity<Buffer>,
+        cx: &App,
+    ) -> Task<Option<String>> {
+        let Some((repository, path)) =
+            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
+        else {
+            return Task::ready(None);
+        };
+
+        let git_index = index.and_then(|index| {
+            index
+                .indices_by_work_dir_abs_path
+                .get(&repository.read(cx).repository_entry.work_directory_abs_path)
+                .copied()
+        });
+        let text = repository.read(cx).load_index_text(git_index, path);
+        cx.background_spawn(async move {
+            let text = text.await;
+            text.ok().flatten()
+        })
+    }
+
     pub fn repository_and_path_for_buffer_id(
         &self,
         buffer_id: BufferId,
@@ -2642,10 +2847,34 @@ impl Repository {
         });
     }
 
-    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
+    pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
         self.repository_entry.status()
     }
 
+    pub fn status(&self, index: Option<GitIndex>) -> oneshot::Receiver<Result<GitStatus>> {
+        self.send_job(move |repo, _cx| async move {
+            match repo {
+                RepositoryState::Local(git_repository) => git_repository.status(index, &[]).await,
+                RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
+            }
+        })
+    }
+
+    pub fn load_index_text(
+        &self,
+        index: Option<GitIndex>,
+        path: RepoPath,
+    ) -> oneshot::Receiver<Option<String>> {
+        self.send_job(move |repo, _cx| async move {
+            match repo {
+                RepositoryState::Local(git_repository) => {
+                    git_repository.load_index_text(index, path).await
+                }
+                RepositoryState::Remote { .. } => None,
+            }
+        })
+    }
+
     pub fn has_conflict(&self, path: &RepoPath) -> bool {
         self.repository_entry
             .current_merge_conflicts
@@ -3533,6 +3762,43 @@ impl Repository {
             }
         })
     }
+
+    pub fn diff_checkpoints(
+        &self,
+        base_checkpoint: GitRepositoryCheckpoint,
+        target_checkpoint: GitRepositoryCheckpoint,
+    ) -> oneshot::Receiver<Result<String>> {
+        self.send_job(move |repo, _cx| async move {
+            match repo {
+                RepositoryState::Local(git_repository) => {
+                    git_repository
+                        .diff_checkpoints(base_checkpoint, target_checkpoint)
+                        .await
+                }
+                RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
+            }
+        })
+    }
+
+    pub fn create_index(&self) -> oneshot::Receiver<Result<GitIndex>> {
+        self.send_job(move |repo, _cx| async move {
+            match repo {
+                RepositoryState::Local(git_repository) => git_repository.create_index().await,
+                RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
+            }
+        })
+    }
+
+    pub fn apply_diff(&self, index: GitIndex, diff: String) -> oneshot::Receiver<Result<()>> {
+        self.send_job(move |repo, _cx| async move {
+            match repo {
+                RepositoryState::Local(git_repository) => {
+                    git_repository.apply_diff(index, diff).await
+                }
+                RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
+            }
+        })
+    }
 }
 
 fn get_permalink_in_rust_registry_src(

crates/worktree/src/worktree.rs 🔗

@@ -1041,7 +1041,10 @@ impl Worktree {
                             if let Some(git_repo) =
                                 snapshot.git_repositories.get(&repo.work_directory_id)
                             {
-                                return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
+                                return Ok(git_repo
+                                    .repo_ptr
+                                    .load_index_text(None, repo_path)
+                                    .await);
                             }
                         }
                     }