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