Git askpass (#25953)

Supersedes #25848

Release Notes:

- git: Supporting push/pull/fetch when remote requires auth

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Conrad Irwin 2025-03-05 22:20:06 -07:00 committed by GitHub
parent 6fdb666bb7
commit c34357e2ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 864 additions and 379 deletions

View file

@ -4,8 +4,10 @@ use crate::{
Project, ProjectItem, ProjectPath,
};
use anyhow::{Context as _, Result};
use askpass::{AskPassDelegate, AskPassSession};
use buffer_diff::BufferDiffEvent;
use client::ProjectId;
use collections::HashMap;
use futures::{
channel::{mpsc, oneshot},
StreamExt as _,
@ -22,6 +24,7 @@ use gpui::{
WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use parking_lot::Mutex;
use rpc::{
proto::{self, git_reset, ToProto},
AnyProtoClient, TypedEnvelope,
@ -34,13 +37,13 @@ use std::{
sync::Arc,
};
use text::BufferId;
use util::{maybe, ResultExt};
use util::{debug_panic, maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
pub struct GitStore {
buffer_store: Entity<BufferStore>,
pub(super) project_id: Option<ProjectId>,
pub(super) client: Option<AnyProtoClient>,
pub(super) client: AnyProtoClient,
repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<GitJob>,
@ -55,6 +58,8 @@ pub struct Repository {
pub git_repo: GitRepo,
pub merge_message: Option<String>,
job_sender: mpsc::UnboundedSender<GitJob>,
askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
latest_askpass_id: u64,
}
#[derive(Clone)]
@ -92,7 +97,7 @@ impl GitStore {
pub fn new(
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
client: Option<AnyProtoClient>,
client: AnyProtoClient,
project_id: Option<ProjectId>,
cx: &mut Context<'_, Self>,
) -> Self {
@ -129,6 +134,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_checkout_files);
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
client.add_entity_request_handler(Self::handle_set_index_text);
client.add_entity_request_handler(Self::handle_askpass);
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
}
@ -164,7 +170,7 @@ impl GitStore {
)
})
.or_else(|| {
let client = client.clone()?;
let client = client.clone();
let project_id = project_id?;
Some((
GitRepo::Remote {
@ -216,6 +222,8 @@ impl GitStore {
cx.new(|_| Repository {
git_store: this.clone(),
worktree_id,
askpass_delegates: Default::default(),
latest_askpass_id: 0,
repository_entry: repo.clone(),
git_repo,
job_sender: self.update_sender.clone(),
@ -362,9 +370,21 @@ impl GitStore {
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let askpass_id = envelope.payload.askpass_id;
let askpass = make_remote_delegate(
this,
envelope.payload.project_id,
worktree_id,
work_directory_id,
askpass_id,
&mut cx,
);
let remote_output = repository_handle
.update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
.update(&mut cx, |repository_handle, cx| {
repository_handle.fetch(askpass, cx)
})?
.await??;
Ok(proto::RemoteMessageResponse {
@ -383,6 +403,16 @@ impl GitStore {
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let askpass_id = envelope.payload.askpass_id;
let askpass = make_remote_delegate(
this,
envelope.payload.project_id,
worktree_id,
work_directory_id,
askpass_id,
&mut cx,
);
let options = envelope
.payload
.options
@ -396,8 +426,8 @@ impl GitStore {
let remote_name = envelope.payload.remote_name.into();
let remote_output = repository_handle
.update(&mut cx, |repository_handle, _cx| {
repository_handle.push(branch_name, remote_name, options)
.update(&mut cx, |repository_handle, cx| {
repository_handle.push(branch_name, remote_name, options, askpass, cx)
})?
.await??;
Ok(proto::RemoteMessageResponse {
@ -415,15 +445,25 @@ impl GitStore {
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let askpass_id = envelope.payload.askpass_id;
let askpass = make_remote_delegate(
this,
envelope.payload.project_id,
worktree_id,
work_directory_id,
askpass_id,
&mut cx,
);
let branch_name = envelope.payload.branch_name.into();
let remote_name = envelope.payload.remote_name.into();
let remote_message = repository_handle
.update(&mut cx, |repository_handle, _cx| {
repository_handle.pull(branch_name, remote_name)
.update(&mut cx, |repository_handle, cx| {
repository_handle.pull(branch_name, remote_name, askpass, cx)
})?
.await??;
Ok(proto::RemoteMessageResponse {
stdout: remote_message.stdout,
stderr: remote_message.stderr,
@ -719,6 +759,31 @@ impl GitStore {
})
}
async fn handle_askpass(
this: Entity<Self>,
envelope: TypedEnvelope<proto::AskPassRequest>,
mut cx: AsyncApp,
) -> Result<proto::AskPassResponse> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?;
let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
debug_panic!("no askpass found");
return Err(anyhow::anyhow!("no askpass found"));
};
let response = askpass.ask_password(envelope.payload.prompt).await?;
delegates
.lock()
.insert(envelope.payload.askpass_id, askpass);
Ok(proto::AskPassResponse { response })
}
async fn handle_check_for_pushed_commits(
this: Entity<Self>,
envelope: TypedEnvelope<proto::CheckForPushedCommits>,
@ -765,6 +830,33 @@ impl GitStore {
}
}
fn make_remote_delegate(
this: Entity<GitStore>,
project_id: u64,
worktree_id: WorktreeId,
work_directory_id: ProjectEntryId,
askpass_id: u64,
cx: &mut AsyncApp,
) -> AskPassDelegate {
AskPassDelegate::new(cx, move |prompt, tx, cx| {
this.update(cx, |this, cx| {
let response = this.client.request(proto::AskPassRequest {
project_id,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
askpass_id,
prompt,
});
cx.spawn(|_, _| async move {
tx.send(response.await?.response).ok();
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
.log_err();
})
}
impl GitRepo {}
impl Repository {
@ -1286,21 +1378,39 @@ impl Repository {
})
}
pub fn fetch(&self) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
self.send_job(|git_repo| async move {
pub fn fetch(
&mut self,
askpass: AskPassDelegate,
cx: &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);
self.send_job(move |git_repo| async move {
match git_repo {
GitRepo::Local(git_repository) => git_repository.fetch(),
GitRepo::Local(git_repository) => {
let askpass = AskPassSession::new(&executor, askpass).await?;
git_repository.fetch(askpass)
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
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 response = client
.request(proto::Fetch {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
askpass_id,
})
.await
.context("sending fetch request")?;
@ -1315,25 +1425,40 @@ impl Repository {
}
pub fn push(
&self,
&mut self,
branch: SharedString,
remote: SharedString,
options: Option<PushOptions>,
askpass: AskPassDelegate,
cx: &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);
self.send_job(move |git_repo| async move {
match git_repo {
GitRepo::Local(git_repository) => git_repository.push(&branch, &remote, options),
GitRepo::Local(git_repository) => {
let askpass = AskPassSession::new(&executor, askpass).await?;
git_repository.push(&branch, &remote, options, askpass)
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
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 response = client
.request(proto::Push {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
askpass_id,
branch_name: branch.to_string(),
remote_name: remote.to_string(),
options: options.map(|options| match options {
@ -1354,24 +1479,38 @@ impl Repository {
}
pub fn pull(
&self,
&mut self,
branch: SharedString,
remote: SharedString,
askpass: AskPassDelegate,
cx: &App,
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
self.send_job(|git_repo| async move {
let executor = cx.background_executor().clone();
let askpass_delegates = self.askpass_delegates.clone();
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
self.send_job(move |git_repo| async move {
match git_repo {
GitRepo::Local(git_repository) => git_repository.pull(&branch, &remote),
GitRepo::Local(git_repository) => {
let askpass = AskPassSession::new(&executor, askpass).await?;
git_repository.pull(&branch, &remote, askpass)
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
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 response = client
.request(proto::Pull {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
askpass_id,
branch_name: branch.to_string(),
remote_name: remote.to_string(),
})

View file

@ -707,8 +707,15 @@ impl Project {
)
});
let git_store =
cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx));
let git_store = cx.new(|cx| {
GitStore::new(
&worktree_store,
buffer_store.clone(),
client.clone().into(),
None,
cx,
)
});
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@ -832,7 +839,7 @@ impl Project {
GitStore::new(
&worktree_store,
buffer_store.clone(),
Some(ssh_proto.clone()),
ssh_proto.clone(),
Some(ProjectId(SSH_PROJECT_ID)),
cx,
)
@ -1040,7 +1047,7 @@ impl Project {
GitStore::new(
&worktree_store,
buffer_store.clone(),
Some(client.clone().into()),
client.clone().into(),
Some(ProjectId(remote_id)),
cx,
)