Detailed changes
@@ -433,6 +433,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
+ .add_request_handler(forward_mutating_project_request::<proto::Stash>)
+ .add_request_handler(forward_mutating_project_request::<proto::StashPop>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
@@ -398,6 +398,18 @@ impl GitRepository for FakeGitRepository {
})
}
+ fn stash_paths(
+ &self,
+ _paths: Vec<RepoPath>,
+ _env: Arc<HashMap<String, String>>,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
fn commit(
&self,
_message: gpui::SharedString,
@@ -55,6 +55,10 @@ actions!(
StageAll,
/// Unstages all changes in the repository.
UnstageAll,
+ /// Stashes all changes in the repository, including untracked files.
+ StashAll,
+ /// Pops the most recent stash.
+ StashPop,
/// Restores all tracked files to their last committed state.
RestoreTrackedFiles,
/// Moves all untracked files to trash.
@@ -395,6 +395,14 @@ pub trait GitRepository: Send + Sync {
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
+ fn stash_paths(
+ &self,
+ paths: Vec<RepoPath>,
+ env: Arc<HashMap<String, String>>,
+ ) -> BoxFuture<Result<()>>;
+
+ fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>>;
+
fn push(
&self,
branch_name: String,
@@ -1189,6 +1197,55 @@ impl GitRepository for RealGitRepository {
.boxed()
}
+ fn stash_paths(
+ &self,
+ paths: Vec<RepoPath>,
+ 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");
+ cmd.current_dir(&working_directory?)
+ .envs(env.iter())
+ .args(["stash", "push", "--quiet"])
+ .arg("--include-untracked");
+
+ cmd.args(paths.iter().map(|p| p.as_ref()));
+
+ let output = cmd.output().await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "Failed to stash:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ Ok(())
+ })
+ .boxed()
+ }
+
+ fn stash_pop(&self, 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");
+ cmd.current_dir(&working_directory?)
+ .envs(env.iter())
+ .args(["stash", "pop"]);
+
+ let output = cmd.output().await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "Failed to stash pop:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ Ok(())
+ })
+ .boxed()
+ }
+
fn commit(
&self,
message: SharedString,
@@ -27,7 +27,10 @@ use git::repository::{
};
use git::status::StageStatus;
use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
-use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
+use git::{
+ ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashPop, TrashUntrackedFiles,
+ UnstageAll,
+};
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
@@ -140,6 +143,13 @@ fn git_panel_context_menu(
UnstageAll.boxed_clone(),
)
.separator()
+ .action_disabled_when(
+ !(state.has_new_changes || state.has_tracked_changes),
+ "Stash All",
+ StashAll.boxed_clone(),
+ )
+ .action("Stash Pop", StashPop.boxed_clone())
+ .separator()
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
.action_disabled_when(
@@ -1415,6 +1425,52 @@ impl GitPanel {
self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
}
+ pub fn stash_pop(&mut self, _: &StashPop, _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_pop(cx))?
+ .await;
+ this.update(cx, |this, cx| {
+ stash_task
+ .map_err(|e| {
+ this.show_error_toast("stash pop", 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;
+ };
+
+ cx.spawn({
+ async move |this, cx| {
+ let stash_task = active_repository
+ .update(cx, |repo, cx| repo.stash_all(cx))?
+ .await;
+ this.update(cx, |this, cx| {
+ stash_task
+ .map_err(|e| {
+ this.show_error_toast("stash", e, cx);
+ })
+ .ok();
+ cx.notify();
+ })
+ }
+ })
+ .detach();
+ }
+
pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
self.commit_editor
.read(cx)
@@ -4365,6 +4421,8 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::generate_commit_message_action))
+ .on_action(cx.listener(Self::stash_all))
+ .on_action(cx.listener(Self::stash_pop))
})
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
@@ -114,6 +114,22 @@ pub fn init(cx: &mut App) {
});
});
}
+ workspace.register_action(|workspace, action: &git::StashAll, window, cx| {
+ let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+ return;
+ };
+ panel.update(cx, |panel, cx| {
+ panel.stash_all(action, window, cx);
+ });
+ });
+ workspace.register_action(|workspace, action: &git::StashPop, window, cx| {
+ let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+ return;
+ };
+ panel.update(cx, |panel, cx| {
+ panel.stash_pop(action, window, cx);
+ });
+ });
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
@@ -420,6 +420,8 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_fetch);
client.add_entity_request_handler(Self::handle_stage);
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_commit);
client.add_entity_request_handler(Self::handle_reset);
client.add_entity_request_handler(Self::handle_show);
@@ -1696,6 +1698,48 @@ impl GitStore {
Ok(proto::Ack {})
}
+ async fn handle_stash(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::Stash>,
+ 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 entries = envelope
+ .payload
+ .paths
+ .into_iter()
+ .map(PathBuf::from)
+ .map(RepoPath::new)
+ .collect();
+
+ repository_handle
+ .update(&mut cx, |repository_handle, cx| {
+ repository_handle.stash_entries(entries, cx)
+ })?
+ .await?;
+
+ Ok(proto::Ack {})
+ }
+
+ async fn handle_stash_pop(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::StashPop>,
+ 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)?;
+
+ repository_handle
+ .update(&mut cx, |repository_handle, cx| {
+ repository_handle.stash_pop(cx)
+ })?
+ .await?;
+
+ Ok(proto::Ack {})
+ }
+
async fn handle_set_index_text(
this: Entity<Self>,
envelope: TypedEnvelope<proto::SetIndexText>,
@@ -3540,6 +3584,82 @@ impl Repository {
self.unstage_entries(to_unstage, cx)
}
+ pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
+ let to_stash = self
+ .cached_status()
+ .map(|entry| entry.repo_path.clone())
+ .collect();
+
+ self.stash_entries(to_stash, cx)
+ }
+
+ pub fn stash_entries(
+ &mut self,
+ entries: Vec<RepoPath>,
+ 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_paths(entries, environment).await,
+ RepositoryState::Remote { project_id, client } => {
+ client
+ .request(proto::Stash {
+ project_id: project_id.0,
+ repository_id: id.to_proto(),
+ paths: entries
+ .into_iter()
+ .map(|repo_path| repo_path.as_ref().to_proto())
+ .collect(),
+ })
+ .await
+ .context("sending stash request")?;
+ Ok(())
+ }
+ }
+ })
+ })?
+ .await??;
+ Ok(())
+ })
+ }
+
+ pub fn stash_pop(&mut self, 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_pop(environment).await,
+ RepositoryState::Remote { project_id, client } => {
+ client
+ .request(proto::StashPop {
+ project_id: project_id.0,
+ repository_id: id.to_proto(),
+ })
+ .await
+ .context("sending stash pop request")?;
+ Ok(())
+ }
+ }
+ })
+ })?
+ .await??;
+ Ok(())
+ })
+ }
+
pub fn commit(
&mut self,
message: SharedString,
@@ -286,6 +286,17 @@ message Unstage {
repeated string paths = 4;
}
+message Stash {
+ uint64 project_id = 1;
+ uint64 repository_id = 2;
+ repeated string paths = 3;
+}
+
+message StashPop {
+ uint64 project_id = 1;
+ uint64 repository_id = 2;
+}
+
message Commit {
uint64 project_id = 1;
reserved 2;
@@ -396,8 +396,10 @@ message Envelope {
GetDocumentColor get_document_color = 353;
GetDocumentColorResponse get_document_color_response = 354;
GetColorPresentation get_color_presentation = 355;
- GetColorPresentationResponse get_color_presentation_response = 356; // current max
+ GetColorPresentationResponse get_color_presentation_response = 356;
+ Stash stash = 357;
+ StashPop stash_pop = 358; // current max
}
reserved 87 to 88;
@@ -261,6 +261,8 @@ messages!(
(Unfollow, Foreground),
(UnshareProject, Foreground),
(Unstage, Background),
+ (Stash, Background),
+ (StashPop, Background),
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
(UpdateChannelBuffer, Foreground),
@@ -419,6 +421,8 @@ request_messages!(
(TaskContextForLocation, TaskContext),
(Test, Test),
(Unstage, Ack),
+ (Stash, Ack),
+ (StashPop, Ack),
(UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
(UpdateProject, Ack),
@@ -549,6 +553,8 @@ entity_messages!(
TaskContextForLocation,
UnshareProject,
Unstage,
+ Stash,
+ StashPop,
UpdateBuffer,
UpdateBufferFile,
UpdateDiagnosticSummary,