git: Add git stash picker (#35927)

Closes #ISSUE

This PR continues work from #32821 by adding a stash entry picker for
pop/drop operations. Additionally, the stash pop action in the git panel
is now disabled when no stash entries exist, preventing error logs from
attempted pops on empty stashes.

Preview:

<img width="1920" height="1256" alt="Screenshot From 2025-09-11
14-08-31"
src="https://github.com/user-attachments/assets/b2f32974-8c69-4e50-8951-24ab2cf93c12"
/>

<img width="1920" height="1256" alt="Screenshot From 2025-09-11
14-08-12"
src="https://github.com/user-attachments/assets/992ce237-43c9-456e-979c-c2e2149d633e"
/>



Release Notes:

- Added a stash picker to pop and drop a specific stash entry
- Disabled the stash pop action on the git panel when no stash entries
exist
- Added git stash apply command
- Added git stash drop command
This commit is contained in:
Alvaro Parker
2025-09-12 14:45:38 -04:00
committed by GitHub
parent 2143c59fba
commit 4b7595c94c
19 changed files with 1179 additions and 21 deletions
+37 -4
View File
@@ -24,11 +24,12 @@ use git::repository::{
PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
UpstreamTrackingStatus, get_git_committer,
};
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, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity,
@@ -121,6 +122,7 @@ struct GitMenuState {
has_unstaged_changes: bool,
has_new_changes: bool,
sort_by_path: bool,
has_stash_items: bool,
}
fn git_panel_context_menu(
@@ -148,7 +150,8 @@ fn git_panel_context_menu(
"Stash All",
StashAll.boxed_clone(),
)
.action("Stash Pop", StashPop.boxed_clone())
.action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone())
.action("View Stash", zed_actions::git::ViewStash.boxed_clone())
.separator()
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
@@ -382,6 +385,7 @@ pub struct GitPanel {
local_committer: Option<GitCommitter>,
local_committer_task: Option<Task<()>>,
bulk_staging: Option<BulkStaging>,
stash_entries: GitStash,
_settings_subscription: Subscription,
}
@@ -569,6 +573,7 @@ impl GitPanel {
horizontal_scrollbar,
vertical_scrollbar,
bulk_staging: None,
stash_entries: Default::default(),
_settings_subscription,
};
@@ -1438,7 +1443,7 @@ impl GitPanel {
cx.spawn({
async move |this, cx| {
let stash_task = active_repository
.update(cx, |repo, cx| repo.stash_pop(cx))?
.update(cx, |repo, cx| repo.stash_pop(None, cx))?
.await;
this.update(cx, |this, cx| {
stash_task
@@ -1453,6 +1458,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;
@@ -2734,6 +2762,8 @@ impl GitPanel {
let repo = repo.read(cx);
self.stash_entries = repo.cached_stash();
for entry in repo.cached_status() {
let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path);
let is_new = entry.status.is_created();
@@ -3102,6 +3132,7 @@ impl GitPanel {
let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
PopoverMenu::new(id.into())
.trigger(
@@ -3118,6 +3149,7 @@ impl GitPanel {
has_unstaged_changes,
has_new_changes,
sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
has_stash_items,
},
window,
cx,
@@ -4173,6 +4205,7 @@ impl GitPanel {
has_unstaged_changes: self.has_unstaged_changes(),
has_new_changes: self.new_count > 0,
sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
has_stash_items: self.stash_entries.entries.len() > 0,
},
window,
cx,