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