From 82a06f0ca924ebaeafb4ed3f72dfafa4ef2e0090 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 27 Mar 2025 10:46:31 +0100 Subject: [PATCH] Introduce primitives in `GitStore` to support reviewing assistant diffs (#27576) Release Notes: - N/A --- crates/assistant2/src/message_editor.rs | 2 +- crates/fs/src/fake_git_repo.rs | 42 ++- crates/git/src/repository.rs | 390 +++++++++++++++++++++--- 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, 666 insertions(+), 63 deletions(-) diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 40e513a506..ed1167f192 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/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 }; diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 8e90f8cc07..89ecd57033 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/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> { + fn load_index_text( + &self, + index: Option, + path: RepoPath, + ) -> BoxFuture> { + 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> { + fn status( + &self, + index: Option, + path_prefixes: &[RepoPath], + ) -> BoxFuture<'static, Result> { + 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> { + fn checkpoint(&self) -> BoxFuture<'static, Result> { unimplemented!() } @@ -433,4 +449,20 @@ impl GitRepository for FakeGitRepository { fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture> { unimplemented!() } + + fn diff_checkpoints( + &self, + _base_checkpoint: GitRepositoryCheckpoint, + _target_checkpoint: GitRepositoryCheckpoint, + ) -> BoxFuture> { + unimplemented!() + } + + fn create_index(&self) -> BoxFuture> { + unimplemented!() + } + + fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture> { + unimplemented!() + } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index f45cc53dcc..6780997a6b 100644 --- a/crates/git/src/repository.rs +++ b/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>; + fn load_index_text(&self, index: Option, path: RepoPath) + -> BoxFuture>; /// 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; - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result>; + fn status( + &self, + index: Option, + path_prefixes: &[RepoPath], + ) -> BoxFuture<'static, Result>; fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result; fn branches(&self) -> BoxFuture>>; @@ -286,7 +291,7 @@ pub trait GitRepository: Send + Sync { fn diff(&self, diff: DiffType) -> BoxFuture>; /// Creates a checkpoint for the repository. - fn checkpoint(&self) -> BoxFuture>; + fn checkpoint(&self) -> BoxFuture<'static, Result>; /// Resets to a previously-created checkpoint. fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture>; @@ -300,6 +305,19 @@ pub trait GitRepository: Send + Sync { /// Deletes a previously-created checkpoint. fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture>; + + /// Computes a diff between two checkpoints. + fn diff_checkpoints( + &self, + base_checkpoint: GitRepositoryCheckpoint, + target_checkpoint: GitRepositoryCheckpoint, + ) -> BoxFuture>; + + /// Creates a new index for the repository. + fn create_index(&self) -> BoxFuture>; + + /// Applies a diff to the repository's index. + fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture>; } 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> { - let repo = self.repository.clone(); + fn load_index_text( + &self, + index: Option, + path: RepoPath, + ) -> BoxFuture> { + 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> { - // This check is required because index.get_path() unwraps internally :( - check_path_to_repo_path_errors(path)?; - - let mut index = repo.index()?; - index.read(false)?; - - 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 content = repo.find_blob(oid)?.content().to_owned(); - Ok(Some(String::from_utf8(content)?)) + match check_path_to_repo_path_errors(&path) { + Ok(_) => {} + Err(err) => { + log::error!("Error with repo path: {:?}", err); + return None; + } } - match logic(&repo.lock(), &path) { - Ok(value) => return value, - Err(err) => log::error!("Error loading index text: {:?}", err), + + let working_directory = match working_directory { + Ok(dir) => dir, + Err(err) => { + log::error!("Error getting working directory: {:?}", err); + return 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 0 " where 100644 is the mode + if ls_files_output.is_empty() { + return Ok(None); // File not in index + } + + 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> { + fn status( + &self, + index: Option, + path_prefixes: &[RepoPath], + ) -> BoxFuture<'static, Result> { 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> { + fn checkpoint(&self) -> BoxFuture<'static, Result> { 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> { + 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> { + 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> { + 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 { @@ -1256,7 +1407,7 @@ impl GitBinary { &mut self, f: impl AsyncFnOnce(&Self) -> Result, ) -> Result { - 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,7 +1432,81 @@ impl GitBinary { Ok(result) } + pub async fn with_index( + &mut self, + index: GitIndex, + f: impl AsyncFnOnce(&Self) -> Result, + ) -> Result { + self.with_option_index(Some(index), f).await + } + + pub async fn with_option_index( + &mut self, + index: Option, + f: impl AsyncFnOnce(&Self) -> Result, + ) -> Result { + 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(&self, args: impl IntoIterator) -> Result + where + S: AsRef, + { + let mut stdout = self.run_raw(args).await?; + if stdout.chars().last() == Some('\n') { + stdout.pop(); + } + Ok(stdout) + } + + /// Returns the result of the command without trimming the trailing newline. + pub async fn run_raw(&self, args: impl IntoIterator) -> Result + where + S: AsRef, + { + let mut command = self.build_command(args); + let output = command.output().await?; + if output.status.success() { + Ok(String::from_utf8(output.stdout)?) + } else { + Err(anyhow!(GitBinaryCommandError { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + status: output.status, + })) + } + } + + pub async fn run_with_stdin(&self, args: &[&str], stdin: String) -> Result { + 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(&self, args: impl IntoIterator) -> smol::process::Command where S: AsRef, { @@ -1292,15 +1517,7 @@ impl GitBinary { command.env("GIT_INDEX_FILE", index_file_path); } command.envs(&self.envs); - let output = command.output().await?; - if output.status.success() { - anyhow::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, - })) - } + command } } @@ -1570,8 +1787,9 @@ fn checkpoint_author_envs() -> HashMap { #[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" diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 3296df0ac8..4bffbb591c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/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(); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 8f37074674..815dfbdfb6 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/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; } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index ebf37e0af2..5bb6012313 100644 --- a/crates/project/src/git_store.rs +++ b/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, } +#[derive(Clone, Debug)] +pub struct GitStoreDiff { + diffs_by_work_dir_abs_path: HashMap, +} + +#[derive(Clone, Debug)] +pub struct GitStoreIndex { + indices_by_work_dir_abs_path: HashMap, +} + +#[derive(Default)] +pub struct GitStoreStatus { + #[allow(dead_code)] + statuses_by_work_dir_abs_path: HashMap, +} + pub struct Repository { pub repository_entry: RepositoryEntry, pub merge_message: Option, @@ -651,8 +667,8 @@ impl GitStore { .collect::>(); 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::>(); 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> { + let repositories_by_work_dir_abs_path = self + .repositories + .values() + .map(|repo| { + ( + repo.read(cx) + .repository_entry + .work_directory_abs_path + .clone(), + repo, + ) + }) + .collect::>(); + + 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> { + 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> { + let repositories_by_work_dir_abs_path = self + .repositories + .values() + .map(|repo| { + ( + repo.read(cx) + .repository_entry + .work_directory_abs_path + .clone(), + repo, + ) + }) + .collect::>(); + + 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, cx: &App) -> Task> { + let repositories_by_work_dir_abs_path = self + .repositories + .values() + .map(|repo| { + ( + repo.read(cx) + .repository_entry + .work_directory_abs_path + .clone(), + repo, + ) + }) + .collect::>(); + + 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, + buffer: &Entity, + cx: &App, + ) -> Task> { + 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 { + pub fn cached_status(&self) -> impl '_ + Iterator { self.repository_entry.status() } + pub fn status(&self, index: Option) -> oneshot::Receiver> { + 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, + path: RepoPath, + ) -> oneshot::Receiver> { + 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> { + 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> { + 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> { + 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( diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0d2b6e00c7..ee2d27db8e 100644 --- a/crates/worktree/src/worktree.rs +++ b/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); } } }