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