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:
@@ -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!()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
})
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -347,6 +347,7 @@ message Commit {
|
||||
string message = 6;
|
||||
optional CommitOptions options = 7;
|
||||
reserved 8;
|
||||
uint64 askpass_id = 9;
|
||||
|
||||
message CommitOptions {
|
||||
bool amend = 1;
|
||||
|
||||
Reference in New Issue
Block a user