Add stash apply action

This commit is contained in:
Alvaro Parker 2025-08-19 08:37:59 -04:00
parent 4a11d9d4c6
commit 775425e8c7
No known key found for this signature in database
10 changed files with 180 additions and 6 deletions

View file

@ -424,6 +424,14 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn stash_apply(
&self,
_index: Option<usize>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
fn stash_drop(
&self,
_index: Option<usize>,

View file

@ -60,6 +60,8 @@ actions!(
StashAll,
/// Pops the most recent stash.
StashPop,
/// Apply the most recent stash.
StashApply,
/// Restores all tracked files to their last committed state.
RestoreTrackedFiles,
/// Moves all untracked files to trash.

View file

@ -408,6 +408,12 @@ pub trait GitRepository: Send + Sync {
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
fn stash_apply(
&self,
index: Option<usize>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
fn stash_drop(
&self,
index: Option<usize>,
@ -1289,6 +1295,35 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn stash_apply(
&self,
index: Option<usize>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
let mut cmd = new_smol_command("git");
let mut args = vec!["stash".to_string(), "apply".to_string()];
if let Some(index) = index {
args.push(format!("stash@{{{}}}", index));
}
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(args);
let output = cmd.output().await?;
anyhow::ensure!(
output.status.success(),
"Failed to apply stash:\n{}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
})
.boxed()
}
fn stash_drop(
&self,
index: Option<usize>,

View file

@ -28,8 +28,8 @@ use git::stash::GitStash;
use git::status::StageStatus;
use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
use git::{
ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashPop, TrashUntrackedFiles,
UnstageAll,
ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop,
TrashUntrackedFiles, UnstageAll,
};
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
@ -1442,6 +1442,29 @@ impl GitPanel {
.detach();
}
pub fn stash_apply(&mut self, _: &StashApply, _window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
cx.spawn({
async move |this, cx| {
let stash_task = active_repository
.update(cx, |repo, cx| repo.stash_apply(None, cx))?
.await;
this.update(cx, |this, cx| {
stash_task
.map_err(|e| {
this.show_error_toast("stash apply", e, cx);
})
.ok();
cx.notify();
})
}
})
.detach();
}
pub fn stash_all(&mut self, _: &StashAll, _window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.clone() else {
return;

View file

@ -135,6 +135,14 @@ pub fn init(cx: &mut App) {
panel.stash_pop(action, window, cx);
});
});
workspace.register_action(|workspace, action: &git::StashApply, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.stash_apply(action, window, cx);
});
});
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;

View file

@ -253,6 +253,22 @@ impl StashListDelegate {
});
cx.emit(DismissEvent);
}
fn apply_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(repo) = self.repo.clone() else {
return;
};
cx.spawn(async move |_, cx| {
repo.update(cx, |repo, cx| repo.stash_apply(Some(stash_index), cx))?
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| {
Some(e.to_string())
});
cx.emit(DismissEvent);
}
}
impl PickerDelegate for StashListDelegate {
@ -356,12 +372,16 @@ impl PickerDelegate for StashListDelegate {
})
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry_match) = self.matches.get(self.selected_index()) else {
return;
};
let stash_index = entry_match.entry.index;
self.pop_stash(stash_index, window, cx);
if secondary {
self.pop_stash(stash_index, window, cx);
} else {
self.apply_stash(stash_index, window, cx);
}
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
@ -486,7 +506,7 @@ impl PickerDelegate for StashListDelegate {
}),
)
.child(
Button::new("pop-stash", "Pop")
Button::new("apply-stash", "Apply")
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
@ -499,6 +519,21 @@ impl PickerDelegate for StashListDelegate {
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.child(
Button::new("pop-stash", "Pop")
.key_binding(
KeyBinding::for_action_in(
&menu::SecondaryConfirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
),
)
.into_any(),

View file

@ -427,6 +427,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_unstage);
client.add_entity_request_handler(Self::handle_stash);
client.add_entity_request_handler(Self::handle_stash_pop);
client.add_entity_request_handler(Self::handle_stash_apply);
client.add_entity_request_handler(Self::handle_stash_drop);
client.add_entity_request_handler(Self::handle_commit);
client.add_entity_request_handler(Self::handle_reset);
@ -1792,6 +1793,24 @@ impl GitStore {
Ok(proto::Ack {})
}
async fn handle_stash_apply(
this: Entity<Self>,
envelope: TypedEnvelope<proto::StashApply>,
mut cx: AsyncApp,
) -> 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 stash_index = envelope.payload.stash_index.map(|i| i as usize);
repository_handle
.update(&mut cx, |repository_handle, cx| {
repository_handle.stash_apply(stash_index, cx)
})?
.await?;
Ok(proto::Ack {})
}
async fn handle_stash_drop(
this: Entity<Self>,
envelope: TypedEnvelope<proto::StashDrop>,
@ -3791,6 +3810,40 @@ impl Repository {
})
}
pub fn stash_apply(
&mut self,
index: Option<usize>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
let id = self.id;
cx.spawn(async move |this, cx| {
this.update(cx, |this, _| {
this.send_job(None, move |git_repo, _cx| async move {
match git_repo {
RepositoryState::Local {
backend,
environment,
..
} => backend.stash_apply(index, environment).await,
RepositoryState::Remote { project_id, client } => {
client
.request(proto::StashApply {
project_id: project_id.0,
repository_id: id.to_proto(),
stash_index: index.map(|i| i as u64),
})
.await
.context("sending stash apply request")?;
Ok(())
}
}
})
})?
.await??;
Ok(())
})
}
pub fn stash_drop(
&mut self,
index: Option<usize>,

View file

@ -318,6 +318,12 @@ message StashPop {
optional uint64 stash_index = 3;
}
message StashApply {
uint64 project_id = 1;
uint64 repository_id = 2;
optional uint64 stash_index = 3;
}
message StashDrop {
uint64 project_id = 1;
uint64 repository_id = 2;

View file

@ -397,7 +397,8 @@ message Envelope {
LspQuery lsp_query = 365;
LspQueryResponse lsp_query_response = 366;
StashDrop stash_drop = 367; // current max
StashDrop stash_drop = 367;
StashApply stash_apply = 368; // current max
}
reserved 87 to 88;

View file

@ -257,6 +257,7 @@ messages!(
(Unstage, Background),
(Stash, Background),
(StashPop, Background),
(StashApply, Background),
(StashDrop, Background),
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
@ -418,6 +419,7 @@ request_messages!(
(Unstage, Ack),
(Stash, Ack),
(StashPop, Ack),
(StashApply, Ack),
(StashDrop, Ack),
(UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
@ -572,6 +574,7 @@ entity_messages!(
Unstage,
Stash,
StashPop,
StashApply,
StashDrop,
UpdateBuffer,
UpdateBufferFile,