git: Pass project environment to git binary invocations (#26301)

Cole Miller created

Closes #26213 

Release Notes:

- Git Beta: pass down environment variables from project to git
operations

Change summary

crates/git/src/repository.rs                 | 111 ++++++++++++++++---
crates/git_ui/src/git_panel.rs               |  10 +
crates/gpui/src/app/entity_map.rs            |   1 
crates/project/src/git.rs                    | 121 ++++++++++++++++-----
crates/project/src/project.rs                |   3 
crates/remote_server/src/headless_project.rs |   3 
6 files changed, 194 insertions(+), 55 deletions(-)

Detailed changes

crates/git/src/repository.rs 🔗

@@ -149,7 +149,12 @@ pub trait GitRepository: Send + Sync {
     /// Also returns `None` for symlinks.
     fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
 
-    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
+    fn set_index_text(
+        &self,
+        path: &RepoPath,
+        content: Option<String>,
+        env: &HashMap<String, String>,
+    ) -> anyhow::Result<()>;
 
     /// Returns the URL of the remote with the given name.
     fn remote_url(&self, name: &str) -> Option<String>;
@@ -167,8 +172,13 @@ pub trait GitRepository: Send + Sync {
     fn create_branch(&self, _: &str) -> Result<()>;
     fn branch_exits(&self, _: &str) -> Result<bool>;
 
-    fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
-    fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
+    fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()>;
+    fn checkout_files(
+        &self,
+        commit: &str,
+        paths: &[RepoPath],
+        env: &HashMap<String, String>,
+    ) -> Result<()>;
 
     fn show(&self, commit: &str) -> Result<CommitDetails>;
 
@@ -189,13 +199,18 @@ pub trait GitRepository: Send + Sync {
     /// Updates the index to match the worktree at the given paths.
     ///
     /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
-    fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
+    fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
     /// Updates the index to match HEAD at the given paths.
     ///
     /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
-    fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
+    fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
 
-    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
+    fn commit(
+        &self,
+        message: &str,
+        name_and_email: Option<(&str, &str)>,
+        env: &HashMap<String, String>,
+    ) -> Result<()>;
 
     fn push(
         &self,
@@ -203,6 +218,7 @@ pub trait GitRepository: Send + Sync {
         upstream_name: &str,
         options: Option<PushOptions>,
         askpass: AskPassSession,
+        env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput>;
 
     fn pull(
@@ -210,8 +226,13 @@ pub trait GitRepository: Send + Sync {
         branch_name: &str,
         upstream_name: &str,
         askpass: AskPassSession,
+        env: &HashMap<String, String>,
+    ) -> Result<RemoteCommandOutput>;
+    fn fetch(
+        &self,
+        askpass: AskPassSession,
+        env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput>;
-    fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
 
     fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
 
@@ -308,7 +329,7 @@ impl GitRepository for RealGitRepository {
         Ok(details)
     }
 
-    fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
+    fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()> {
         let working_directory = self.working_directory()?;
 
         let mode_flag = match mode {
@@ -317,6 +338,7 @@ impl GitRepository for RealGitRepository {
         };
 
         let output = new_std_command(&self.git_binary_path)
+            .envs(env)
             .current_dir(&working_directory)
             .args(["reset", mode_flag, commit])
             .output()?;
@@ -329,7 +351,12 @@ impl GitRepository for RealGitRepository {
         Ok(())
     }
 
-    fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
+    fn checkout_files(
+        &self,
+        commit: &str,
+        paths: &[RepoPath],
+        env: &HashMap<String, String>,
+    ) -> Result<()> {
         if paths.is_empty() {
             return Ok(());
         }
@@ -337,6 +364,7 @@ impl GitRepository for RealGitRepository {
 
         let output = new_std_command(&self.git_binary_path)
             .current_dir(&working_directory)
+            .envs(env)
             .args(["checkout", commit, "--"])
             .args(paths.iter().map(|path| path.as_ref()))
             .output()?;
@@ -385,11 +413,17 @@ impl GitRepository for RealGitRepository {
         Some(content)
     }
 
-    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
+    fn set_index_text(
+        &self,
+        path: &RepoPath,
+        content: Option<String>,
+        env: &HashMap<String, String>,
+    ) -> anyhow::Result<()> {
         let working_directory = self.working_directory()?;
         if let Some(content) = content {
             let mut child = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
+                .envs(env)
                 .args(["hash-object", "-w", "--stdin"])
                 .stdin(Stdio::piped())
                 .stdout(Stdio::piped())
@@ -402,6 +436,7 @@ impl GitRepository for RealGitRepository {
 
             let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
+                .envs(env)
                 .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
                 .arg(path.as_ref())
                 .output()?;
@@ -415,6 +450,7 @@ impl GitRepository for RealGitRepository {
         } else {
             let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
+                .envs(env)
                 .args(["update-index", "--force-remove"])
                 .arg(path.as_ref())
                 .output()?;
@@ -607,12 +643,13 @@ impl GitRepository for RealGitRepository {
         Ok(String::from_utf8_lossy(&output.stdout).to_string())
     }
 
-    fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
+    fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
         let working_directory = self.working_directory()?;
 
         if !paths.is_empty() {
             let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
+                .envs(env)
                 .args(["update-index", "--add", "--remove", "--"])
                 .args(paths.iter().map(|p| p.as_ref()))
                 .output()?;
@@ -627,12 +664,13 @@ impl GitRepository for RealGitRepository {
         Ok(())
     }
 
-    fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
+    fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
         let working_directory = self.working_directory()?;
 
         if !paths.is_empty() {
             let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
+                .envs(env)
                 .args(["reset", "--quiet", "--"])
                 .args(paths.iter().map(|p| p.as_ref()))
                 .output()?;
@@ -647,11 +685,17 @@ impl GitRepository for RealGitRepository {
         Ok(())
     }
 
-    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(
+        &self,
+        message: &str,
+        name_and_email: Option<(&str, &str)>,
+        env: &HashMap<String, String>,
+    ) -> Result<()> {
         let working_directory = self.working_directory()?;
 
         let mut cmd = new_std_command(&self.git_binary_path);
         cmd.current_dir(&working_directory)
+            .envs(env)
             .args(["commit", "--quiet", "-m"])
             .arg(message)
             .arg("--cleanup=strip");
@@ -677,11 +721,13 @@ impl GitRepository for RealGitRepository {
         remote_name: &str,
         options: Option<PushOptions>,
         ask_pass: AskPassSession,
+        env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput> {
         let working_directory = self.working_directory()?;
 
         let mut command = new_smol_command("git");
         command
+            .envs(env)
             .env("GIT_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS_REQUIRE", "force")
@@ -705,11 +751,13 @@ impl GitRepository for RealGitRepository {
         branch_name: &str,
         remote_name: &str,
         ask_pass: AskPassSession,
+        env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput> {
         let working_directory = self.working_directory()?;
 
         let mut command = new_smol_command("git");
         command
+            .envs(env)
             .env("GIT_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS_REQUIRE", "force")
@@ -724,11 +772,16 @@ impl GitRepository for RealGitRepository {
         run_remote_command(ask_pass, git_process)
     }
 
-    fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
+    fn fetch(
+        &self,
+        ask_pass: AskPassSession,
+        env: &HashMap<String, String>,
+    ) -> Result<RemoteCommandOutput> {
         let working_directory = self.working_directory()?;
 
         let mut command = new_smol_command("git");
         command
+            .envs(env)
             .env("GIT_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS", ask_pass.script_path())
             .env("SSH_ASKPASS_REQUIRE", "force")
@@ -919,7 +972,12 @@ impl GitRepository for FakeGitRepository {
         state.head_contents.get(path.as_ref()).cloned()
     }
 
-    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
+    fn set_index_text(
+        &self,
+        path: &RepoPath,
+        content: Option<String>,
+        _env: &HashMap<String, String>,
+    ) -> anyhow::Result<()> {
         let mut state = self.state.lock();
         if let Some(message) = state.simulated_index_write_error_message.clone() {
             return Err(anyhow::anyhow!(message));
@@ -952,11 +1010,11 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 
-    fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
+    fn reset(&self, _: &str, _: ResetMode, _: &HashMap<String, String>) -> Result<()> {
         unimplemented!()
     }
 
-    fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
+    fn checkout_files(&self, _: &str, _: &[RepoPath], _: &HashMap<String, String>) -> Result<()> {
         unimplemented!()
     }
 
@@ -1042,15 +1100,20 @@ impl GitRepository for FakeGitRepository {
             .cloned()
     }
 
-    fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
+    fn stage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
         unimplemented!()
     }
 
-    fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
+    fn unstage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
         unimplemented!()
     }
 
-    fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(
+        &self,
+        _message: &str,
+        _name_and_email: Option<(&str, &str)>,
+        _env: &HashMap<String, String>,
+    ) -> Result<()> {
         unimplemented!()
     }
 
@@ -1060,6 +1123,7 @@ impl GitRepository for FakeGitRepository {
         _remote: &str,
         _options: Option<PushOptions>,
         _ask_pass: AskPassSession,
+        _env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput> {
         unimplemented!()
     }
@@ -1069,11 +1133,16 @@ impl GitRepository for FakeGitRepository {
         _branch: &str,
         _remote: &str,
         _ask_pass: AskPassSession,
+        _env: &HashMap<String, String>,
     ) -> Result<RemoteCommandOutput> {
         unimplemented!()
     }
 
-    fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
+    fn fetch(
+        &self,
+        _ask_pass: AskPassSession,
+        _env: &HashMap<String, String>,
+    ) -> Result<RemoteCommandOutput> {
         unimplemented!()
     }
 

crates/git_ui/src/git_panel.rs 🔗

@@ -901,13 +901,14 @@ impl GitPanel {
             let buffers = futures::future::join_all(tasks).await;
 
             active_repository
-                .update(&mut cx, |repo, _| {
+                .update(&mut cx, |repo, cx| {
                     repo.checkout_files(
                         "HEAD",
                         entries
                             .iter()
                             .map(|entries| entries.repo_path.clone())
                             .collect(),
+                        cx,
                     )
                 })?
                 .await??;
@@ -1289,7 +1290,8 @@ impl GitPanel {
 
         let task = if self.has_staged_changes() {
             // Repository serializes all git operations, so we can just send a commit immediately
-            let commit_task = active_repository.read(cx).commit(message.into(), None);
+            let commit_task =
+                active_repository.update(cx, |repo, cx| repo.commit(message.into(), None, cx));
             cx.background_spawn(async move { commit_task.await? })
         } else {
             let changed_files = self
@@ -1310,7 +1312,7 @@ impl GitPanel {
             cx.spawn(|_, mut cx| async move {
                 stage_task.await?;
                 let commit_task = active_repository
-                    .update(&mut cx, |repo, _| repo.commit(message.into(), None))?;
+                    .update(&mut cx, |repo, cx| repo.commit(message.into(), None, cx))?;
                 commit_task.await?
             })
         };
@@ -1346,7 +1348,7 @@ impl GitPanel {
                 if let Ok(true) = confirmation.await {
                     let prior_head = prior_head.await?;
 
-                    repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
+                    repo.update(&mut cx, |repo, cx| repo.reset("HEAD^", ResetMode::Soft, cx))?
                         .await??;
 
                     Ok(Some(prior_head))

crates/gpui/src/app/entity_map.rs 🔗

@@ -178,6 +178,7 @@ impl EntityMap {
     }
 }
 
+#[track_caller]
 fn double_lease_panic<T>(operation: &str) -> ! {
     panic!(
         "cannot {operation} {} while it is already being updated",

crates/project/src/git.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     buffer_store::{BufferStore, BufferStoreEvent},
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
-    Project, ProjectItem, ProjectPath,
+    Project, ProjectEnvironment, ProjectItem, ProjectPath,
 };
 use anyhow::{Context as _, Result};
 use askpass::{AskPassDelegate, AskPassSession};
@@ -10,6 +10,7 @@ use client::ProjectId;
 use collections::HashMap;
 use futures::{
     channel::{mpsc, oneshot},
+    future::OptionFuture,
     StreamExt as _,
 };
 use git::repository::DiffType;
@@ -43,6 +44,7 @@ use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
 
 pub struct GitStore {
     buffer_store: Entity<BufferStore>,
+    environment: Option<Entity<ProjectEnvironment>>,
     pub(super) project_id: Option<ProjectId>,
     pub(super) client: AnyProtoClient,
     repositories: Vec<Entity<Repository>>,
@@ -54,6 +56,7 @@ pub struct GitStore {
 pub struct Repository {
     commit_message_buffer: Option<Entity<Buffer>>,
     git_store: WeakEntity<GitStore>,
+    project_environment: Option<WeakEntity<ProjectEnvironment>>,
     pub worktree_id: WorktreeId,
     pub repository_entry: RepositoryEntry,
     pub dot_git_abs_path: PathBuf,
@@ -101,6 +104,7 @@ impl GitStore {
     pub fn new(
         worktree_store: &Entity<WorktreeStore>,
         buffer_store: Entity<BufferStore>,
+        environment: Option<Entity<ProjectEnvironment>>,
         client: AnyProtoClient,
         project_id: Option<ProjectId>,
         cx: &mut Context<'_, Self>,
@@ -115,6 +119,7 @@ impl GitStore {
             project_id,
             client,
             buffer_store,
+            environment,
             repositories: Vec::new(),
             active_index: None,
             update_sender,
@@ -225,6 +230,10 @@ impl GitStore {
                             existing_handle
                         } else {
                             cx.new(|_| Repository {
+                                project_environment: self
+                                    .environment
+                                    .as_ref()
+                                    .map(|env| env.downgrade()),
                                 git_store: this.clone(),
                                 worktree_id,
                                 askpass_delegates: Default::default(),
@@ -282,9 +291,13 @@ impl GitStore {
         if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
             let buffer_id = diff.read(cx).buffer_id;
             if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
-                let recv = repo
-                    .read(cx)
-                    .set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));
+                let recv = repo.update(cx, |repo, cx| {
+                    repo.set_index_text(
+                        &path,
+                        new_index_text.as_ref().map(|rope| rope.to_string()),
+                        cx,
+                    )
+                });
                 let diff = diff.downgrade();
                 cx.spawn(|this, mut cx| async move {
                     if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await
@@ -542,10 +555,11 @@ impl GitStore {
             Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
         repository_handle
-            .update(&mut cx, |repository_handle, _| {
+            .update(&mut cx, |repository_handle, cx| {
                 repository_handle.set_index_text(
                     &RepoPath::from_str(&envelope.payload.path),
                     envelope.payload.text,
+                    cx,
                 )
             })?
             .await??;
@@ -567,8 +581,8 @@ impl GitStore {
         let email = envelope.payload.email.map(SharedString::from);
 
         repository_handle
-            .update(&mut cx, |repository_handle, _| {
-                repository_handle.commit(message, name.zip(email))
+            .update(&mut cx, |repository_handle, cx| {
+                repository_handle.commit(message, name.zip(email), cx)
             })?
             .await??;
         Ok(proto::Ack {})
@@ -703,8 +717,8 @@ impl GitStore {
         };
 
         repository_handle
-            .update(&mut cx, |repository_handle, _| {
-                repository_handle.reset(&envelope.payload.commit, mode)
+            .update(&mut cx, |repository_handle, cx| {
+                repository_handle.reset(&envelope.payload.commit, mode, cx)
             })?
             .await??;
         Ok(proto::Ack {})
@@ -727,8 +741,8 @@ impl GitStore {
             .collect();
 
         repository_handle
-            .update(&mut cx, |repository_handle, _| {
-                repository_handle.checkout_files(&envelope.payload.commit, paths)
+            .update(&mut cx, |repository_handle, cx| {
+                repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
             })?
             .await??;
         Ok(proto::Ack {})
@@ -1115,11 +1129,14 @@ impl Repository {
         &self,
         commit: &str,
         paths: Vec<RepoPath>,
+        cx: &mut App,
     ) -> oneshot::Receiver<Result<()>> {
         let commit = commit.to_string();
+        let env = self.worktree_environment(cx);
+
         self.send_job(|git_repo| async move {
             match git_repo {
-                GitRepo::Local(repo) => repo.checkout_files(&commit, &paths),
+                GitRepo::Local(repo) => repo.checkout_files(&commit, &paths, &env.await),
                 GitRepo::Remote {
                     project_id,
                     client,
@@ -1145,11 +1162,20 @@ impl Repository {
         })
     }
 
-    pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
+    pub fn reset(
+        &self,
+        commit: &str,
+        reset_mode: ResetMode,
+        cx: &mut App,
+    ) -> oneshot::Receiver<Result<()>> {
         let commit = commit.to_string();
+        let env = self.worktree_environment(cx);
         self.send_job(|git_repo| async move {
             match git_repo {
-                GitRepo::Local(git_repo) => git_repo.reset(&commit, reset_mode),
+                GitRepo::Local(git_repo) => {
+                    let env = env.await;
+                    git_repo.reset(&commit, reset_mode, &env)
+                }
                 GitRepo::Remote {
                     project_id,
                     client,
@@ -1219,6 +1245,7 @@ impl Repository {
         if entries.is_empty() {
             return Task::ready(Ok(()));
         }
+        let env = self.worktree_environment(cx);
 
         let mut save_futures = Vec::new();
         if let Some(buffer_store) = self.buffer_store(cx) {
@@ -1245,11 +1272,12 @@ impl Repository {
             for save_future in save_futures {
                 save_future.await?;
             }
+            let env = env.await;
 
             this.update(&mut cx, |this, _| {
                 this.send_job(|git_repo| async move {
                     match git_repo {
-                        GitRepo::Local(repo) => repo.stage_paths(&entries),
+                        GitRepo::Local(repo) => repo.stage_paths(&entries, &env),
                         GitRepo::Remote {
                             project_id,
                             client,
@@ -1288,6 +1316,7 @@ impl Repository {
         if entries.is_empty() {
             return Task::ready(Ok(()));
         }
+        let env = self.worktree_environment(cx);
 
         let mut save_futures = Vec::new();
         if let Some(buffer_store) = self.buffer_store(cx) {
@@ -1314,11 +1343,12 @@ impl Repository {
             for save_future in save_futures {
                 save_future.await?;
             }
+            let env = env.await;
 
             this.update(&mut cx, |this, _| {
                 this.send_job(|git_repo| async move {
                     match git_repo {
-                        GitRepo::Local(repo) => repo.unstage_paths(&entries),
+                        GitRepo::Local(repo) => repo.unstage_paths(&entries, &env),
                         GitRepo::Remote {
                             project_id,
                             client,
@@ -1375,19 +1405,42 @@ impl Repository {
         self.repository_entry.status_len()
     }
 
+    fn worktree_environment(
+        &self,
+        cx: &mut App,
+    ) -> impl Future<Output = HashMap<String, String>> + 'static {
+        let task = self.project_environment.as_ref().and_then(|env| {
+            env.update(cx, |env, cx| {
+                env.get_environment(
+                    Some(self.worktree_id),
+                    Some(self.worktree_abs_path.clone()),
+                    cx,
+                )
+            })
+            .ok()
+        });
+        async move { OptionFuture::from(task).await.flatten().unwrap_or_default() }
+    }
+
     pub fn commit(
         &self,
         message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
+        cx: &mut App,
     ) -> oneshot::Receiver<Result<()>> {
+        let env = self.worktree_environment(cx);
         self.send_job(|git_repo| async move {
             match git_repo {
-                GitRepo::Local(repo) => repo.commit(
-                    message.as_ref(),
-                    name_and_email
-                        .as_ref()
-                        .map(|(name, email)| (name.as_ref(), email.as_ref())),
-                ),
+                GitRepo::Local(repo) => {
+                    let env = env.await;
+                    repo.commit(
+                        message.as_ref(),
+                        name_and_email
+                            .as_ref()
+                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
+                        &env,
+                    )
+                }
                 GitRepo::Remote {
                     project_id,
                     client,
@@ -1416,17 +1469,19 @@ impl Repository {
     pub fn fetch(
         &mut self,
         askpass: AskPassDelegate,
-        cx: &App,
+        cx: &mut App,
     ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
         let executor = cx.background_executor().clone();
         let askpass_delegates = self.askpass_delegates.clone();
         let askpass_id = util::post_inc(&mut self.latest_askpass_id);
+        let env = self.worktree_environment(cx);
 
         self.send_job(move |git_repo| async move {
             match git_repo {
                 GitRepo::Local(git_repository) => {
                     let askpass = AskPassSession::new(&executor, askpass).await?;
-                    git_repository.fetch(askpass)
+                    let env = env.await;
+                    git_repository.fetch(askpass, &env)
                 }
                 GitRepo::Remote {
                     project_id,
@@ -1465,17 +1520,19 @@ impl Repository {
         remote: SharedString,
         options: Option<PushOptions>,
         askpass: AskPassDelegate,
-        cx: &App,
+        cx: &mut App,
     ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
         let executor = cx.background_executor().clone();
         let askpass_delegates = self.askpass_delegates.clone();
         let askpass_id = util::post_inc(&mut self.latest_askpass_id);
+        let env = self.worktree_environment(cx);
 
         self.send_job(move |git_repo| async move {
             match git_repo {
                 GitRepo::Local(git_repository) => {
+                    let env = env.await;
                     let askpass = AskPassSession::new(&executor, askpass).await?;
-                    git_repository.push(&branch, &remote, options, askpass)
+                    git_repository.push(&branch, &remote, options, askpass, &env)
                 }
                 GitRepo::Remote {
                     project_id,
@@ -1518,16 +1575,19 @@ impl Repository {
         branch: SharedString,
         remote: SharedString,
         askpass: AskPassDelegate,
-        cx: &App,
+        cx: &mut App,
     ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
         let executor = cx.background_executor().clone();
         let askpass_delegates = self.askpass_delegates.clone();
         let askpass_id = util::post_inc(&mut self.latest_askpass_id);
+        let env = self.worktree_environment(cx);
+
         self.send_job(move |git_repo| async move {
             match git_repo {
                 GitRepo::Local(git_repository) => {
                     let askpass = AskPassSession::new(&executor, askpass).await?;
-                    git_repository.pull(&branch, &remote, askpass)
+                    let env = env.await;
+                    git_repository.pull(&branch, &remote, askpass, &env)
                 }
                 GitRepo::Remote {
                     project_id,
@@ -1565,13 +1625,16 @@ impl Repository {
         &self,
         path: &RepoPath,
         content: Option<String>,
+        cx: &mut App,
     ) -> oneshot::Receiver<anyhow::Result<()>> {
         let path = path.clone();
+        let env = self.worktree_environment(cx);
+
         self.send_keyed_job(
             Some(GitJobKey::WriteIndex(path.clone())),
             |git_repo| async move {
                 match git_repo {
-                    GitRepo::Local(repo) => repo.set_index_text(&path, content),
+                    GitRepo::Local(repo) => repo.set_index_text(&path, content, &env.await),
                     GitRepo::Remote {
                         project_id,
                         client,

crates/project/src/project.rs 🔗

@@ -844,6 +844,7 @@ impl Project {
                 GitStore::new(
                     &worktree_store,
                     buffer_store.clone(),
+                    Some(environment.clone()),
                     client.clone().into(),
                     None,
                     cx,
@@ -972,6 +973,7 @@ impl Project {
                 GitStore::new(
                     &worktree_store,
                     buffer_store.clone(),
+                    Some(environment.clone()),
                     ssh_proto.clone(),
                     Some(ProjectId(SSH_PROJECT_ID)),
                     cx,
@@ -1179,6 +1181,7 @@ impl Project {
             GitStore::new(
                 &worktree_store,
                 buffer_store.clone(),
+                None,
                 client.clone().into(),
                 Some(ProjectId(remote_id)),
                 cx,

crates/remote_server/src/headless_project.rs 🔗

@@ -87,10 +87,12 @@ impl HeadlessProject {
             buffer_store
         });
 
+        let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
         let git_store = cx.new(|cx| {
             GitStore::new(
                 &worktree_store,
                 buffer_store.clone(),
+                Some(environment.clone()),
                 session.clone().into(),
                 None,
                 cx,
@@ -105,7 +107,6 @@ impl HeadlessProject {
                 cx,
             )
         });
-        let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
         let toolchain_store = cx.new(|cx| {
             ToolchainStore::local(
                 languages.clone(),