git: Add askpass delegate to git-commit handlers (#42239)

In my local setup, I always enforce git-commit signing with GPG/SSH
which automatically enforces `git commit -S` when committing. This
changeset will now show a modal to the user for them to specify the
passphrase (if any) so that they can unlock their private key for
signing when committing in Zed.

<img width="1086" height="948" alt="Screenshot 2025-11-07 at 11 09
09 PM"
src="https://github.com/user-attachments/assets/ac34b427-c833-41c7-b634-8781493f8a5e"
/>


Release Notes:

- Handle automatic git-commit signing by presenting the user with an
askpass modal
This commit is contained in:
Jakub Konka
2025-11-10 07:57:50 +00:00
committed by GitHub
parent b8081ad7a6
commit 359160c8b1
5 changed files with 69 additions and 29 deletions
+1
View File
@@ -526,6 +526,7 @@ impl GitRepository for FakeGitRepository {
_message: gpui::SharedString,
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
_options: CommitOptions,
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
+44 -26
View File
@@ -491,6 +491,7 @@ pub trait GitRepository: Send + Sync {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
askpass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
@@ -1630,41 +1631,39 @@ impl GitRepository for RealGitRepository {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.any_git_binary_path.clone();
self.executor
.spawn(async move {
let mut cmd = new_smol_command(git_binary_path);
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(["commit", "--quiet", "-m"])
.arg(&message.to_string())
.arg("--cleanup=strip");
let executor = self.executor.clone();
async move {
let mut cmd = new_smol_command(git_binary_path);
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(["commit", "--quiet", "-m"])
.arg(&message.to_string())
.arg("--cleanup=strip")
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());
if options.amend {
cmd.arg("--amend");
}
if options.amend {
cmd.arg("--amend");
}
if options.signoff {
cmd.arg("--signoff");
}
if options.signoff {
cmd.arg("--signoff");
}
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?;
run_git_command(env, ask_pass, cmd, &executor).await?;
anyhow::ensure!(
output.status.success(),
"Failed to commit:\n{}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
})
.boxed()
Ok(())
}
.boxed()
}
fn push(
@@ -2469,8 +2468,17 @@ mod tests {
use super::*;
use gpui::TestAppContext;
fn disable_git_global_config() {
unsafe {
std::env::set_var("GIT_CONFIG_GLOBAL", "");
std::env::set_var("GIT_CONFIG_SYSTEM", "");
}
}
#[gpui::test]
async fn test_checkpoint_basic(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
@@ -2486,6 +2494,7 @@ mod tests {
cx.executor(),
)
.unwrap();
repo.stage_paths(vec![repo_path("file")], Arc::new(HashMap::default()))
.await
.unwrap();
@@ -2493,6 +2502,7 @@ mod tests {
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
@@ -2519,6 +2529,7 @@ mod tests {
"Commit after checkpoint".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
@@ -2556,6 +2567,8 @@ mod tests {
#[gpui::test]
async fn test_checkpoint_empty_repo(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
@@ -2600,6 +2613,8 @@ mod tests {
#[gpui::test]
async fn test_compare_checkpoints(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
@@ -2639,6 +2654,8 @@ mod tests {
#[gpui::test]
async fn test_checkpoint_exclude_binary_files(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
@@ -2669,6 +2686,7 @@ mod tests {
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
+3 -2
View File
@@ -1585,6 +1585,7 @@ impl GitPanel {
return;
}
let askpass = self.askpass_delegate("git commit", window, cx);
let commit_message = self.custom_or_suggested_commit_message(window, cx);
let Some(mut message) = commit_message else {
@@ -1599,7 +1600,7 @@ 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.update(cx, |repo, cx| {
repo.commit(message.into(), None, options, cx)
repo.commit(message.into(), None, options, askpass, cx)
});
cx.background_spawn(async move { commit_task.await? })
} else {
@@ -1621,7 +1622,7 @@ impl GitPanel {
cx.spawn(async move |_, cx| {
stage_task.await?;
let commit_task = active_repository.update(cx, |repo, cx| {
repo.commit(message.into(), None, options, cx)
repo.commit(message.into(), None, options, askpass, cx)
})?;
commit_task.await?
})
+20 -1
View File
@@ -1957,6 +1957,15 @@ impl GitStore {
) -> Result<proto::Ack> {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let askpass_id = envelope.payload.askpass_id;
let askpass = make_remote_delegate(
this,
envelope.payload.project_id,
repository_id,
askpass_id,
&mut cx,
);
let message = SharedString::from(envelope.payload.message);
let name = envelope.payload.name.map(SharedString::from);
@@ -1972,6 +1981,7 @@ impl GitStore {
amend: options.amend,
signoff: options.signoff,
},
askpass,
cx,
)
})?
@@ -4216,9 +4226,12 @@ impl Repository {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
askpass: AskPassDelegate,
_cx: &mut App,
) -> oneshot::Receiver<Result<()>> {
let id = self.id;
let askpass_delegates = self.askpass_delegates.clone();
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
self.send_job(Some("git commit".into()), move |git_repo, _cx| async move {
match git_repo {
@@ -4228,10 +4241,15 @@ impl Repository {
..
} => {
backend
.commit(message, name_and_email, options, environment)
.commit(message, name_and_email, options, askpass, environment)
.await
}
RepositoryState::Remote { project_id, client } => {
askpass_delegates.lock().insert(askpass_id, askpass);
let _defer = util::defer(|| {
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
debug_assert!(askpass_delegate.is_some());
});
let (name, email) = name_and_email.unzip();
client
.request(proto::Commit {
@@ -4244,6 +4262,7 @@ impl Repository {
amend: options.amend,
signoff: options.signoff,
}),
askpass_id,
})
.await
.context("sending commit request")?;
+1
View File
@@ -347,6 +347,7 @@ message Commit {
string message = 6;
optional CommitOptions options = 7;
reserved 8;
uint64 askpass_id = 9;
message CommitOptions {
bool amend = 1;