commit_view.rs

  1use anyhow::{Context as _, Result};
  2use buffer_diff::{BufferDiff, BufferDiffSnapshot};
  3use editor::{
  4    Editor, EditorEvent, MultiBuffer, MultiBufferOffset, SelectionEffects,
  5    multibuffer_context_lines,
  6};
  7use git::repository::{CommitDetails, CommitDiff, RepoPath};
  8use gpui::{
  9    Action, AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Entity,
 10    EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task, WeakEntity,
 11    Window, actions,
 12};
 13use language::{
 14    Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
 15    Point, ReplicaId, Rope, TextBuffer,
 16};
 17use multi_buffer::PathKey;
 18use project::{Project, WorktreeId, git_store::Repository};
 19use std::{
 20    any::{Any, TypeId},
 21    fmt::Write as _,
 22    path::PathBuf,
 23    sync::Arc,
 24};
 25use ui::{
 26    Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*,
 27};
 28use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
 29use workspace::{
 30    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 31    Workspace,
 32    item::{BreadcrumbText, ItemEvent, TabContentParams},
 33    notifications::NotifyTaskExt,
 34    pane::SaveIntent,
 35    searchable::SearchableItemHandle,
 36};
 37
 38use crate::git_panel::GitPanel;
 39
 40actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
 41
 42pub fn init(cx: &mut App) {
 43    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 44        register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| {
 45            toolbar.apply_stash(window, cx);
 46        });
 47        register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| {
 48            toolbar.remove_stash(window, cx);
 49        });
 50        register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| {
 51            toolbar.pop_stash(window, cx);
 52        });
 53    })
 54    .detach();
 55}
 56
 57pub struct CommitView {
 58    commit: CommitDetails,
 59    editor: Entity<Editor>,
 60    stash: Option<usize>,
 61    multibuffer: Entity<MultiBuffer>,
 62}
 63
 64struct GitBlob {
 65    path: RepoPath,
 66    worktree_id: WorktreeId,
 67    is_deleted: bool,
 68}
 69
 70struct CommitMetadataFile {
 71    title: Arc<RelPath>,
 72    worktree_id: WorktreeId,
 73}
 74
 75const COMMIT_METADATA_SORT_PREFIX: u64 = 0;
 76const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
 77
 78impl CommitView {
 79    pub fn open(
 80        commit_sha: String,
 81        repo: WeakEntity<Repository>,
 82        workspace: WeakEntity<Workspace>,
 83        stash: Option<usize>,
 84        window: &mut Window,
 85        cx: &mut App,
 86    ) {
 87        let commit_diff = repo
 88            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
 89            .ok();
 90        let commit_details = repo
 91            .update(cx, |repo, _| repo.show(commit_sha.clone()))
 92            .ok();
 93
 94        window
 95            .spawn(cx, async move |cx| {
 96                let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
 97                let commit_diff = commit_diff.log_err()?.log_err()?;
 98                let commit_details = commit_details.log_err()?.log_err()?;
 99                let repo = repo.upgrade()?;
100
101                workspace
102                    .update_in(cx, |workspace, window, cx| {
103                        let project = workspace.project();
104                        let commit_view = cx.new(|cx| {
105                            CommitView::new(
106                                commit_details,
107                                commit_diff,
108                                repo,
109                                project.clone(),
110                                stash,
111                                window,
112                                cx,
113                            )
114                        });
115
116                        let pane = workspace.active_pane();
117                        pane.update(cx, |pane, cx| {
118                            let ix = pane.items().position(|item| {
119                                let commit_view = item.downcast::<CommitView>();
120                                commit_view
121                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
122                            });
123                            if let Some(ix) = ix {
124                                pane.activate_item(ix, true, true, window, cx);
125                            } else {
126                                pane.add_item(Box::new(commit_view), true, true, None, window, cx);
127                            }
128                        })
129                    })
130                    .log_err()
131            })
132            .detach();
133    }
134
135    fn new(
136        commit: CommitDetails,
137        commit_diff: CommitDiff,
138        repository: Entity<Repository>,
139        project: Entity<Project>,
140        stash: Option<usize>,
141        window: &mut Window,
142        cx: &mut Context<Self>,
143    ) -> Self {
144        let language_registry = project.read(cx).languages().clone();
145        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
146        let editor = cx.new(|cx| {
147            let mut editor =
148                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
149            editor.disable_inline_diagnostics();
150            editor.set_expand_all_diff_hunks(cx);
151            editor
152        });
153
154        let first_worktree_id = project
155            .read(cx)
156            .worktrees(cx)
157            .next()
158            .map(|worktree| worktree.read(cx).id());
159
160        let mut metadata_buffer_id = None;
161        if let Some(worktree_id) = first_worktree_id {
162            let title = if let Some(stash) = stash {
163                format!("stash@{{{}}}", stash)
164            } else {
165                format!("commit {}", commit.sha)
166            };
167            let file = Arc::new(CommitMetadataFile {
168                title: RelPath::unix(&title).unwrap().into(),
169                worktree_id,
170            });
171            let buffer = cx.new(|cx| {
172                let buffer = TextBuffer::new_normalized(
173                    ReplicaId::LOCAL,
174                    cx.entity_id().as_non_zero_u64().into(),
175                    LineEnding::default(),
176                    format_commit(&commit, stash.is_some()).into(),
177                );
178                metadata_buffer_id = Some(buffer.remote_id());
179                Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
180            });
181            multibuffer.update(cx, |multibuffer, cx| {
182                multibuffer.set_excerpts_for_path(
183                    PathKey::with_sort_prefix(COMMIT_METADATA_SORT_PREFIX, file.title.clone()),
184                    buffer.clone(),
185                    vec![Point::zero()..buffer.read(cx).max_point()],
186                    0,
187                    cx,
188                );
189            });
190            editor.update(cx, |editor, cx| {
191                editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
192                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
193                    selections.select_ranges(vec![MultiBufferOffset(0)..MultiBufferOffset(0)]);
194                });
195            });
196        }
197
198        cx.spawn(async move |this, cx| {
199            for file in commit_diff.files {
200                let is_deleted = file.new_text.is_none();
201                let new_text = file.new_text.unwrap_or_default();
202                let old_text = file.old_text;
203                let worktree_id = repository
204                    .update(cx, |repository, cx| {
205                        repository
206                            .repo_path_to_project_path(&file.path, cx)
207                            .map(|path| path.worktree_id)
208                            .or(first_worktree_id)
209                    })?
210                    .context("project has no worktrees")?;
211                let file = Arc::new(GitBlob {
212                    path: file.path.clone(),
213                    is_deleted,
214                    worktree_id,
215                }) as Arc<dyn language::File>;
216
217                let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
218                let buffer_diff =
219                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
220
221                this.update(cx, |this, cx| {
222                    this.multibuffer.update(cx, |multibuffer, cx| {
223                        let snapshot = buffer.read(cx).snapshot();
224                        let diff = buffer_diff.read(cx);
225                        let diff_hunk_ranges = diff
226                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
227                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
228                            .collect::<Vec<_>>();
229                        let path = snapshot.file().unwrap().path().clone();
230                        let _is_newly_added = multibuffer.set_excerpts_for_path(
231                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
232                            buffer,
233                            diff_hunk_ranges,
234                            multibuffer_context_lines(cx),
235                            cx,
236                        );
237                        multibuffer.add_diff(buffer_diff, cx);
238                    });
239                })?;
240            }
241            anyhow::Ok(())
242        })
243        .detach();
244
245        Self {
246            commit,
247            editor,
248            multibuffer,
249            stash,
250        }
251    }
252}
253
254impl language::File for GitBlob {
255    fn as_local(&self) -> Option<&dyn language::LocalFile> {
256        None
257    }
258
259    fn disk_state(&self) -> DiskState {
260        if self.is_deleted {
261            DiskState::Deleted
262        } else {
263            DiskState::New
264        }
265    }
266
267    fn path_style(&self, _: &App) -> PathStyle {
268        PathStyle::Posix
269    }
270
271    fn path(&self) -> &Arc<RelPath> {
272        self.path.as_ref()
273    }
274
275    fn full_path(&self, _: &App) -> PathBuf {
276        self.path.as_std_path().to_path_buf()
277    }
278
279    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
280        self.path.file_name().unwrap()
281    }
282
283    fn worktree_id(&self, _: &App) -> WorktreeId {
284        self.worktree_id
285    }
286
287    fn to_proto(&self, _cx: &App) -> language::proto::File {
288        unimplemented!()
289    }
290
291    fn is_private(&self) -> bool {
292        false
293    }
294}
295
296impl language::File for CommitMetadataFile {
297    fn as_local(&self) -> Option<&dyn language::LocalFile> {
298        None
299    }
300
301    fn disk_state(&self) -> DiskState {
302        DiskState::New
303    }
304
305    fn path_style(&self, _: &App) -> PathStyle {
306        PathStyle::Posix
307    }
308
309    fn path(&self) -> &Arc<RelPath> {
310        &self.title
311    }
312
313    fn full_path(&self, _: &App) -> PathBuf {
314        PathBuf::from(self.title.as_unix_str().to_owned())
315    }
316
317    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
318        self.title.file_name().unwrap()
319    }
320
321    fn worktree_id(&self, _: &App) -> WorktreeId {
322        self.worktree_id
323    }
324
325    fn to_proto(&self, _: &App) -> language::proto::File {
326        unimplemented!()
327    }
328
329    fn is_private(&self) -> bool {
330        false
331    }
332}
333
334async fn build_buffer(
335    mut text: String,
336    blob: Arc<dyn File>,
337    language_registry: &Arc<language::LanguageRegistry>,
338    cx: &mut AsyncApp,
339) -> Result<Entity<Buffer>> {
340    let line_ending = LineEnding::detect(&text);
341    LineEnding::normalize(&mut text);
342    let text = Rope::from(text);
343    let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
344    let language = if let Some(language) = language {
345        language_registry
346            .load_language(&language)
347            .await
348            .ok()
349            .and_then(|e| e.log_err())
350    } else {
351        None
352    };
353    let buffer = cx.new(|cx| {
354        let buffer = TextBuffer::new_normalized(
355            ReplicaId::LOCAL,
356            cx.entity_id().as_non_zero_u64().into(),
357            line_ending,
358            text,
359        );
360        let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
361        buffer.set_language(language, cx);
362        buffer
363    })?;
364    Ok(buffer)
365}
366
367async fn build_buffer_diff(
368    mut old_text: Option<String>,
369    buffer: &Entity<Buffer>,
370    language_registry: &Arc<LanguageRegistry>,
371    cx: &mut AsyncApp,
372) -> Result<Entity<BufferDiff>> {
373    if let Some(old_text) = &mut old_text {
374        LineEnding::normalize(old_text);
375    }
376
377    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
378
379    let base_buffer = cx
380        .update(|cx| {
381            Buffer::build_snapshot(
382                old_text.as_deref().unwrap_or("").into(),
383                buffer.language().cloned(),
384                Some(language_registry.clone()),
385                cx,
386            )
387        })?
388        .await;
389
390    let diff_snapshot = cx
391        .update(|cx| {
392            BufferDiffSnapshot::new_with_base_buffer(
393                buffer.text.clone(),
394                old_text.map(Arc::new),
395                base_buffer,
396                cx,
397            )
398        })?
399        .await;
400
401    cx.new(|cx| {
402        let mut diff = BufferDiff::new(&buffer.text, cx);
403        diff.set_snapshot(diff_snapshot, &buffer.text, cx);
404        diff
405    })
406}
407
408fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
409    let mut result = String::new();
410    if is_stash {
411        writeln!(&mut result, "stash commit {}", commit.sha).unwrap();
412    } else {
413        writeln!(&mut result, "commit {}", commit.sha).unwrap();
414    }
415    writeln!(
416        &mut result,
417        "Author: {} <{}>",
418        commit.author_name, commit.author_email
419    )
420    .unwrap();
421    let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
422    writeln!(
423        &mut result,
424        "Date:   {}",
425        time_format::format_localized_timestamp(
426            time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
427            time::OffsetDateTime::now_utc(),
428            local_offset,
429            time_format::TimestampFormat::MediumAbsolute,
430        ),
431    )
432    .unwrap();
433    result.push('\n');
434    for line in commit.message.split('\n') {
435        if line.is_empty() {
436            result.push('\n');
437        } else {
438            writeln!(&mut result, "    {}", line).unwrap();
439        }
440    }
441    if result.ends_with("\n\n") {
442        result.pop();
443    }
444    result
445}
446
447impl EventEmitter<EditorEvent> for CommitView {}
448
449impl Focusable for CommitView {
450    fn focus_handle(&self, cx: &App) -> FocusHandle {
451        self.editor.focus_handle(cx)
452    }
453}
454
455impl Item for CommitView {
456    type Event = EditorEvent;
457
458    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
459        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
460    }
461
462    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
463        Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
464            .color(if params.selected {
465                Color::Default
466            } else {
467                Color::Muted
468            })
469            .into_any_element()
470    }
471
472    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
473        let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
474        let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
475        format!("{short_sha} - {subject}").into()
476    }
477
478    fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
479        let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
480        let subject = self.commit.message.split('\n').next().unwrap();
481        Some(format!("{short_sha} - {subject}").into())
482    }
483
484    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
485        Editor::to_item_events(event, f)
486    }
487
488    fn telemetry_event_text(&self) -> Option<&'static str> {
489        Some("Commit View Opened")
490    }
491
492    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
493        self.editor
494            .update(cx, |editor, cx| editor.deactivated(window, cx));
495    }
496
497    fn act_as_type<'a>(
498        &'a self,
499        type_id: TypeId,
500        self_handle: &'a Entity<Self>,
501        _: &'a App,
502    ) -> Option<gpui::AnyEntity> {
503        if type_id == TypeId::of::<Self>() {
504            Some(self_handle.clone().into())
505        } else if type_id == TypeId::of::<Editor>() {
506            Some(self.editor.clone().into())
507        } else {
508            None
509        }
510    }
511
512    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
513        Some(Box::new(self.editor.clone()))
514    }
515
516    fn for_each_project_item(
517        &self,
518        cx: &App,
519        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
520    ) {
521        self.editor.for_each_project_item(cx, f)
522    }
523
524    fn set_nav_history(
525        &mut self,
526        nav_history: ItemNavHistory,
527        _: &mut Window,
528        cx: &mut Context<Self>,
529    ) {
530        self.editor.update(cx, |editor, _| {
531            editor.set_nav_history(Some(nav_history));
532        });
533    }
534
535    fn navigate(
536        &mut self,
537        data: Box<dyn Any>,
538        window: &mut Window,
539        cx: &mut Context<Self>,
540    ) -> bool {
541        self.editor
542            .update(cx, |editor, cx| editor.navigate(data, window, cx))
543    }
544
545    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
546        ToolbarItemLocation::PrimaryLeft
547    }
548
549    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
550        self.editor.breadcrumbs(theme, cx)
551    }
552
553    fn added_to_workspace(
554        &mut self,
555        workspace: &mut Workspace,
556        window: &mut Window,
557        cx: &mut Context<Self>,
558    ) {
559        self.editor.update(cx, |editor, cx| {
560            editor.added_to_workspace(workspace, window, cx)
561        });
562    }
563
564    fn can_split(&self) -> bool {
565        true
566    }
567
568    fn clone_on_split(
569        &self,
570        _workspace_id: Option<workspace::WorkspaceId>,
571        window: &mut Window,
572        cx: &mut Context<Self>,
573    ) -> Task<Option<Entity<Self>>>
574    where
575        Self: Sized,
576    {
577        Task::ready(Some(cx.new(|cx| {
578            let editor = cx.new(|cx| {
579                self.editor
580                    .update(cx, |editor, cx| editor.clone(window, cx))
581            });
582            let multibuffer = editor.read(cx).buffer().clone();
583            Self {
584                editor,
585                multibuffer,
586                commit: self.commit.clone(),
587                stash: self.stash,
588            }
589        })))
590    }
591}
592
593impl Render for CommitView {
594    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
595        let is_stash = self.stash.is_some();
596        div()
597            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
598            .bg(cx.theme().colors().editor_background)
599            .flex()
600            .items_center()
601            .justify_center()
602            .size_full()
603            .child(self.editor.clone())
604    }
605}
606
607pub struct CommitViewToolbar {
608    commit_view: Option<WeakEntity<CommitView>>,
609    workspace: WeakEntity<Workspace>,
610}
611
612impl CommitViewToolbar {
613    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
614        Self {
615            commit_view: None,
616            workspace: workspace.weak_handle(),
617        }
618    }
619
620    fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
621        self.commit_view.as_ref()?.upgrade()
622    }
623
624    async fn close_commit_view(
625        commit_view: Entity<CommitView>,
626        workspace: WeakEntity<Workspace>,
627        cx: &mut AsyncWindowContext,
628    ) -> anyhow::Result<()> {
629        workspace
630            .update_in(cx, |workspace, window, cx| {
631                let active_pane = workspace.active_pane();
632                let commit_view_id = commit_view.entity_id();
633                active_pane.update(cx, |pane, cx| {
634                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
635                })
636            })?
637            .await?;
638        anyhow::Ok(())
639    }
640
641    fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
642        self.stash_action(
643            "Apply",
644            window,
645            cx,
646            async move |repository, sha, stash, commit_view, workspace, cx| {
647                let result = repository.update(cx, |repo, cx| {
648                    if !stash_matches_index(&sha, stash, repo) {
649                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
650                    }
651                    Ok(repo.stash_apply(Some(stash), cx))
652                })?;
653
654                match result {
655                    Ok(task) => task.await?,
656                    Err(err) => {
657                        Self::close_commit_view(commit_view, workspace, cx).await?;
658                        return Err(err);
659                    }
660                };
661                Self::close_commit_view(commit_view, workspace, cx).await?;
662                anyhow::Ok(())
663            },
664        );
665    }
666
667    fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
668        self.stash_action(
669            "Pop",
670            window,
671            cx,
672            async move |repository, sha, stash, commit_view, workspace, cx| {
673                let result = repository.update(cx, |repo, cx| {
674                    if !stash_matches_index(&sha, stash, repo) {
675                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
676                    }
677                    Ok(repo.stash_pop(Some(stash), cx))
678                })?;
679
680                match result {
681                    Ok(task) => task.await?,
682                    Err(err) => {
683                        Self::close_commit_view(commit_view, workspace, cx).await?;
684                        return Err(err);
685                    }
686                };
687                Self::close_commit_view(commit_view, workspace, cx).await?;
688                anyhow::Ok(())
689            },
690        );
691    }
692
693    fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
694        self.stash_action(
695            "Drop",
696            window,
697            cx,
698            async move |repository, sha, stash, commit_view, workspace, cx| {
699                let result = repository.update(cx, |repo, cx| {
700                    if !stash_matches_index(&sha, stash, repo) {
701                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
702                    }
703                    Ok(repo.stash_drop(Some(stash), cx))
704                })?;
705
706                match result {
707                    Ok(task) => task.await??,
708                    Err(err) => {
709                        Self::close_commit_view(commit_view, workspace, cx).await?;
710                        return Err(err);
711                    }
712                };
713                Self::close_commit_view(commit_view, workspace, cx).await?;
714                anyhow::Ok(())
715            },
716        );
717    }
718
719    fn stash_action<AsyncFn>(
720        &mut self,
721        str_action: &str,
722        window: &mut Window,
723        cx: &mut Context<Self>,
724        callback: AsyncFn,
725    ) where
726        AsyncFn: AsyncFnOnce(
727                Entity<Repository>,
728                &SharedString,
729                usize,
730                Entity<CommitView>,
731                WeakEntity<Workspace>,
732                &mut AsyncWindowContext,
733            ) -> anyhow::Result<()>
734            + 'static,
735    {
736        let Some(commit_view) = self.commit_view(cx) else {
737            return;
738        };
739        let Some(stash) = commit_view.read(cx).stash else {
740            return;
741        };
742        let sha = commit_view.read(cx).commit.sha.clone();
743        let answer = window.prompt(
744            PromptLevel::Info,
745            &format!("{} stash@{{{}}}?", str_action, stash),
746            None,
747            &[str_action, "Cancel"],
748            cx,
749        );
750
751        let workspace = self.workspace.clone();
752        cx.spawn_in(window, async move |_, cx| {
753            if answer.await != Ok(0) {
754                return anyhow::Ok(());
755            }
756            let repo = workspace.update(cx, |workspace, cx| {
757                workspace
758                    .panel::<GitPanel>(cx)
759                    .and_then(|p| p.read(cx).active_repository.clone())
760            })?;
761
762            let Some(repo) = repo else {
763                return Ok(());
764            };
765            callback(repo, &sha, stash, commit_view, workspace, cx).await?;
766            anyhow::Ok(())
767        })
768        .detach_and_notify_err(window, cx);
769    }
770}
771
772impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
773
774impl ToolbarItemView for CommitViewToolbar {
775    fn set_active_pane_item(
776        &mut self,
777        active_pane_item: Option<&dyn ItemHandle>,
778        _: &mut Window,
779        cx: &mut Context<Self>,
780    ) -> ToolbarItemLocation {
781        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
782            && entity.read(cx).stash.is_some()
783        {
784            self.commit_view = Some(entity.downgrade());
785            return ToolbarItemLocation::PrimaryRight;
786        }
787        ToolbarItemLocation::Hidden
788    }
789
790    fn pane_focus_update(
791        &mut self,
792        _pane_focused: bool,
793        _window: &mut Window,
794        _cx: &mut Context<Self>,
795    ) {
796    }
797}
798
799impl Render for CommitViewToolbar {
800    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
801        let Some(commit_view) = self.commit_view(cx) else {
802            return div();
803        };
804
805        let is_stash = commit_view.read(cx).stash.is_some();
806        if !is_stash {
807            return div();
808        }
809
810        let focus_handle = commit_view.focus_handle(cx);
811
812        h_group_xl().my_neg_1().py_1().items_center().child(
813            h_group_sm()
814                .child(
815                    Button::new("apply-stash", "Apply")
816                        .tooltip(Tooltip::for_action_title_in(
817                            "Apply current stash",
818                            &ApplyCurrentStash,
819                            &focus_handle,
820                        ))
821                        .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
822                )
823                .child(
824                    Button::new("pop-stash", "Pop")
825                        .tooltip(Tooltip::for_action_title_in(
826                            "Pop current stash",
827                            &PopCurrentStash,
828                            &focus_handle,
829                        ))
830                        .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
831                )
832                .child(
833                    Button::new("remove-stash", "Remove")
834                        .icon(IconName::Trash)
835                        .tooltip(Tooltip::for_action_title_in(
836                            "Remove current stash",
837                            &DropCurrentStash,
838                            &focus_handle,
839                        ))
840                        .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
841                ),
842        )
843    }
844}
845
846fn register_workspace_action<A: Action>(
847    workspace: &mut Workspace,
848    callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
849) {
850    workspace.register_action(move |workspace, action: &A, window, cx| {
851        if workspace.has_active_modal(window, cx) {
852            cx.propagate();
853            return;
854        }
855
856        workspace.active_pane().update(cx, |pane, cx| {
857            pane.toolbar().update(cx, move |workspace, cx| {
858                if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
859                    toolbar.update(cx, move |toolbar, cx| {
860                        callback(toolbar, action, window, cx);
861                        cx.notify();
862                    });
863                }
864            });
865        })
866    });
867}
868
869fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
870    match repo
871        .cached_stash()
872        .entries
873        .iter()
874        .find(|entry| entry.index == index)
875    {
876        Some(entry) => entry.oid.to_string() == sha,
877        None => false,
878    }
879}