commit_view.rs

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