Detailed changes
@@ -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
};
@@ -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!()
+ }
}
@@ -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"
@@ -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();
@@ -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;
}
@@ -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(
@@ -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);
}
}
}