History

git: Add diff view for stash entries (#38280)

Continues the work from #35927 to add a git diff view for stash entries. [Screencast From 2025-09-17 19-46-01.webm](https://github.com/user-attachments/assets/ded33782-adef-4696-8e34-3665911c09c7) Stash entries are [represented as commits](https://git-scm.com/docs/git-stash#_discussion) except they have up to 3 parents: ``` .----W (this is the stash entry) / /| -----H----I | \| U ``` Where `H` is the `HEAD` commit, `I` is a commit that records the state of the index, and `U` is another commit that records untracked files (when using `git stash -u`). Given this, I modified the existing commit view struct to allow loading stash and commits entries with git sha identifier so that we can get a similar git diff view for both of them. The stash diff is generated by comparing the stash commit with its parent (`<commit>^` or `H` in the diagram) which generates the same diff as doing `git stash show -p <stash entry>`. This *can* be counter-intuitive since a user may expect the comparison to be made between the stash commit and the current commit (`HEAD`), but given that the default behavior in git cli is to compare with the stash parent, I went for that approach. Hoping to get some feedback from a Zed team member to see if they agree with this approach. Release Notes: - Add git diff view for stash entries - Add toolbar on git diff view for stash entries - Prompt before executing a destructive stash action on diff view - Fix commit view for merge commits (see #38289)

Alvaro Parker committed 1 year ago

Commit db404fc1 parent eda7a4911 files · +461 −57Browse files

Unified · Split

11 files
  1. assets/keymaps/default-linux.json +10 −1
  2. assets/keymaps/default-macos.json +11 −1
  3. assets/keymaps/default-windows.json +11 −1
  4. crates/git/src/repository.rs +4 −4
  5. crates/git_ui/src/blame_ui.rs +6 −25
  6. crates/git_ui/src/commit_tooltip.rs +2 −1
  7. crates/git_ui/src/commit_view.rs +337 −17
  8. crates/git_ui/src/git_panel.rs +2 −1
  9. crates/git_ui/src/git_ui.rs +2 −1
  10. crates/git_ui/src/stash_picker.rs +73 −5
  11. crates/zed/src/zed.rs +3 −0
assets/keymaps/default-linux.json+10−1
@@ -1080,7 +1080,8 @@ · 1079 unmodified lines
1080 {1080 {
1081 "context": "StashList || (StashList > Picker > Editor)",1081 "context": "StashList || (StashList > Picker > Editor)",
1082 "bindings": {1082 "bindings": {
1083 "ctrl-shift-backspace": "stash_picker::DropStashItem"1083 "ctrl-shift-backspace": "stash_picker::DropStashItem",
1084 "ctrl-shift-v": "stash_picker::ShowStashItem"
1084 }1085 }
1085 },1086 },
1086 {1087 {
@@ -1266,6 +1267,14 @@ · 179 unmodified lines
1266 "ctrl-pagedown": "settings_editor::FocusNextFile"1267 "ctrl-pagedown": "settings_editor::FocusNextFile"
1267 }1268 }
1268 },1269 },
1270 {
1271 "context": "StashDiff > Editor",
1272 "bindings": {
1273 "ctrl-space": "git::ApplyCurrentStash",
1274 "ctrl-shift-space": "git::PopCurrentStash",
1275 "ctrl-shift-backspace": "git::DropCurrentStash"
1276 }
1277 },
1269 {1278 {
1270 "context": "SettingsWindow > NavigationMenu",1279 "context": "SettingsWindow > NavigationMenu",
1271 "use_key_equivalents": true,1280 "use_key_equivalents": true,
assets/keymaps/default-macos.json+11−1
@@ -1153,7 +1153,8 @@ · 1152 unmodified lines
1153 "context": "StashList || (StashList > Picker > Editor)",1153 "context": "StashList || (StashList > Picker > Editor)",
1154 "use_key_equivalents": true,1154 "use_key_equivalents": true,
1155 "bindings": {1155 "bindings": {
1156 "ctrl-shift-backspace": "stash_picker::DropStashItem"1156 "ctrl-shift-backspace": "stash_picker::DropStashItem",
1157 "ctrl-shift-v": "stash_picker::ShowStashItem"
1157 }1158 }
1158 },1159 },
1159 {1160 {
@@ -1371,6 +1372,15 @@ · 211 unmodified lines
1371 "cmd-}": "settings_editor::FocusNextFile"1372 "cmd-}": "settings_editor::FocusNextFile"
1372 }1373 }
1373 },1374 },
1375 {
1376 "context": "StashDiff > Editor",
1377 "use_key_equivalents": true,
1378 "bindings": {
1379 "ctrl-space": "git::ApplyCurrentStash",
1380 "ctrl-shift-space": "git::PopCurrentStash",
1381 "ctrl-shift-backspace": "git::DropCurrentStash"
1382 }
1383 },
1374 {1384 {
1375 "context": "SettingsWindow > NavigationMenu",1385 "context": "SettingsWindow > NavigationMenu",
1376 "use_key_equivalents": true,1386 "use_key_equivalents": true,
assets/keymaps/default-windows.json+11−1
@@ -1106,7 +1106,8 @@ · 1105 unmodified lines
1106 "context": "StashList || (StashList > Picker > Editor)",1106 "context": "StashList || (StashList > Picker > Editor)",
1107 "use_key_equivalents": true,1107 "use_key_equivalents": true,
1108 "bindings": {1108 "bindings": {
1109 "ctrl-shift-backspace": "stash_picker::DropStashItem"1109 "ctrl-shift-backspace": "stash_picker::DropStashItem",
1110 "ctrl-shift-v": "stash_picker::ShowStashItem"
1110 }1111 }
1111 },1112 },
1112 {1113 {
@@ -1294,6 +1295,15 @@ · 181 unmodified lines
1294 "ctrl-pagedown": "settings_editor::FocusNextFile"1295 "ctrl-pagedown": "settings_editor::FocusNextFile"
1295 }1296 }
1296 },1297 },
1298 {
1299 "context": "StashDiff > Editor",
1300 "use_key_equivalents": true,
1301 "bindings": {
1302 "ctrl-space": "git::ApplyCurrentStash",
1303 "ctrl-shift-space": "git::PopCurrentStash",
1304 "ctrl-shift-backspace": "git::DropCurrentStash"
1305 }
1306 },
1297 {1307 {
1298 "context": "SettingsWindow > NavigationMenu",1308 "context": "SettingsWindow > NavigationMenu",
1299 "use_key_equivalents": true,1309 "use_key_equivalents": true,
crates/git/src/repository.rs+4−4
@@ -693,10 +693,11 @@ impl GitRepository for RealGitRepository { · 692 unmodified lines
693 .args([693 .args([
694 "--no-optional-locks",694 "--no-optional-locks",
695 "show",695 "show",
696 "--format=%P",696 "--format=",
697 "-z",697 "-z",
698 "--no-renames",698 "--no-renames",
699 "--name-status",699 "--name-status",
700 "--first-parent",
700 ])701 ])
701 .arg(&commit)702 .arg(&commit)
702 .stdin(Stdio::null())703 .stdin(Stdio::null())
@@ -707,9 +708,8 @@ impl GitRepository for RealGitRepository { · 4 unmodified lines
707 .context("starting git show process")?;708 .context("starting git show process")?;
708 709
709 let show_stdout = String::from_utf8_lossy(&show_output.stdout);710 let show_stdout = String::from_utf8_lossy(&show_output.stdout);
710 let mut lines = show_stdout.split('\n');711 let changes = parse_git_diff_name_status(&show_stdout);
711 let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0');712 let parent_sha = format!("{}^", commit);
712 let changes = parse_git_diff_name_status(lines.next().unwrap_or(""));
713 713
714 let mut cat_file_process = util::command::new_smol_command(&git_binary_path)714 let mut cat_file_process = util::command::new_smol_command(&git_binary_path)
715 .current_dir(&working_directory)715 .current_dir(&working_directory)
crates/git_ui/src/blame_ui.rs+6−25
@@ -98,25 +98,10 @@ impl BlameRenderer for GitBlameRenderer { · 97 unmodified lines
98 let workspace = workspace.clone();98 let workspace = workspace.clone();
99 move |_, window, cx| {99 move |_, window, cx| {
100 CommitView::open(100 CommitView::open(
101 CommitSummary {101 blame_entry.sha.to_string(),
102 sha: blame_entry.sha.to_string().into(),
103 subject: blame_entry
104 .summary
105 .clone()
106 .unwrap_or_default()
107 .into(),
108 commit_timestamp: blame_entry
109 .committer_time
110 .unwrap_or_default(),
111 author_name: blame_entry
112 .committer_name
113 .clone()
114 .unwrap_or_default()
115 .into(),
116 has_parent: true,
117 },
118 repository.downgrade(),102 repository.downgrade(),
119 workspace.clone(),103 workspace.clone(),
104 None,
120 window,105 window,
121 cx,106 cx,
122 )107 )
@@ -335,9 +320,10 @@ impl BlameRenderer for GitBlameRenderer { · 212 unmodified lines
335 .icon_size(IconSize::Small)320 .icon_size(IconSize::Small)
336 .on_click(move |_, window, cx| {321 .on_click(move |_, window, cx| {
337 CommitView::open(322 CommitView::open(
338 commit_summary.clone(),323 commit_summary.sha.clone().into(),
339 repository.downgrade(),324 repository.downgrade(),
340 workspace.clone(),325 workspace.clone(),
326 None,
341 window,327 window,
342 cx,328 cx,
343 );329 );
@@ -374,15 +360,10 @@ impl BlameRenderer for GitBlameRenderer { · 30 unmodified lines
374 cx: &mut App,360 cx: &mut App,
375 ) {361 ) {
376 CommitView::open(362 CommitView::open(
377 CommitSummary {363 blame_entry.sha.to_string(),
378 sha: blame_entry.sha.to_string().into(),
379 subject: blame_entry.summary.clone().unwrap_or_default().into(),
380 commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
381 author_name: blame_entry.committer_name.unwrap_or_default().into(),
382 has_parent: true,
383 },
384 repository.downgrade(),364 repository.downgrade(),
385 workspace,365 workspace,
366 None,
386 window,367 window,
387 cx,368 cx,
388 )369 )
crates/git_ui/src/commit_tooltip.rs+2−1
@@ -318,9 +318,10 @@ impl Render for CommitTooltip { · 317 unmodified lines
318 .on_click(318 .on_click(
319 move |_, window, cx| {319 move |_, window, cx| {
320 CommitView::open(320 CommitView::open(
321 commit_summary.clone(),321 commit_summary.sha.to_string(),
322 repo.downgrade(),322 repo.downgrade(),
323 workspace.clone(),323 workspace.clone(),
324 None,
324 window,325 window,
325 cx,326 cx,
326 );327 );
crates/git_ui/src/commit_view.rs+337−17
@@ -1,10 +1,11 @@
1use anyhow::{Context as _, Result};1use anyhow::{Context as _, Result};
2use buffer_diff::{BufferDiff, BufferDiffSnapshot};2use buffer_diff::{BufferDiff, BufferDiffSnapshot};
3use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};3use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
4use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};4use git::repository::{CommitDetails, CommitDiff, RepoPath};
5use gpui::{5use gpui::{
6 AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,6 Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
7 FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window,7 Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity,
8 Window, actions,
8};9};
9use language::{10use language::{
10 Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,11 Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
@@ -18,17 +19,42 @@ use std::{ · 7 unmodified lines
18 path::PathBuf,19 path::PathBuf,
19 sync::Arc,20 sync::Arc,
20};21};
21use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};22use ui::{
23 Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*,
24};
22use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};25use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
23use workspace::{26use workspace::{
24 Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,27 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
28 Workspace,
25 item::{BreadcrumbText, ItemEvent, TabContentParams},29 item::{BreadcrumbText, ItemEvent, TabContentParams},
30 notifications::NotifyTaskExt,
31 pane::SaveIntent,
26 searchable::SearchableItemHandle,32 searchable::SearchableItemHandle,
27};33};
28 34
35use crate::git_panel::GitPanel;
36
37actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
38
39pub fn init(cx: &mut App) {
40 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
41 register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| {
42 toolbar.apply_stash(window, cx);
43 });
44 register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| {
45 toolbar.remove_stash(window, cx);
46 });
47 register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| {
48 toolbar.pop_stash(window, cx);
49 });
50 })
51 .detach();
52}
53
29pub struct CommitView {54pub struct CommitView {
30 commit: CommitDetails,55 commit: CommitDetails,
31 editor: Entity<Editor>,56 editor: Entity<Editor>,
57 stash: Option<usize>,
32 multibuffer: Entity<MultiBuffer>,58 multibuffer: Entity<MultiBuffer>,
33}59}
34 60
@@ -48,17 +74,18 @@ const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; · 13 unmodified lines
48 74
49impl CommitView {75impl CommitView {
50 pub fn open(76 pub fn open(
51 commit: CommitSummary,77 commit_sha: String,
52 repo: WeakEntity<Repository>,78 repo: WeakEntity<Repository>,
53 workspace: WeakEntity<Workspace>,79 workspace: WeakEntity<Workspace>,
80 stash: Option<usize>,
54 window: &mut Window,81 window: &mut Window,
55 cx: &mut App,82 cx: &mut App,
56 ) {83 ) {
57 let commit_diff = repo84 let commit_diff = repo
58 .update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string()))85 .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
59 .ok();86 .ok();
60 let commit_details = repo87 let commit_details = repo
61 .update(cx, |repo, _| repo.show(commit.sha.to_string()))88 .update(cx, |repo, _| repo.show(commit_sha.clone()))
62 .ok();89 .ok();
63 90
64 window91 window
@@ -77,6 +104,7 @@ impl CommitView { · 12 unmodified lines
77 commit_diff,104 commit_diff,
78 repo,105 repo,
79 project.clone(),106 project.clone(),
107 stash,
80 window,108 window,
81 cx,109 cx,
82 )110 )
@@ -87,7 +115,7 @@ impl CommitView { · 4 unmodified lines
87 let ix = pane.items().position(|item| {115 let ix = pane.items().position(|item| {
88 let commit_view = item.downcast::<CommitView>();116 let commit_view = item.downcast::<CommitView>();
89 commit_view117 commit_view
90 .is_some_and(|view| view.read(cx).commit.sha == commit.sha)118 .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
91 });119 });
92 if let Some(ix) = ix {120 if let Some(ix) = ix {
93 pane.activate_item(ix, true, true, window, cx);121 pane.activate_item(ix, true, true, window, cx);
@@ -106,6 +134,7 @@ impl CommitView { · 12 unmodified lines
106 commit_diff: CommitDiff,134 commit_diff: CommitDiff,
107 repository: Entity<Repository>,135 repository: Entity<Repository>,
108 project: Entity<Project>,136 project: Entity<Project>,
137 stash: Option<usize>,
109 window: &mut Window,138 window: &mut Window,
110 cx: &mut Context<Self>,139 cx: &mut Context<Self>,
111 ) -> Self {140 ) -> Self {
@@ -127,10 +156,13 @@ impl CommitView { · 15 unmodified lines
127 156
128 let mut metadata_buffer_id = None;157 let mut metadata_buffer_id = None;
129 if let Some(worktree_id) = first_worktree_id {158 if let Some(worktree_id) = first_worktree_id {
159 let title = if let Some(stash) = stash {
160 format!("stash@{{{}}}", stash)
161 } else {
162 format!("commit {}", commit.sha)
163 };
130 let file = Arc::new(CommitMetadataFile {164 let file = Arc::new(CommitMetadataFile {
131 title: RelPath::unix(&format!("commit {}", commit.sha))165 title: RelPath::unix(&title).unwrap().into(),
132 .unwrap()
133 .into(),
134 worktree_id,166 worktree_id,
135 });167 });
136 let buffer = cx.new(|cx| {168 let buffer = cx.new(|cx| {
@@ -138,7 +170,7 @@ impl CommitView { · 1 unmodified line
138 ReplicaId::LOCAL,170 ReplicaId::LOCAL,
139 cx.entity_id().as_non_zero_u64().into(),171 cx.entity_id().as_non_zero_u64().into(),
140 LineEnding::default(),172 LineEnding::default(),
141 format_commit(&commit).into(),173 format_commit(&commit, stash.is_some()).into(),
142 );174 );
143 metadata_buffer_id = Some(buffer.remote_id());175 metadata_buffer_id = Some(buffer.remote_id());
144 Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)176 Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
@@ -211,6 +243,7 @@ impl CommitView { · 66 unmodified lines
211 commit,243 commit,
212 editor,244 editor,
213 multibuffer,245 multibuffer,
246 stash,
214 }247 }
215 }248 }
216}249}
@@ -369,9 +402,13 @@ async fn build_buffer_diff( · 152 unmodified lines
369 })402 })
370}403}
371 404
372fn format_commit(commit: &CommitDetails) -> String {405fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
373 let mut result = String::new();406 let mut result = String::new();
374 writeln!(&mut result, "commit {}", commit.sha).unwrap();407 if is_stash {
408 writeln!(&mut result, "stash commit {}", commit.sha).unwrap();
409 } else {
410 writeln!(&mut result, "commit {}", commit.sha).unwrap();
411 }
375 writeln!(412 writeln!(
376 &mut result,413 &mut result,
377 "Author: {} <{}>",414 "Author: {} <{}>",
@@ -538,13 +575,296 @@ impl Item for CommitView { · 160 unmodified lines
538 editor,575 editor,
539 multibuffer,576 multibuffer,
540 commit: self.commit.clone(),577 commit: self.commit.clone(),
578 stash: self.stash,
541 }579 }
542 }))580 }))
543 }581 }
544}582}
545 583
546impl Render for CommitView {584impl Render for CommitView {
547 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {585 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
548 self.editor.clone()586 let is_stash = self.stash.is_some();
587 div()
588 .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
589 .bg(cx.theme().colors().editor_background)
590 .flex()
591 .items_center()
592 .justify_center()
593 .size_full()
594 .child(self.editor.clone())
595 }
596}
597
598pub struct CommitViewToolbar {
599 commit_view: Option<WeakEntity<CommitView>>,
600 workspace: WeakEntity<Workspace>,
601}
602
603impl CommitViewToolbar {
604 pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
605 Self {
606 commit_view: None,
607 workspace: workspace.weak_handle(),
608 }
609 }
610
611 fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
612 self.commit_view.as_ref()?.upgrade()
613 }
614
615 async fn close_commit_view(
616 commit_view: Entity<CommitView>,
617 workspace: WeakEntity<Workspace>,
618 cx: &mut AsyncWindowContext,
619 ) -> anyhow::Result<()> {
620 workspace
621 .update_in(cx, |workspace, window, cx| {
622 let active_pane = workspace.active_pane();
623 let commit_view_id = commit_view.entity_id();
624 active_pane.update(cx, |pane, cx| {
625 pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
626 })
627 })?
628 .await?;
629 anyhow::Ok(())
630 }
631
632 fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
633 self.stash_action(
634 "Apply",
635 window,
636 cx,
637 async move |repository, sha, stash, commit_view, workspace, cx| {
638 let result = repository.update(cx, |repo, cx| {
639 if !stash_matches_index(&sha, stash, repo) {
640 return Err(anyhow::anyhow!("Stash has changed, not applying"));
641 }
642 Ok(repo.stash_apply(Some(stash), cx))
643 })?;
644
645 match result {
646 Ok(task) => task.await?,
647 Err(err) => {
648 Self::close_commit_view(commit_view, workspace, cx).await?;
649 return Err(err);
650 }
651 };
652 Self::close_commit_view(commit_view, workspace, cx).await?;
653 anyhow::Ok(())
654 },
655 );
656 }
657
658 fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
659 self.stash_action(
660 "Pop",
661 window,
662 cx,
663 async move |repository, sha, stash, commit_view, workspace, cx| {
664 let result = repository.update(cx, |repo, cx| {
665 if !stash_matches_index(&sha, stash, repo) {
666 return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
667 }
668 Ok(repo.stash_pop(Some(stash), cx))
669 })?;
670
671 match result {
672 Ok(task) => task.await?,
673 Err(err) => {
674 Self::close_commit_view(commit_view, workspace, cx).await?;
675 return Err(err);
676 }
677 };
678 Self::close_commit_view(commit_view, workspace, cx).await?;
679 anyhow::Ok(())
680 },
681 );
682 }
683
684 fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
685 self.stash_action(
686 "Drop",
687 window,
688 cx,
689 async move |repository, sha, stash, commit_view, workspace, cx| {
690 let result = repository.update(cx, |repo, cx| {
691 if !stash_matches_index(&sha, stash, repo) {
692 return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
693 }
694 Ok(repo.stash_drop(Some(stash), cx))
695 })?;
696
697 match result {
698 Ok(task) => task.await??,
699 Err(err) => {
700 Self::close_commit_view(commit_view, workspace, cx).await?;
701 return Err(err);
702 }
703 };
704 Self::close_commit_view(commit_view, workspace, cx).await?;
705 anyhow::Ok(())
706 },
707 );
708 }
709
710 fn stash_action<AsyncFn>(
711 &mut self,
712 str_action: &str,
713 window: &mut Window,
714 cx: &mut Context<Self>,
715 callback: AsyncFn,
716 ) where
717 AsyncFn: AsyncFnOnce(
718 Entity<Repository>,
719 &SharedString,
720 usize,
721 Entity<CommitView>,
722 WeakEntity<Workspace>,
723 &mut AsyncWindowContext,
724 ) -> anyhow::Result<()>
725 + 'static,
726 {
727 let Some(commit_view) = self.commit_view(cx) else {
728 return;
729 };
730 let Some(stash) = commit_view.read(cx).stash else {
731 return;
732 };
733 let sha = commit_view.read(cx).commit.sha.clone();
734 let answer = window.prompt(
735 PromptLevel::Info,
736 &format!("{} stash@{{{}}}?", str_action, stash),
737 None,
738 &[str_action, "Cancel"],
739 cx,
740 );
741
742 let workspace = self.workspace.clone();
743 cx.spawn_in(window, async move |_, cx| {
744 if answer.await != Ok(0) {
745 return anyhow::Ok(());
746 }
747 let repo = workspace.update(cx, |workspace, cx| {
748 workspace
749 .panel::<GitPanel>(cx)
750 .and_then(|p| p.read(cx).active_repository.clone())
751 })?;
752
753 let Some(repo) = repo else {
754 return Ok(());
755 };
756 callback(repo, &sha, stash, commit_view, workspace, cx).await?;
757 anyhow::Ok(())
758 })
759 .detach_and_notify_err(window, cx);
760 }
761}
762
763impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
764
765impl ToolbarItemView for CommitViewToolbar {
766 fn set_active_pane_item(
767 &mut self,
768 active_pane_item: Option<&dyn ItemHandle>,
769 _: &mut Window,
770 cx: &mut Context<Self>,
771 ) -> ToolbarItemLocation {
772 if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
773 && entity.read(cx).stash.is_some()
774 {
775 self.commit_view = Some(entity.downgrade());
776 return ToolbarItemLocation::PrimaryRight;
777 }
778 ToolbarItemLocation::Hidden
779 }
780
781 fn pane_focus_update(
782 &mut self,
783 _pane_focused: bool,
784 _window: &mut Window,
785 _cx: &mut Context<Self>,
786 ) {
787 }
788}
789
790impl Render for CommitViewToolbar {
791 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
792 let Some(commit_view) = self.commit_view(cx) else {
793 return div();
794 };
795
796 let is_stash = commit_view.read(cx).stash.is_some();
797 if !is_stash {
798 return div();
799 }
800
801 let focus_handle = commit_view.focus_handle(cx);
802
803 h_group_xl().my_neg_1().py_1().items_center().child(
804 h_group_sm()
805 .child(
806 Button::new("apply-stash", "Apply")
807 .tooltip(Tooltip::for_action_title_in(
808 "Apply current stash",
809 &ApplyCurrentStash,
810 &focus_handle,
811 ))
812 .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
813 )
814 .child(
815 Button::new("pop-stash", "Pop")
816 .tooltip(Tooltip::for_action_title_in(
817 "Pop current stash",
818 &PopCurrentStash,
819 &focus_handle,
820 ))
821 .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
822 )
823 .child(
824 Button::new("remove-stash", "Remove")
825 .icon(IconName::Trash)
826 .tooltip(Tooltip::for_action_title_in(
827 "Remove current stash",
828 &DropCurrentStash,
829 &focus_handle,
830 ))
831 .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
832 ),
833 )
834 }
835}
836
837fn register_workspace_action<A: Action>(
838 workspace: &mut Workspace,
839 callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
840) {
841 workspace.register_action(move |workspace, action: &A, window, cx| {
842 if workspace.has_active_modal(window, cx) {
843 cx.propagate();
844 return;
845 }
846
847 workspace.active_pane().update(cx, |pane, cx| {
848 pane.toolbar().update(cx, move |workspace, cx| {
849 if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
850 toolbar.update(cx, move |toolbar, cx| {
851 callback(toolbar, action, window, cx);
852 cx.notify();
853 });
854 }
855 });
856 })
857 });
858}
859
860fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
861 match repo
862 .cached_stash()
863 .entries
864 .iter()
865 .find(|entry| entry.index == index)
866 {
867 Some(entry) => entry.oid.to_string() == sha,
868 None => false,
549 }869 }
550}870}
crates/git_ui/src/git_panel.rs+2−1
@@ -3611,9 +3611,10 @@ impl GitPanel { · 3610 unmodified lines
3611 let repo = active_repository.downgrade();3611 let repo = active_repository.downgrade();
3612 move |_, window, cx| {3612 move |_, window, cx| {
3613 CommitView::open(3613 CommitView::open(
3614 commit.clone(),3614 commit.sha.to_string(),
3615 repo.clone(),3615 repo.clone(),
3616 workspace.clone(),3616 workspace.clone(),
3617 None,
3617 window,3618 window,
3618 cx,3619 cx,
3619 );3620 );
crates/git_ui/src/git_ui.rs+2−1
@@ -34,7 +34,7 @@ mod askpass_modal; · 33 unmodified lines
34pub mod branch_picker;34pub mod branch_picker;
35mod commit_modal;35mod commit_modal;
36pub mod commit_tooltip;36pub mod commit_tooltip;
37mod commit_view;37pub mod commit_view;
38mod conflict_view;38mod conflict_view;
39pub mod file_diff_view;39pub mod file_diff_view;
40pub mod git_panel;40pub mod git_panel;
@@ -59,6 +59,7 @@ pub fn init(cx: &mut App) { · 18 unmodified lines
59 GitPanelSettings::register(cx);59 GitPanelSettings::register(cx);
60 60
61 editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);61 editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
62 commit_view::init(cx);
62 63
63 cx.observe_new(|editor: &mut Editor, _, cx| {64 cx.observe_new(|editor: &mut Editor, _, cx| {
64 conflict_view::register_editor(editor, editor.buffer().clone(), cx);65 conflict_view::register_editor(editor, editor.buffer().clone(), cx);
crates/git_ui/src/stash_picker.rs+73−5
@@ -5,18 +5,21 @@ use git::stash::StashEntry; · 4 unmodified lines
5use gpui::{5use gpui::{
6 Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,6 Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
7 InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,7 InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
8 SharedString, Styled, Subscription, Task, Window, actions, rems,8 SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg,
9};9};
10use picker::{Picker, PickerDelegate};10use picker::{Picker, PickerDelegate};
11use project::git_store::{Repository, RepositoryEvent};11use project::git_store::{Repository, RepositoryEvent};
12use std::sync::Arc;12use std::sync::Arc;
13use time::{OffsetDateTime, UtcOffset};13use time::{OffsetDateTime, UtcOffset};
14use time_format;14use time_format;
15use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};15use ui::{
16 ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*,
17};
16use util::ResultExt;18use util::ResultExt;
17use workspace::notifications::DetachAndPromptErr;19use workspace::notifications::DetachAndPromptErr;
18use workspace::{ModalView, Workspace};20use workspace::{ModalView, Workspace};
19 21
22use crate::commit_view::CommitView;
20use crate::stash_picker;23use crate::stash_picker;
21 24
22actions!(25actions!(
@@ -24,6 +27,8 @@ actions!( · 1 unmodified line
24 [27 [
25 /// Drop the selected stash entry.28 /// Drop the selected stash entry.
26 DropStashItem,29 DropStashItem,
30 /// Show the diff view of the selected stash entry.
31 ShowStashItem,
27 ]32 ]
28);33);
29 34
@@ -38,8 +43,9 @@ pub fn open( · 8 unmodified lines
38 cx: &mut Context<Workspace>,43 cx: &mut Context<Workspace>,
39) {44) {
40 let repository = workspace.project().read(cx).active_repository(cx);45 let repository = workspace.project().read(cx).active_repository(cx);
46 let weak_workspace = workspace.weak_handle();
41 workspace.toggle_modal(window, cx, |window, cx| {47 workspace.toggle_modal(window, cx, |window, cx| {
42 StashList::new(repository, rems(34.), window, cx)48 StashList::new(repository, weak_workspace, rems(34.), window, cx)
43 })49 })
44}50}
45 51
@@ -53,6 +59,7 @@ pub struct StashList { · 7 unmodified lines
53impl StashList {59impl StashList {
54 fn new(60 fn new(
55 repository: Option<Entity<Repository>>,61 repository: Option<Entity<Repository>>,
62 workspace: WeakEntity<Workspace>,
56 width: Rems,63 width: Rems,
57 window: &mut Window,64 window: &mut Window,
58 cx: &mut Context<Self>,65 cx: &mut Context<Self>,
@@ -98,7 +105,7 @@ impl StashList { · 39 unmodified lines
98 })105 })
99 .detach_and_log_err(cx);106 .detach_and_log_err(cx);
100 107
101 let delegate = StashListDelegate::new(repository, window, cx);108 let delegate = StashListDelegate::new(repository, workspace, window, cx);
102 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));109 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
103 let picker_focus_handle = picker.focus_handle(cx);110 let picker_focus_handle = picker.focus_handle(cx);
104 picker.update(cx, |picker, _| {111 picker.update(cx, |picker, _| {
@@ -131,6 +138,20 @@ impl StashList { · 26 unmodified lines
131 cx.notify();138 cx.notify();
132 }139 }
133 140
141 fn handle_show_stash(
142 &mut self,
143 _: &ShowStashItem,
144 window: &mut Window,
145 cx: &mut Context<Self>,
146 ) {
147 self.picker.update(cx, |picker, cx| {
148 picker
149 .delegate
150 .show_stash_at(picker.delegate.selected_index(), window, cx);
151 });
152 cx.notify();
153 }
154
134 fn handle_modifiers_changed(155 fn handle_modifiers_changed(
135 &mut self,156 &mut self,
136 ev: &ModifiersChangedEvent,157 ev: &ModifiersChangedEvent,
@@ -157,6 +178,7 @@ impl Render for StashList { · 20 unmodified lines
157 .w(self.width)178 .w(self.width)
158 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))179 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
159 .on_action(cx.listener(Self::handle_drop_stash))180 .on_action(cx.listener(Self::handle_drop_stash))
181 .on_action(cx.listener(Self::handle_show_stash))
160 .child(self.picker.clone())182 .child(self.picker.clone())
161 }183 }
162}184}
@@ -172,6 +194,7 @@ pub struct StashListDelegate { · 9 unmodified lines
172 matches: Vec<StashEntryMatch>,194 matches: Vec<StashEntryMatch>,
173 all_stash_entries: Option<Vec<StashEntry>>,195 all_stash_entries: Option<Vec<StashEntry>>,
174 repo: Option<Entity<Repository>>,196 repo: Option<Entity<Repository>>,
197 workspace: WeakEntity<Workspace>,
175 selected_index: usize,198 selected_index: usize,
176 last_query: String,199 last_query: String,
177 modifiers: Modifiers,200 modifiers: Modifiers,
@@ -182,6 +205,7 @@ pub struct StashListDelegate { · 4 unmodified lines
182impl StashListDelegate {205impl StashListDelegate {
183 fn new(206 fn new(
184 repo: Option<Entity<Repository>>,207 repo: Option<Entity<Repository>>,
208 workspace: WeakEntity<Workspace>,
185 _window: &mut Window,209 _window: &mut Window,
186 cx: &mut Context<StashList>,210 cx: &mut Context<StashList>,
187 ) -> Self {211 ) -> Self {
@@ -192,6 +216,7 @@ impl StashListDelegate { · 4 unmodified lines
192 Self {216 Self {
193 matches: vec![],217 matches: vec![],
194 repo,218 repo,
219 workspace,
195 all_stash_entries: None,220 all_stash_entries: None,
196 selected_index: 0,221 selected_index: 0,
197 last_query: Default::default(),222 last_query: Default::default(),
@@ -235,6 +260,25 @@ impl StashListDelegate { · 37 unmodified lines
235 });260 });
236 }261 }
237 262
263 fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
264 let Some(entry_match) = self.matches.get(ix) else {
265 return;
266 };
267 let stash_sha = entry_match.entry.oid.to_string();
268 let stash_index = entry_match.entry.index;
269 let Some(repo) = self.repo.clone() else {
270 return;
271 };
272 CommitView::open(
273 stash_sha,
274 repo.downgrade(),
275 self.workspace.clone(),
276 Some(stash_index),
277 window,
278 cx,
279 );
280 }
281
238 fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {282 fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
239 let Some(repo) = self.repo.clone() else {283 let Some(repo) = self.repo.clone() else {
240 return;284 return;
@@ -390,7 +434,7 @@ impl PickerDelegate for StashListDelegate { · 149 unmodified lines
390 ix: usize,434 ix: usize,
391 selected: bool,435 selected: bool,
392 _window: &mut Window,436 _window: &mut Window,
393 _cx: &mut Context<Picker<Self>>,437 cx: &mut Context<Picker<Self>>,
394 ) -> Option<Self::ListItem> {438 ) -> Option<Self::ListItem> {
395 let entry_match = &self.matches[ix];439 let entry_match = &self.matches[ix];
396 440
@@ -432,11 +476,35 @@ impl PickerDelegate for StashListDelegate { · 35 unmodified lines
432 .size(LabelSize::Small),476 .size(LabelSize::Small),
433 );477 );
434 478
479 let show_button = div()
480 .group("show-button-hover")
481 .child(
482 ButtonLike::new("show-button")
483 .child(
484 svg()
485 .size(IconSize::Medium.rems())
486 .flex_none()
487 .path(IconName::Eye.path())
488 .text_color(Color::Default.color(cx))
489 .group_hover("show-button-hover", |this| {
490 this.text_color(Color::Accent.color(cx))
491 })
492 .hover(|this| this.text_color(Color::Accent.color(cx))),
493 )
494 .tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem))
495 .on_click(cx.listener(move |picker, _, window, cx| {
496 cx.stop_propagation();
497 picker.delegate.show_stash_at(ix, window, cx);
498 })),
499 )
500 .into_any_element();
501
435 Some(502 Some(
436 ListItem::new(SharedString::from(format!("stash-{ix}")))503 ListItem::new(SharedString::from(format!("stash-{ix}")))
437 .inset(true)504 .inset(true)
438 .spacing(ListItemSpacing::Sparse)505 .spacing(ListItemSpacing::Sparse)
439 .toggle_state(selected)506 .toggle_state(selected)
507 .end_slot(show_button)
440 .child(508 .child(
441 v_flex()509 v_flex()
442 .w_full()510 .w_full()
crates/zed/src/zed.rs+3−0
@@ -25,6 +25,7 @@ use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; · 24 unmodified lines
25use fs::Fs;25use fs::Fs;
26use futures::future::Either;26use futures::future::Either;
27use futures::{StreamExt, channel::mpsc, select_biased};27use futures::{StreamExt, channel::mpsc, select_biased};
28use git_ui::commit_view::CommitViewToolbar;
28use git_ui::git_panel::GitPanel;29use git_ui::git_panel::GitPanel;
29use git_ui::project_diff::ProjectDiffToolbar;30use git_ui::project_diff::ProjectDiffToolbar;
30use gpui::{31use gpui::{
@@ -1049,6 +1050,8 @@ fn initialize_pane( · 1018 unmodified lines
1049 toolbar.add_item(migration_banner, window, cx);1050 toolbar.add_item(migration_banner, window, cx);
1050 let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));1051 let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1051 toolbar.add_item(project_diff_toolbar, window, cx);1052 toolbar.add_item(project_diff_toolbar, window, cx);
1053 let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx));
1054 toolbar.add_item(commit_view_toolbar, window, cx);
1052 let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);1055 let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1053 toolbar.add_item(agent_diff_toolbar, window, cx);1056 toolbar.add_item(agent_diff_toolbar, window, cx);
1054 let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));1057 let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t