Make `GitRepository::status` async and remove cx parameter (#27514)

Antonio Scandurra created

This lays the groundwork for using `status` as part of the new agent
panel.

Release Notes:

- N/A

Change summary

Cargo.lock                                        |   1 
crates/assistant2/src/thread.rs                   |   2 
crates/assistant_eval/src/headless_assistant.rs   |   5 
crates/evals/src/eval.rs                          |   2 
crates/extension_cli/Cargo.toml                   |   1 
crates/extension_cli/src/main.rs                  |   2 
crates/extension_host/src/extension_store_test.rs |   2 
crates/fs/src/fake_git_repo.rs                    |  62 
crates/fs/src/fs.rs                               |  11 
crates/git/src/repository.rs                      | 871 ++++++++--------
crates/git/src/status.rs                          |  46 
crates/gpui/src/platform.rs                       |   5 
crates/project/src/git_store.rs                   |  67 
crates/project/src/project.rs                     |   2 
crates/project/src/project_tests.rs               |   9 
crates/remote_server/src/unix.rs                  |   2 
crates/worktree/src/worktree.rs                   |  18 
crates/worktree/src/worktree_tests.rs             |  30 
crates/zed/src/main.rs                            |   2 
19 files changed, 549 insertions(+), 591 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4749,6 +4749,7 @@ dependencies = [
  "env_logger 0.11.7",
  "extension",
  "fs",
+ "gpui",
  "language",
  "log",
  "reqwest_client",

crates/assistant2/src/thread.rs 🔗

@@ -1429,7 +1429,7 @@ impl Thread {
 
                     // Get diff asynchronously
                     let diff = repo
-                        .diff(git::repository::DiffType::HeadToWorktree, cx.clone())
+                        .diff(git::repository::DiffType::HeadToWorktree)
                         .await
                         .ok();
 

crates/assistant_eval/src/headless_assistant.rs 🔗

@@ -149,7 +149,10 @@ pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
     cx.set_http_client(client.http_client().clone());
 
     let git_binary_path = None;
-    let fs = Arc::new(RealFs::new(git_binary_path));
+    let fs = Arc::new(RealFs::new(
+        git_binary_path,
+        cx.background_executor().clone(),
+    ));
 
     let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
 

crates/evals/src/eval.rs 🔗

@@ -273,7 +273,7 @@ async fn run_evaluation(
     let repos_dir = Path::new(EVAL_REPOS_DIR);
     let db_path = Path::new(EVAL_DB_PATH);
     let api_key = std::env::var("OPENAI_API_KEY").unwrap();
-    let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
+    let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())) as Arc<dyn Fs>;
     let clock = Arc::new(RealSystemClock);
     let client = cx
         .update(|cx| {

crates/extension_cli/Cargo.toml 🔗

@@ -18,6 +18,7 @@ clap = { workspace = true, features = ["derive"] }
 env_logger.workspace = true
 extension.workspace = true
 fs.workspace = true
+gpui.workspace = true
 language.workspace = true
 log.workspace = true
 reqwest_client.workspace = true

crates/extension_cli/src/main.rs 🔗

@@ -34,7 +34,7 @@ async fn main() -> Result<()> {
     env_logger::init();
 
     let args = Args::parse();
-    let fs = Arc::new(RealFs::default());
+    let fs = Arc::new(RealFs::new(None, gpui::background_executor()));
     let engine = wasmtime::Engine::default();
     let mut wasm_store = WasmStore::new(&engine)?;
 

crates/extension_host/src/extension_store_test.rs 🔗

@@ -477,7 +477,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
     let test_extension_id = "test-extension";
     let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
 
-    let fs = Arc::new(RealFs::default());
+    let fs = Arc::new(RealFs::new(None, cx.executor()));
     let extensions_dir = TempTree::new(json!({
         "installed": {},
         "work": {}

crates/fs/src/fake_git_repo.rs 🔗

@@ -62,7 +62,7 @@ impl FakeGitRepository {
             .unwrap()
     }
 
-    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<Result<T>>
+    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
     where
         F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
         T: Send,
@@ -81,7 +81,7 @@ impl FakeGitRepository {
 impl GitRepository for FakeGitRepository {
     fn reload_index(&self) {}
 
-    fn load_index_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
+    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
         async {
             self.with_state_async(false, move |state| {
                 state
@@ -96,7 +96,7 @@ impl GitRepository for FakeGitRepository {
         .boxed()
     }
 
-    fn load_committed_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
+    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
         async {
             self.with_state_async(false, move |state| {
                 state
@@ -116,7 +116,6 @@ impl GitRepository for FakeGitRepository {
         path: RepoPath,
         content: Option<String>,
         _env: HashMap<String, String>,
-        _cx: AsyncApp,
     ) -> BoxFuture<anyhow::Result<()>> {
         self.with_state_async(true, move |state| {
             if let Some(message) = state.simulated_index_write_error_message.clone() {
@@ -142,7 +141,7 @@ impl GitRepository for FakeGitRepository {
         vec![]
     }
 
-    fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
+    fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
         unimplemented!()
     }
 
@@ -172,7 +171,12 @@ impl GitRepository for FakeGitRepository {
         self.path()
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
+    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>> {
+        let status = self.status_blocking(path_prefixes);
+        async move { status }.boxed()
+    }
+
+    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
         let workdir_path = self.dot_git_path.parent().unwrap();
 
         // Load gitignores
@@ -317,26 +321,21 @@ impl GitRepository for FakeGitRepository {
         })
     }
 
-    fn change_branch(&self, name: String, _cx: AsyncApp) -> BoxFuture<Result<()>> {
+    fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
         self.with_state_async(true, |state| {
             state.current_branch_name = Some(name);
             Ok(())
         })
     }
 
-    fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
+    fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
         self.with_state_async(true, move |state| {
             state.branches.insert(name.to_owned());
             Ok(())
         })
     }
 
-    fn blame(
-        &self,
-        path: RepoPath,
-        _content: Rope,
-        _cx: &mut AsyncApp,
-    ) -> BoxFuture<Result<git::blame::Blame>> {
+    fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
         self.with_state_async(false, move |state| {
             state
                 .blames
@@ -350,7 +349,6 @@ impl GitRepository for FakeGitRepository {
         &self,
         _paths: Vec<RepoPath>,
         _env: HashMap<String, String>,
-        _cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -359,7 +357,6 @@ impl GitRepository for FakeGitRepository {
         &self,
         _paths: Vec<RepoPath>,
         _env: HashMap<String, String>,
-        _cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -369,7 +366,6 @@ impl GitRepository for FakeGitRepository {
         _message: gpui::SharedString,
         _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
         _env: HashMap<String, String>,
-        _cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -406,38 +402,23 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 
-    fn get_remotes(
-        &self,
-        _branch: Option<String>,
-        _cx: AsyncApp,
-    ) -> BoxFuture<Result<Vec<Remote>>> {
+    fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
         unimplemented!()
     }
 
-    fn check_for_pushed_commit(
-        &self,
-        _cx: gpui::AsyncApp,
-    ) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
+    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
         future::ready(Ok(Vec::new())).boxed()
     }
 
-    fn diff(
-        &self,
-        _diff: git::repository::DiffType,
-        _cx: gpui::AsyncApp,
-    ) -> BoxFuture<Result<String>> {
+    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
         unimplemented!()
     }
 
-    fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
+    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
         unimplemented!()
     }
 
-    fn restore_checkpoint(
-        &self,
-        _checkpoint: GitRepositoryCheckpoint,
-        _cx: AsyncApp,
-    ) -> BoxFuture<Result<()>> {
+    fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
 
@@ -445,16 +426,11 @@ impl GitRepository for FakeGitRepository {
         &self,
         _left: GitRepositoryCheckpoint,
         _right: GitRepositoryCheckpoint,
-        _cx: AsyncApp,
     ) -> BoxFuture<Result<bool>> {
         unimplemented!()
     }
 
-    fn delete_checkpoint(
-        &self,
-        _checkpoint: GitRepositoryCheckpoint,
-        _cx: AsyncApp,
-    ) -> BoxFuture<Result<()>> {
+    fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
 }

crates/fs/src/fs.rs 🔗

@@ -8,6 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 use ashpd::desktop::trash;
 use gpui::App;
+use gpui::BackgroundExecutor;
 use gpui::Global;
 use gpui::ReadGlobal as _;
 use std::borrow::Cow;
@@ -240,9 +241,9 @@ impl From<MTime> for proto::Timestamp {
     }
 }
 
-#[derive(Default)]
 pub struct RealFs {
     git_binary_path: Option<PathBuf>,
+    executor: BackgroundExecutor,
 }
 
 pub trait FileHandle: Send + Sync + std::fmt::Debug {
@@ -294,8 +295,11 @@ impl FileHandle for std::fs::File {
 pub struct RealWatcher {}
 
 impl RealFs {
-    pub fn new(git_binary_path: Option<PathBuf>) -> Self {
-        Self { git_binary_path }
+    pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
+        Self {
+            git_binary_path,
+            executor,
+        }
     }
 }
 
@@ -754,6 +758,7 @@ impl Fs for RealFs {
         Some(Arc::new(RealGitRepository::new(
             dotgit_path,
             self.git_binary_path.clone(),
+            self.executor.clone(),
         )?))
     }
 

crates/git/src/repository.rs 🔗

@@ -5,13 +5,13 @@ use collections::HashMap;
 use futures::future::BoxFuture;
 use futures::{select_biased, AsyncWriteExt, FutureExt as _};
 use git2::BranchType;
-use gpui::{AppContext, AsyncApp, BackgroundExecutor, SharedString};
+use gpui::{AsyncApp, BackgroundExecutor, SharedString};
 use parking_lot::Mutex;
 use rope::Rope;
 use schemars::JsonSchema;
 use serde::Deserialize;
 use std::borrow::{Borrow, Cow};
-use std::ffi::OsStr;
+use std::ffi::{OsStr, OsString};
 use std::future;
 use std::path::Component;
 use std::process::{ExitStatus, Stdio};
@@ -23,7 +23,7 @@ use std::{
 };
 use sum_tree::MapSeekTarget;
 use thiserror::Error;
-use util::command::new_smol_command;
+use util::command::{new_smol_command, new_std_command};
 use util::ResultExt;
 use uuid::Uuid;
 
@@ -161,19 +161,18 @@ 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, cx: AsyncApp) -> BoxFuture<Option<String>>;
+    fn load_index_text(&self, 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.
     ///
     /// Also returns `None` for symlinks.
-    fn load_committed_text(&self, path: RepoPath, cx: AsyncApp) -> BoxFuture<Option<String>>;
+    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
 
     fn set_index_text(
         &self,
         path: RepoPath,
         content: Option<String>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<anyhow::Result<()>>;
 
     /// Returns the URL of the remote with the given name.
@@ -184,13 +183,13 @@ pub trait GitRepository: Send + Sync {
 
     fn merge_head_shas(&self) -> Vec<String>;
 
-    // Note: this method blocks the current thread!
-    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
+    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'static, Result<GitStatus>>;
+    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
 
-    fn change_branch(&self, _: String, _: AsyncApp) -> BoxFuture<Result<()>>;
-    fn create_branch(&self, _: String, _: AsyncApp) -> BoxFuture<Result<()>>;
+    fn change_branch(&self, name: String) -> BoxFuture<Result<()>>;
+    fn create_branch(&self, name: String) -> BoxFuture<Result<()>>;
 
     fn reset(
         &self,
@@ -206,14 +205,9 @@ pub trait GitRepository: Send + Sync {
         env: HashMap<String, String>,
     ) -> BoxFuture<Result<()>>;
 
-    fn show(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDetails>>;
+    fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
 
-    fn blame(
-        &self,
-        path: RepoPath,
-        content: Rope,
-        cx: &mut AsyncApp,
-    ) -> BoxFuture<Result<crate::blame::Blame>>;
+    fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
 
     /// Returns the absolute path to the repository. For worktrees, this will be the path to the
     /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
@@ -234,7 +228,6 @@ pub trait GitRepository: Send + Sync {
         &self,
         paths: Vec<RepoPath>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>>;
     /// Updates the index to match HEAD at the given paths.
     ///
@@ -243,7 +236,6 @@ pub trait GitRepository: Send + Sync {
         &self,
         paths: Vec<RepoPath>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>>;
 
     fn commit(
@@ -251,7 +243,6 @@ pub trait GitRepository: Send + Sync {
         message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>>;
 
     fn push(
@@ -261,6 +252,8 @@ pub trait GitRepository: Send + Sync {
         options: Option<PushOptions>,
         askpass: AskPassSession,
         env: HashMap<String, String>,
+        // This method takes an AsyncApp to ensure it's invoked on the main thread,
+        // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>>;
 
@@ -270,6 +263,8 @@ pub trait GitRepository: Send + Sync {
         upstream_name: String,
         askpass: AskPassSession,
         env: HashMap<String, String>,
+        // This method takes an AsyncApp to ensure it's invoked on the main thread,
+        // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>>;
 
@@ -277,45 +272,34 @@ pub trait GitRepository: Send + Sync {
         &self,
         askpass: AskPassSession,
         env: HashMap<String, String>,
+        // This method takes an AsyncApp to ensure it's invoked on the main thread,
+        // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>>;
 
-    fn get_remotes(
-        &self,
-        branch_name: Option<String>,
-        cx: AsyncApp,
-    ) -> BoxFuture<Result<Vec<Remote>>>;
+    fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>;
 
     /// returns a list of remote branches that contain HEAD
-    fn check_for_pushed_commit(&self, cx: AsyncApp) -> BoxFuture<Result<Vec<SharedString>>>;
+    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>;
 
     /// Run git diff
-    fn diff(&self, diff: DiffType, cx: AsyncApp) -> BoxFuture<Result<String>>;
+    fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
 
     /// Creates a checkpoint for the repository.
-    fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>>;
+    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>>;
 
     /// Resets to a previously-created checkpoint.
-    fn restore_checkpoint(
-        &self,
-        checkpoint: GitRepositoryCheckpoint,
-        cx: AsyncApp,
-    ) -> BoxFuture<Result<()>>;
+    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
 
     /// Compares two checkpoints, returning true if they are equal
     fn compare_checkpoints(
         &self,
         left: GitRepositoryCheckpoint,
         right: GitRepositoryCheckpoint,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<bool>>;
 
     /// Deletes a previously-created checkpoint.
-    fn delete_checkpoint(
-        &self,
-        checkpoint: GitRepositoryCheckpoint,
-        cx: AsyncApp,
-    ) -> BoxFuture<Result<()>>;
+    fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
 }
 
 pub enum DiffType {
@@ -338,15 +322,21 @@ impl std::fmt::Debug for dyn GitRepository {
 pub struct RealGitRepository {
     pub repository: Arc<Mutex<git2::Repository>>,
     pub git_binary_path: PathBuf,
+    executor: BackgroundExecutor,
 }
 
 impl RealGitRepository {
-    pub fn new(dotgit_path: &Path, git_binary_path: Option<PathBuf>) -> Option<Self> {
+    pub fn new(
+        dotgit_path: &Path,
+        git_binary_path: Option<PathBuf>,
+        executor: BackgroundExecutor,
+    ) -> Option<Self> {
         let workdir_root = dotgit_path.parent()?;
         let repository = git2::Repository::open(workdir_root).log_err()?;
         Some(Self {
             repository: Arc::new(Mutex::new(repository)),
             git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
+            executor,
         })
     }
 
@@ -386,29 +376,30 @@ impl GitRepository for RealGitRepository {
         repo.commondir().into()
     }
 
-    fn show(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
+    fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
         let repo = self.repository.clone();
-        cx.background_spawn(async move {
-            let repo = repo.lock();
-            let Ok(commit) = repo.revparse_single(&commit)?.into_commit() else {
-                anyhow::bail!("{} is not a commit", commit);
-            };
-            let details = CommitDetails {
-                sha: commit.id().to_string().into(),
-                message: String::from_utf8_lossy(commit.message_raw_bytes())
-                    .to_string()
-                    .into(),
-                commit_timestamp: commit.time().seconds(),
-                committer_email: String::from_utf8_lossy(commit.committer().email_bytes())
-                    .to_string()
-                    .into(),
-                committer_name: String::from_utf8_lossy(commit.committer().name_bytes())
-                    .to_string()
-                    .into(),
-            };
-            Ok(details)
-        })
-        .boxed()
+        self.executor
+            .spawn(async move {
+                let repo = repo.lock();
+                let Ok(commit) = repo.revparse_single(&commit)?.into_commit() else {
+                    anyhow::bail!("{} is not a commit", commit);
+                };
+                let details = CommitDetails {
+                    sha: commit.id().to_string().into(),
+                    message: String::from_utf8_lossy(commit.message_raw_bytes())
+                        .to_string()
+                        .into(),
+                    commit_timestamp: commit.time().seconds(),
+                    committer_email: String::from_utf8_lossy(commit.committer().email_bytes())
+                        .to_string()
+                        .into(),
+                    committer_name: String::from_utf8_lossy(commit.committer().name_bytes())
+                        .to_string()
+                        .into(),
+                };
+                Ok(details)
+            })
+            .boxed()
     }
 
     fn reset(
@@ -473,48 +464,50 @@ impl GitRepository for RealGitRepository {
         .boxed()
     }
 
-    fn load_index_text(&self, path: RepoPath, cx: AsyncApp) -> BoxFuture<Option<String>> {
+    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
         let repo = self.repository.clone();
-        cx.background_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)?;
-
-                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 logic(&repo.lock(), &path) {
-                Ok(value) => return value,
-                Err(err) => log::error!("Error loading index text: {:?}", err),
-            }
-            None
-        })
-        .boxed()
+        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)?;
+
+                    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 logic(&repo.lock(), &path) {
+                    Ok(value) => return value,
+                    Err(err) => log::error!("Error loading index text: {:?}", err),
+                }
+                None
+            })
+            .boxed()
     }
 
-    fn load_committed_text(&self, path: RepoPath, cx: AsyncApp) -> BoxFuture<Option<String>> {
+    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
         let repo = self.repository.clone();
-        cx.background_spawn(async move {
-            let repo = repo.lock();
-            let head = repo.head().ok()?.peel_to_tree().log_err()?;
-            let entry = head.get_path(&path).ok()?;
-            if entry.filemode() == i32::from(git2::FileMode::Link) {
-                return None;
-            }
-            let content = repo.find_blob(entry.id()).log_err()?.content().to_owned();
-            let content = String::from_utf8(content).log_err()?;
-            Some(content)
-        })
-        .boxed()
+        self.executor
+            .spawn(async move {
+                let repo = repo.lock();
+                let head = repo.head().ok()?.peel_to_tree().log_err()?;
+                let entry = head.get_path(&path).ok()?;
+                if entry.filemode() == i32::from(git2::FileMode::Link) {
+                    return None;
+                }
+                let content = repo.find_blob(entry.id()).log_err()?.content().to_owned();
+                let content = String::from_utf8(content).log_err()?;
+                Some(content)
+            })
+            .boxed()
     }
 
     fn set_index_text(
@@ -522,65 +515,65 @@ impl GitRepository for RealGitRepository {
         path: RepoPath,
         content: Option<String>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<anyhow::Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        cx.background_spawn(async move {
-            let working_directory = working_directory?;
-            if let Some(content) = content {
-                let mut child = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory)
-                    .envs(&env)
-                    .args(["hash-object", "-w", "--stdin"])
-                    .stdin(Stdio::piped())
-                    .stdout(Stdio::piped())
-                    .spawn()?;
-                child
-                    .stdin
-                    .take()
-                    .unwrap()
-                    .write_all(content.as_bytes())
-                    .await?;
-                let output = child.output().await?.stdout;
-                let sha = String::from_utf8(output)?;
-
-                log::debug!("indexing SHA: {sha}, path {path:?}");
-
-                let output = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory)
-                    .envs(env)
-                    .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
-                    .arg(path.to_unix_style())
-                    .output()
-                    .await?;
-
-                if !output.status.success() {
-                    return Err(anyhow!(
-                        "Failed to stage:\n{}",
-                        String::from_utf8_lossy(&output.stderr)
-                    ));
-                }
-            } else {
-                let output = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory)
-                    .envs(env)
-                    .args(["update-index", "--force-remove"])
-                    .arg(path.to_unix_style())
-                    .output()
-                    .await?;
-
-                if !output.status.success() {
-                    return Err(anyhow!(
-                        "Failed to unstage:\n{}",
-                        String::from_utf8_lossy(&output.stderr)
-                    ));
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                if let Some(content) = content {
+                    let mut child = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory)
+                        .envs(&env)
+                        .args(["hash-object", "-w", "--stdin"])
+                        .stdin(Stdio::piped())
+                        .stdout(Stdio::piped())
+                        .spawn()?;
+                    child
+                        .stdin
+                        .take()
+                        .unwrap()
+                        .write_all(content.as_bytes())
+                        .await?;
+                    let output = child.output().await?.stdout;
+                    let sha = String::from_utf8(output)?;
+
+                    log::debug!("indexing SHA: {sha}, path {path:?}");
+
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory)
+                        .envs(env)
+                        .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
+                        .arg(path.to_unix_style())
+                        .output()
+                        .await?;
+
+                    if !output.status.success() {
+                        return Err(anyhow!(
+                            "Failed to stage:\n{}",
+                            String::from_utf8_lossy(&output.stderr)
+                        ));
+                    }
+                } else {
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory)
+                        .envs(env)
+                        .args(["update-index", "--force-remove"])
+                        .arg(path.to_unix_style())
+                        .output()
+                        .await?;
+
+                    if !output.status.success() {
+                        return Err(anyhow!(
+                            "Failed to unstage:\n{}",
+                            String::from_utf8_lossy(&output.stderr)
+                        ));
+                    }
                 }
-            }
 
-            Ok(())
-        })
-        .boxed()
+                Ok(())
+            })
+            .boxed()
     }
 
     fn remote_url(&self, name: &str) -> Option<String> {
@@ -614,14 +607,32 @@ impl GitRepository for RealGitRepository {
         shas
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
-        let working_directory = self
-            .repository
-            .lock()
-            .workdir()
-            .context("failed to read git work directory")?
-            .to_path_buf();
-        GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
+    fn status(&self, 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);
+        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()
+            })
+            .boxed()
+    }
+
+    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
+        let output = new_std_command(&self.git_binary_path)
+            .current_dir(self.working_directory()?)
+            .args(git_status_args(path_prefixes))
+            .output()?;
+        if output.status.success() {
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            stdout.parse()
+        } else {
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            Err(anyhow!("git status failed: {}", stderr))
+        }
     }
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
@@ -685,146 +696,145 @@ impl GitRepository for RealGitRepository {
         .boxed()
     }
 
-    fn change_branch(&self, name: String, cx: AsyncApp) -> BoxFuture<Result<()>> {
+    fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
         let repo = self.repository.clone();
-        cx.background_spawn(async move {
-            let repo = repo.lock();
-            let revision = repo.find_branch(&name, BranchType::Local)?;
-            let revision = revision.get();
-            let as_tree = revision.peel_to_tree()?;
-            repo.checkout_tree(as_tree.as_object(), None)?;
-            repo.set_head(
-                revision
-                    .name()
-                    .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
-            )?;
-            Ok(())
-        })
-        .boxed()
+        self.executor
+            .spawn(async move {
+                let repo = repo.lock();
+                let revision = repo.find_branch(&name, BranchType::Local)?;
+                let revision = revision.get();
+                let as_tree = revision.peel_to_tree()?;
+                repo.checkout_tree(as_tree.as_object(), None)?;
+                repo.set_head(
+                    revision
+                        .name()
+                        .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
+                )?;
+                Ok(())
+            })
+            .boxed()
     }
 
-    fn create_branch(&self, name: String, cx: AsyncApp) -> BoxFuture<Result<()>> {
+    fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
         let repo = self.repository.clone();
-        cx.background_spawn(async move {
-            let repo = repo.lock();
-            let current_commit = repo.head()?.peel_to_commit()?;
-            repo.branch(&name, &current_commit, false)?;
-            Ok(())
-        })
-        .boxed()
+        self.executor
+            .spawn(async move {
+                let repo = repo.lock();
+                let current_commit = repo.head()?.peel_to_commit()?;
+                repo.branch(&name, &current_commit, false)?;
+                Ok(())
+            })
+            .boxed()
     }
 
-    fn blame(
-        &self,
-        path: RepoPath,
-        content: Rope,
-        cx: &mut AsyncApp,
-    ) -> BoxFuture<Result<crate::blame::Blame>> {
+    fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
 
         const REMOTE_NAME: &str = "origin";
         let remote_url = self.remote_url(REMOTE_NAME);
 
-        cx.background_spawn(async move {
-            crate::blame::Blame::for_path(
-                &git_binary_path,
-                &working_directory?,
-                &path,
-                &content,
-                remote_url,
-            )
-            .await
-        })
-        .boxed()
+        self.executor
+            .spawn(async move {
+                crate::blame::Blame::for_path(
+                    &git_binary_path,
+                    &working_directory?,
+                    &path,
+                    &content,
+                    remote_url,
+                )
+                .await
+            })
+            .boxed()
     }
 
-    fn diff(&self, diff: DiffType, cx: AsyncApp) -> BoxFuture<Result<String>> {
+    fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        cx.background_spawn(async move {
-            let args = match diff {
-                DiffType::HeadToIndex => Some("--staged"),
-                DiffType::HeadToWorktree => None,
-            };
+        self.executor
+            .spawn(async move {
+                let args = match diff {
+                    DiffType::HeadToIndex => Some("--staged"),
+                    DiffType::HeadToWorktree => None,
+                };
 
-            let output = new_smol_command(&git_binary_path)
-                .current_dir(&working_directory?)
-                .args(["diff"])
-                .args(args)
-                .output()
-                .await?;
+                let output = new_smol_command(&git_binary_path)
+                    .current_dir(&working_directory?)
+                    .args(["diff"])
+                    .args(args)
+                    .output()
+                    .await?;
 
-            if !output.status.success() {
-                return Err(anyhow!(
-                    "Failed to run git diff:\n{}",
-                    String::from_utf8_lossy(&output.stderr)
-                ));
-            }
-            Ok(String::from_utf8_lossy(&output.stdout).to_string())
-        })
-        .boxed()
+                if !output.status.success() {
+                    return Err(anyhow!(
+                        "Failed to run git diff:\n{}",
+                        String::from_utf8_lossy(&output.stderr)
+                    ));
+                }
+                Ok(String::from_utf8_lossy(&output.stdout).to_string())
+            })
+            .boxed()
     }
 
     fn stage_paths(
         &self,
         paths: Vec<RepoPath>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        cx.background_spawn(async move {
-            if !paths.is_empty() {
-                let output = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory?)
-                    .envs(env)
-                    .args(["update-index", "--add", "--remove", "--"])
-                    .args(paths.iter().map(|p| p.to_unix_style()))
-                    .output()
-                    .await?;
-
-                if !output.status.success() {
-                    return Err(anyhow!(
-                        "Failed to stage paths:\n{}",
-                        String::from_utf8_lossy(&output.stderr)
-                    ));
+        self.executor
+            .spawn(async move {
+                if !paths.is_empty() {
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory?)
+                        .envs(env)
+                        .args(["update-index", "--add", "--remove", "--"])
+                        .args(paths.iter().map(|p| p.to_unix_style()))
+                        .output()
+                        .await?;
+
+                    if !output.status.success() {
+                        return Err(anyhow!(
+                            "Failed to stage paths:\n{}",
+                            String::from_utf8_lossy(&output.stderr)
+                        ));
+                    }
                 }
-            }
-            Ok(())
-        })
-        .boxed()
+                Ok(())
+            })
+            .boxed()
     }
 
     fn unstage_paths(
         &self,
         paths: Vec<RepoPath>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
 
-        cx.background_spawn(async move {
-            if !paths.is_empty() {
-                let output = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory?)
-                    .envs(env)
-                    .args(["reset", "--quiet", "--"])
-                    .args(paths.iter().map(|p| p.as_ref()))
-                    .output()
-                    .await?;
-
-                if !output.status.success() {
-                    return Err(anyhow!(
-                        "Failed to unstage:\n{}",
-                        String::from_utf8_lossy(&output.stderr)
-                    ));
+        self.executor
+            .spawn(async move {
+                if !paths.is_empty() {
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory?)
+                        .envs(env)
+                        .args(["reset", "--quiet", "--"])
+                        .args(paths.iter().map(|p| p.as_ref()))
+                        .output()
+                        .await?;
+
+                    if !output.status.success() {
+                        return Err(anyhow!(
+                            "Failed to unstage:\n{}",
+                            String::from_utf8_lossy(&output.stderr)
+                        ));
+                    }
                 }
-            }
-            Ok(())
-        })
-        .boxed()
+                Ok(())
+            })
+            .boxed()
     }
 
     fn commit(
@@ -832,32 +842,32 @@ impl GitRepository for RealGitRepository {
         message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
         env: HashMap<String, String>,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
-        cx.background_spawn(async move {
-            let mut cmd = new_smol_command("git");
-            cmd.current_dir(&working_directory?)
-                .envs(env)
-                .args(["commit", "--quiet", "-m"])
-                .arg(&message.to_string())
-                .arg("--cleanup=strip");
+        self.executor
+            .spawn(async move {
+                let mut cmd = new_smol_command("git");
+                cmd.current_dir(&working_directory?)
+                    .envs(env)
+                    .args(["commit", "--quiet", "-m"])
+                    .arg(&message.to_string())
+                    .arg("--cleanup=strip");
 
-            if let Some((name, email)) = name_and_email {
-                cmd.arg("--author").arg(&format!("{name} <{email}>"));
-            }
+                if let Some((name, email)) = name_and_email {
+                    cmd.arg("--author").arg(&format!("{name} <{email}>"));
+                }
 
-            let output = cmd.output().await?;
+                let output = cmd.output().await?;
 
-            if !output.status.success() {
-                return Err(anyhow!(
-                    "Failed to commit:\n{}",
-                    String::from_utf8_lossy(&output.stderr)
-                ));
-            }
-            Ok(())
-        })
-        .boxed()
+                if !output.status.success() {
+                    return Err(anyhow!(
+                        "Failed to commit:\n{}",
+                        String::from_utf8_lossy(&output.stderr)
+                    ));
+                }
+                Ok(())
+            })
+            .boxed()
     }
 
     fn push(
@@ -867,8 +877,6 @@ impl GitRepository for RealGitRepository {
         options: Option<PushOptions>,
         ask_pass: AskPassSession,
         env: HashMap<String, String>,
-        // note: git push *must* be started on the main thread for
-        // git-credentials manager to work (hence taking an AsyncApp)
         _cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>> {
         let working_directory = self.working_directory();
@@ -954,197 +962,194 @@ impl GitRepository for RealGitRepository {
         .boxed()
     }
 
-    fn get_remotes(
-        &self,
-        branch_name: Option<String>,
-        cx: AsyncApp,
-    ) -> BoxFuture<Result<Vec<Remote>>> {
+    fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        cx.background_spawn(async move {
-            let working_directory = working_directory?;
-            if let Some(branch_name) = branch_name {
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                if let Some(branch_name) = branch_name {
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory)
+                        .args(["config", "--get"])
+                        .arg(format!("branch.{}.remote", branch_name))
+                        .output()
+                        .await?;
+
+                    if output.status.success() {
+                        let remote_name = String::from_utf8_lossy(&output.stdout);
+
+                        return Ok(vec![Remote {
+                            name: remote_name.trim().to_string().into(),
+                        }]);
+                    }
+                }
+
                 let output = new_smol_command(&git_binary_path)
                     .current_dir(&working_directory)
-                    .args(["config", "--get"])
-                    .arg(format!("branch.{}.remote", branch_name))
+                    .args(["remote"])
                     .output()
                     .await?;
 
                 if output.status.success() {
-                    let remote_name = String::from_utf8_lossy(&output.stdout);
-
-                    return Ok(vec![Remote {
-                        name: remote_name.trim().to_string().into(),
-                    }]);
+                    let remote_names = String::from_utf8_lossy(&output.stdout)
+                        .split('\n')
+                        .filter(|name| !name.is_empty())
+                        .map(|name| Remote {
+                            name: name.trim().to_string().into(),
+                        })
+                        .collect();
+
+                    return Ok(remote_names);
+                } else {
+                    return Err(anyhow!(
+                        "Failed to get remotes:\n{}",
+                        String::from_utf8_lossy(&output.stderr)
+                    ));
                 }
-            }
-
-            let output = new_smol_command(&git_binary_path)
-                .current_dir(&working_directory)
-                .args(["remote"])
-                .output()
-                .await?;
-
-            if output.status.success() {
-                let remote_names = String::from_utf8_lossy(&output.stdout)
-                    .split('\n')
-                    .filter(|name| !name.is_empty())
-                    .map(|name| Remote {
-                        name: name.trim().to_string().into(),
-                    })
-                    .collect();
-
-                return Ok(remote_names);
-            } else {
-                return Err(anyhow!(
-                    "Failed to get remotes:\n{}",
-                    String::from_utf8_lossy(&output.stderr)
-                ));
-            }
-        })
-        .boxed()
+            })
+            .boxed()
     }
 
-    fn check_for_pushed_commit(&self, cx: AsyncApp) -> BoxFuture<Result<Vec<SharedString>>> {
+    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        cx.background_spawn(async move {
-            let working_directory = working_directory?;
-            let git_cmd = async |args: &[&str]| -> Result<String> {
-                let output = new_smol_command(&git_binary_path)
-                    .current_dir(&working_directory)
-                    .args(args)
-                    .output()
-                    .await?;
-                if output.status.success() {
-                    Ok(String::from_utf8(output.stdout)?)
-                } else {
-                    Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string()))
-                }
-            };
+        self.executor
+            .spawn(async move {
+                let working_directory = working_directory?;
+                let git_cmd = async |args: &[&str]| -> Result<String> {
+                    let output = new_smol_command(&git_binary_path)
+                        .current_dir(&working_directory)
+                        .args(args)
+                        .output()
+                        .await?;
+                    if output.status.success() {
+                        Ok(String::from_utf8(output.stdout)?)
+                    } else {
+                        Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string()))
+                    }
+                };
 
-            let head = git_cmd(&["rev-parse", "HEAD"])
-                .await
-                .context("Failed to get HEAD")?
-                .trim()
-                .to_owned();
-
-            let mut remote_branches = vec![];
-            let mut add_if_matching = async |remote_head: &str| {
-                if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await {
-                    if merge_base.trim() == head {
-                        if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
-                            remote_branches.push(s.to_owned().into());
+                let head = git_cmd(&["rev-parse", "HEAD"])
+                    .await
+                    .context("Failed to get HEAD")?
+                    .trim()
+                    .to_owned();
+
+                let mut remote_branches = vec![];
+                let mut add_if_matching = async |remote_head: &str| {
+                    if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await {
+                        if merge_base.trim() == head {
+                            if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
+                                remote_branches.push(s.to_owned().into());
+                            }
                         }
                     }
+                };
+
+                // check the main branch of each remote
+                let remotes = git_cmd(&["remote"])
+                    .await
+                    .context("Failed to get remotes")?;
+                for remote in remotes.lines() {
+                    if let Ok(remote_head) =
+                        git_cmd(&["symbolic-ref", &format!("refs/remotes/{remote}/HEAD")]).await
+                    {
+                        add_if_matching(remote_head.trim()).await;
+                    }
                 }
-            };
 
-            // check the main branch of each remote
-            let remotes = git_cmd(&["remote"])
-                .await
-                .context("Failed to get remotes")?;
-            for remote in remotes.lines() {
+                // ... and the remote branch that the checked-out one is tracking
                 if let Ok(remote_head) =
-                    git_cmd(&["symbolic-ref", &format!("refs/remotes/{remote}/HEAD")]).await
+                    git_cmd(&["rev-parse", "--symbolic-full-name", "@{u}"]).await
                 {
                     add_if_matching(remote_head.trim()).await;
                 }
-            }
 
-            // ... and the remote branch that the checked-out one is tracking
-            if let Ok(remote_head) = git_cmd(&["rev-parse", "--symbolic-full-name", "@{u}"]).await {
-                add_if_matching(remote_head.trim()).await;
-            }
-
-            Ok(remote_branches)
-        })
-        .boxed()
+                Ok(remote_branches)
+            })
+            .boxed()
     }
 
-    fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
+    fn checkpoint(&self) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
-        let executor = cx.background_executor().clone();
-        cx.background_spawn(async move {
-            let working_directory = working_directory?;
-            let mut git = GitBinary::new(git_binary_path, working_directory, executor)
-                .envs(checkpoint_author_envs());
-            git.with_temp_index(async |git| {
-                let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok();
-                git.run(&["add", "--all"]).await?;
-                let tree = git.run(&["write-tree"]).await?;
-                let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
-                    git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
-                        .await?
-                } else {
-                    git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
-                };
-                let ref_name = format!("refs/zed/{}", Uuid::new_v4());
-                git.run(&["update-ref", &ref_name, &checkpoint_sha]).await?;
-
-                Ok(GitRepositoryCheckpoint {
-                    ref_name,
-                    head_sha: if let Some(head_sha) = head_sha {
-                        Some(head_sha.parse()?)
+        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)
+                    .envs(checkpoint_author_envs());
+                git.with_temp_index(async |git| {
+                    let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok();
+                    git.run(&["add", "--all"]).await?;
+                    let tree = git.run(&["write-tree"]).await?;
+                    let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
+                        git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
+                            .await?
                     } else {
-                        None
-                    },
-                    commit_sha: checkpoint_sha.parse()?,
+                        git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
+                    };
+                    let ref_name = format!("refs/zed/{}", Uuid::new_v4());
+                    git.run(&["update-ref", &ref_name, &checkpoint_sha]).await?;
+
+                    Ok(GitRepositoryCheckpoint {
+                        ref_name,
+                        head_sha: if let Some(head_sha) = head_sha {
+                            Some(head_sha.parse()?)
+                        } else {
+                            None
+                        },
+                        commit_sha: checkpoint_sha.parse()?,
+                    })
                 })
+                .await
             })
-            .await
-        })
-        .boxed()
+            .boxed()
     }
 
-    fn restore_checkpoint(
-        &self,
-        checkpoint: GitRepositoryCheckpoint,
-        cx: AsyncApp,
-    ) -> BoxFuture<Result<()>> {
+    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
 
-        let executor = cx.background_executor().clone();
-        cx.background_spawn(async move {
-            let working_directory = working_directory?;
+        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.run(&[
-                "restore",
-                "--source",
-                &checkpoint.commit_sha.to_string(),
-                "--worktree",
-                ".",
-            ])
-            .await?;
-
-            git.with_temp_index(async move |git| {
-                git.run(&["read-tree", &checkpoint.commit_sha.to_string()])
-                    .await?;
-                git.run(&["clean", "-d", "--force"]).await
-            })
-            .await?;
+                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
+                git.run(&[
+                    "restore",
+                    "--source",
+                    &checkpoint.commit_sha.to_string(),
+                    "--worktree",
+                    ".",
+                ])
+                .await?;
 
-            if let Some(head_sha) = checkpoint.head_sha {
-                git.run(&["reset", "--mixed", &head_sha.to_string()])
-                    .await?;
-            } else {
-                git.run(&["update-ref", "-d", "HEAD"]).await?;
-            }
+                git.with_temp_index(async move |git| {
+                    git.run(&["read-tree", &checkpoint.commit_sha.to_string()])
+                        .await?;
+                    git.run(&["clean", "-d", "--force"]).await
+                })
+                .await?;
 
-            Ok(())
-        })
-        .boxed()
+                if let Some(head_sha) = checkpoint.head_sha {
+                    git.run(&["reset", "--mixed", &head_sha.to_string()])
+                        .await?;
+                } else {
+                    git.run(&["update-ref", "-d", "HEAD"]).await?;
+                }
+
+                Ok(())
+            })
+            .boxed()
     }
 
     fn compare_checkpoints(
         &self,
         left: GitRepositoryCheckpoint,
         right: GitRepositoryCheckpoint,
-        cx: AsyncApp,
     ) -> BoxFuture<Result<bool>> {
         if left.head_sha != right.head_sha {
             return future::ready(Ok(false)).boxed();

crates/git/src/status.rs 🔗

@@ -1,7 +1,7 @@
 use crate::repository::RepoPath;
 use anyhow::{anyhow, Result};
 use serde::{Deserialize, Serialize};
-use std::{path::Path, process::Stdio, sync::Arc};
+use std::{path::Path, str::FromStr, sync::Arc};
 use util::ResultExt;
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -443,45 +443,11 @@ pub struct GitStatus {
     pub entries: Arc<[(RepoPath, FileStatus)]>,
 }
 
-impl GitStatus {
-    pub(crate) fn new(
-        git_binary: &Path,
-        working_directory: &Path,
-        path_prefixes: &[RepoPath],
-    ) -> Result<Self> {
-        let child = util::command::new_std_command(git_binary)
-            .current_dir(working_directory)
-            .args([
-                "--no-optional-locks",
-                "status",
-                "--porcelain=v1",
-                "--untracked-files=all",
-                "--no-renames",
-                "-z",
-            ])
-            .args(path_prefixes.iter().map(|path_prefix| {
-                if path_prefix.0.as_ref() == Path::new("") {
-                    Path::new(".")
-                } else {
-                    path_prefix
-                }
-            }))
-            .stdin(Stdio::null())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped())
-            .spawn()
-            .map_err(|e| anyhow!("Failed to start git status process: {e}"))?;
-
-        let output = child
-            .wait_with_output()
-            .map_err(|e| anyhow!("Failed to read git status output: {e}"))?;
-
-        if !output.status.success() {
-            let stderr = String::from_utf8_lossy(&output.stderr);
-            return Err(anyhow!("git status process failed: {stderr}"));
-        }
-        let stdout = String::from_utf8_lossy(&output.stdout);
-        let mut entries = stdout
+impl FromStr for GitStatus {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        let mut entries = s
             .split('\0')
             .filter_map(|entry| {
                 let sep = entry.get(2..3)?;

crates/gpui/src/platform.rs 🔗

@@ -74,6 +74,11 @@ pub(crate) use windows::*;
 #[cfg(any(test, feature = "test-support"))]
 pub use test::TestScreenCaptureSource;
 
+/// Returns a background executor for the current platform.
+pub fn background_executor() -> BackgroundExecutor {
+    current_platform(true).background_executor()
+}
+
 #[cfg(target_os = "macos")]
 pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
     Rc::new(MacPlatform::new(headless))

crates/project/src/git_store.rs 🔗

@@ -773,11 +773,11 @@ impl GitStore {
                     anyhow::Ok(Some((repo, relative_path, content)))
                 });
 
-                cx.spawn(async move |cx| {
+                cx.spawn(async move |_cx| {
                     let Some((repo, relative_path, content)) = blame_params? else {
                         return Ok(None);
                     };
-                    repo.blame(relative_path.clone(), content, cx)
+                    repo.blame(relative_path.clone(), content)
                         .await
                         .with_context(|| format!("Failed to blame {:?}", relative_path.0))
                         .map(Some)
@@ -1282,16 +1282,13 @@ impl GitStore {
                         let index_text = if current_index_text.is_some() {
                             local_repo
                                 .repo()
-                                .load_index_text(relative_path.clone(), cx.clone())
+                                .load_index_text(relative_path.clone())
                                 .await
                         } else {
                             None
                         };
                         let head_text = if current_head_text.is_some() {
-                            local_repo
-                                .repo()
-                                .load_committed_text(relative_path, cx.clone())
-                                .await
+                            local_repo.repo().load_committed_text(relative_path).await
                         } else {
                             None
                         };
@@ -2832,9 +2829,9 @@ impl Repository {
     }
 
     pub fn show(&self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
-        self.send_job(|git_repo, cx| async move {
+        self.send_job(|git_repo, _cx| async move {
             match git_repo {
-                RepositoryState::Local(git_repository) => git_repository.show(commit, cx).await,
+                RepositoryState::Local(git_repository) => git_repository.show(commit).await,
                 RepositoryState::Remote {
                     project_id,
                     client,
@@ -2901,9 +2898,9 @@ impl Repository {
             let env = env.await;
 
             this.update(cx, |this, _| {
-                this.send_job(|git_repo, cx| async move {
+                this.send_job(|git_repo, _cx| async move {
                     match git_repo {
-                        RepositoryState::Local(repo) => repo.stage_paths(entries, env, cx).await,
+                        RepositoryState::Local(repo) => repo.stage_paths(entries, env).await,
                         RepositoryState::Remote {
                             project_id,
                             client,
@@ -2969,9 +2966,9 @@ impl Repository {
             let env = env.await;
 
             this.update(cx, |this, _| {
-                this.send_job(|git_repo, cx| async move {
+                this.send_job(|git_repo, _cx| async move {
                     match git_repo {
-                        RepositoryState::Local(repo) => repo.unstage_paths(entries, env, cx).await,
+                        RepositoryState::Local(repo) => repo.unstage_paths(entries, env).await,
                         RepositoryState::Remote {
                             project_id,
                             client,
@@ -3055,11 +3052,11 @@ impl Repository {
         cx: &mut App,
     ) -> oneshot::Receiver<Result<()>> {
         let env = self.worktree_environment(cx);
-        self.send_job(|git_repo, cx| async move {
+        self.send_job(|git_repo, _cx| async move {
             match git_repo {
                 RepositoryState::Local(repo) => {
                     let env = env.await;
-                    repo.commit(message, name_and_email, env, cx).await
+                    repo.commit(message, name_and_email, env).await
                 }
                 RepositoryState::Remote {
                     project_id,
@@ -3254,10 +3251,10 @@ impl Repository {
 
         self.send_keyed_job(
             Some(GitJobKey::WriteIndex(path.clone())),
-            |git_repo, cx| async {
+            |git_repo, _cx| async {
                 match git_repo {
                     RepositoryState::Local(repo) => {
-                        repo.set_index_text(path, content, env.await, cx).await
+                        repo.set_index_text(path, content, env.await).await
                     }
                     RepositoryState::Remote {
                         project_id,
@@ -3283,10 +3280,10 @@ impl Repository {
         &self,
         branch_name: Option<String>,
     ) -> oneshot::Receiver<Result<Vec<Remote>>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.get_remotes(branch_name, cx).await
+                    git_repository.get_remotes(branch_name).await
                 }
                 RepositoryState::Remote {
                     project_id,
@@ -3352,9 +3349,9 @@ impl Repository {
     }
 
     pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
-                RepositoryState::Local(git_repository) => git_repository.diff(diff_type, cx).await,
+                RepositoryState::Local(git_repository) => git_repository.diff(diff_type).await,
                 RepositoryState::Remote {
                     project_id,
                     client,
@@ -3383,10 +3380,10 @@ impl Repository {
     }
 
     pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.create_branch(branch_name, cx).await
+                    git_repository.create_branch(branch_name).await
                 }
                 RepositoryState::Remote {
                     project_id,
@@ -3408,10 +3405,10 @@ impl Repository {
     }
 
     pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.change_branch(branch_name, cx).await
+                    git_repository.change_branch(branch_name).await
                 }
                 RepositoryState::Remote {
                     project_id,
@@ -3433,10 +3430,10 @@ impl Repository {
     }
 
     pub fn check_for_pushed_commits(&self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.check_for_pushed_commit(cx).await
+                    git_repository.check_for_pushed_commit().await
                 }
                 RepositoryState::Remote {
                     project_id,
@@ -3459,9 +3456,9 @@ impl Repository {
     }
 
     pub fn checkpoint(&self) -> oneshot::Receiver<Result<GitRepositoryCheckpoint>> {
-        self.send_job(|repo, cx| async move {
+        self.send_job(|repo, _cx| async move {
             match repo {
-                RepositoryState::Local(git_repository) => git_repository.checkpoint(cx).await,
+                RepositoryState::Local(git_repository) => git_repository.checkpoint().await,
                 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
             }
         })
@@ -3471,10 +3468,10 @@ impl Repository {
         &self,
         checkpoint: GitRepositoryCheckpoint,
     ) -> oneshot::Receiver<Result<()>> {
-        self.send_job(move |repo, cx| async move {
+        self.send_job(move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.restore_checkpoint(checkpoint, cx).await
+                    git_repository.restore_checkpoint(checkpoint).await
                 }
                 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
             }
@@ -3513,10 +3510,10 @@ impl Repository {
         left: GitRepositoryCheckpoint,
         right: GitRepositoryCheckpoint,
     ) -> oneshot::Receiver<Result<bool>> {
-        self.send_job(move |repo, cx| async move {
+        self.send_job(move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.compare_checkpoints(left, right, cx).await
+                    git_repository.compare_checkpoints(left, right).await
                 }
                 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
             }
@@ -3527,10 +3524,10 @@ impl Repository {
         &self,
         checkpoint: GitRepositoryCheckpoint,
     ) -> oneshot::Receiver<Result<()>> {
-        self.send_job(move |repo, cx| async move {
+        self.send_job(move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(git_repository) => {
-                    git_repository.delete_checkpoint(checkpoint, cx).await
+                    git_repository.delete_checkpoint(checkpoint).await
                 }
                 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
             }

crates/project/src/project.rs 🔗

@@ -1476,7 +1476,7 @@ impl Project {
     ) -> Entity<Project> {
         use clock::FakeSystemClock;
 
-        let fs = Arc::new(RealFs::default());
+        let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
         let languages = LanguageRegistry::test(cx.background_executor().clone());
         let clock = Arc::new(FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();

crates/project/src/project_tests.rs 🔗

@@ -99,7 +99,12 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     )
     .unwrap();
 
-    let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
+    let project = Project::test(
+        Arc::new(RealFs::new(None, cx.executor())),
+        [root_link_path.as_ref()],
+        cx,
+    )
+    .await;
 
     project.update(cx, |project, cx| {
         let tree = project.worktrees(cx).next().unwrap().read(cx);
@@ -3332,7 +3337,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
         }
     }));
 
-    let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
+    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
 
     let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
         let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));

crates/remote_server/src/unix.rs 🔗

@@ -445,7 +445,7 @@ pub fn execute_run(
         let extension_host_proxy = ExtensionHostProxy::global(cx);
 
         let project = cx.new(|cx| {
-            let fs = Arc::new(RealFs::new(None));
+            let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
             let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);
 
             let proxy_url = read_proxy_settings(cx);

crates/worktree/src/worktree.rs 🔗

@@ -1035,16 +1035,13 @@ impl Worktree {
             Worktree::Local(this) => {
                 let path = Arc::from(path);
                 let snapshot = this.snapshot();
-                cx.spawn(async move |cx| {
+                cx.spawn(async move |_cx| {
                     if let Some(repo) = snapshot.local_repo_containing_path(&path) {
                         if let Some(repo_path) = repo.relativize(&path).log_err() {
                             if let Some(git_repo) =
                                 snapshot.git_repositories.get(&repo.work_directory_id)
                             {
-                                return Ok(git_repo
-                                    .repo_ptr
-                                    .load_index_text(repo_path, cx.clone())
-                                    .await);
+                                return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
                             }
                         }
                     }
@@ -1062,16 +1059,13 @@ impl Worktree {
             Worktree::Local(this) => {
                 let path = Arc::from(path);
                 let snapshot = this.snapshot();
-                cx.spawn(async move |cx| {
+                cx.spawn(async move |_cx| {
                     if let Some(repo) = snapshot.local_repo_containing_path(&path) {
                         if let Some(repo_path) = repo.relativize(&path).log_err() {
                             if let Some(git_repo) =
                                 snapshot.git_repositories.get(&repo.work_directory_id)
                             {
-                                return Ok(git_repo
-                                    .repo_ptr
-                                    .load_committed_text(repo_path, cx.clone())
-                                    .await);
+                                return Ok(git_repo.repo_ptr.load_committed_text(repo_path).await);
                             }
                         }
                     }
@@ -5027,7 +5021,7 @@ impl BackgroundScanner {
         }
 
         for (_work_directory, mut paths) in paths_by_git_repo {
-            if let Ok(status) = paths.repo.status(&paths.repo_paths) {
+            if let Ok(status) = paths.repo.status_blocking(&paths.repo_paths) {
                 let mut changed_path_statuses = Vec::new();
                 let statuses = paths.entry.statuses_by_path.clone();
                 let mut cursor = statuses.cursor::<PathProgress>(&());
@@ -5531,7 +5525,7 @@ async fn do_git_status_update(
     log::trace!("updating git statuses for repo {repository_name}");
     let Some(statuses) = local_repository
         .repo()
-        .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
+        .status_blocking(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
         .log_err()
     else {
         return;

crates/worktree/src/worktree_tests.rs 🔗

@@ -351,7 +351,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
     const OLD_NAME: &str = "aaa.rs";
     const NEW_NAME: &str = "AAA.rs";
 
-    let fs = Arc::new(RealFs::default());
+    let fs = Arc::new(RealFs::new(None, cx.executor()));
     let temp_root = TempTree::new(json!({
         OLD_NAME: "",
     }));
@@ -876,7 +876,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
     let worktree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -965,7 +965,7 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1028,7 +1028,7 @@ async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext)
     let tree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1085,7 +1085,7 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
     let tree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1166,7 +1166,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1271,7 +1271,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         dir.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1382,7 +1382,7 @@ async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         dot_git_worktree_dir.clone(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -1512,7 +1512,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
         assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
     });
 
-    let fs_real = Arc::new(RealFs::default());
+    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
     let temp_root = TempTree::new(json!({
         "a": {}
     }));
@@ -2126,7 +2126,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root_path,
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -2233,7 +2233,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -2422,7 +2422,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -2551,7 +2551,7 @@ async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root.path(),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -2619,7 +2619,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root.path().join(project_root),
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )
@@ -2685,7 +2685,7 @@ async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
     let tree = Worktree::local(
         root_path,
         true,
-        Arc::new(RealFs::default()),
+        Arc::new(RealFs::new(None, cx.executor())),
         Default::default(),
         &mut cx.to_async(),
     )

crates/zed/src/main.rs 🔗

@@ -264,7 +264,7 @@ fn main() {
         };
     log::info!("Using git binary path: {:?}", git_binary_path);
 
-    let fs = Arc::new(RealFs::new(git_binary_path));
+    let fs = Arc::new(RealFs::new(git_binary_path, app.background_executor()));
     let user_settings_file_rx = watch_config_file(
         &app.background_executor(),
         fs.clone(),