use anyhow::{Context as _, Result};use buffer_diff::{BufferDiff, BufferDiffSnapshot};use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};use git::repository::{CommitDetails, CommitDiff, RepoPath};use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};use gpui::{ AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,};use language::{ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope, TextBuffer, ToPoint,};use multi_buffer::ExcerptInfo;use multi_buffer::PathKey;use project::{Project, WorktreeId, git_store::Repository};use std::{ any::{Any, TypeId}, path::PathBuf, sync::Arc,};use theme::ActiveTheme;use ui::{ Avatar, Button, ButtonCommon, Clickable, Color, Icon, IconName, IconSize, Label, LabelCommon as _, LabelSize, SharedString, div, h_flex, v_flex,};use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{BreadcrumbText, ItemEvent, TabContentParams}, notifications::NotifyTaskExt, pane::SaveIntent, searchable::SearchableItemHandle,}; use crate::git_panel::GitPanel; actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| { CommitView::apply_stash(workspace, window, cx); }); workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| { CommitView::remove_stash(workspace, window, cx); }); workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| { CommitView::pop_stash(workspace, window, cx); }); }) .detach();} pub struct CommitView { commit: CommitDetails, editor: Entity<Editor>, stash: Option<usize>, multibuffer: Entity<MultiBuffer>, repository: Entity<Repository>, remote: Option<GitRemote>,} struct GitBlob { path: RepoPath, worktree_id: WorktreeId, is_deleted: bool, display_name: Arc<str>,} const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; impl CommitView { pub fn open( commit_sha: String, repo: WeakEntity<Repository>, workspace: WeakEntity<Workspace>, stash: Option<usize>, file_filter: Option<RepoPath>, window: &mut Window, cx: &mut App, ) { let commit_diff = repo .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone())) .ok(); let commit_details = repo .update(cx, |repo, _| repo.show(commit_sha.clone())) .ok(); window .spawn(cx, async move |cx| { let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?); let mut commit_diff = commit_diff.log_err()?.log_err()?; let commit_details = commit_details.log_err()?.log_err()?; // Filter to specific file if requested if let Some(ref filter_path) = file_filter { commit_diff.files.retain(|f| &f.path == filter_path); } let repo = repo.upgrade()?; workspace .update_in(cx, |workspace, window, cx| { let project = workspace.project(); let commit_view = cx.new(|cx| { CommitView::new( commit_details, commit_diff, repo, project.clone(), stash, window, cx, ) }); let pane = workspace.active_pane(); pane.update(cx, |pane, cx| { let ix = pane.items().position(|item| { let commit_view = item.downcast::<CommitView>(); commit_view .is_some_and(|view| view.read(cx).commit.sha == commit_sha) }); if let Some(ix) = ix { pane.activate_item(ix, true, true, window, cx); } else { pane.add_item(Box::new(commit_view), true, true, None, window, cx); } }) }) .log_err() }) .detach(); } fn new( commit: CommitDetails, commit_diff: CommitDiff, repository: Entity<Repository>, project: Entity<Project>, stash: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let language_registry = project.read(cx).languages().clone(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly)); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); editor.disable_inline_diagnostics(); editor.set_expand_all_diff_hunks(cx); editor.register_addon(CommitViewAddon { multibuffer: multibuffer.downgrade(), }); editor }); let commit_sha = Arc::<str>::from(commit.sha.as_ref()); let first_worktree_id = project .read(cx) .worktrees(cx) .next() .map(|worktree| worktree.read(cx).id()); let repository_clone = repository.clone(); let commit_message = commit.message.clone(); cx.spawn(async move |this, cx| { for file in commit_diff.files { let is_deleted = file.new_text.is_none(); let new_text = file.new_text.unwrap_or_default(); let old_text = file.old_text; let worktree_id = repository_clone .update(cx, |repository, cx| { repository .repo_path_to_project_path(&file.path, cx) .map(|path| path.worktree_id) .or(first_worktree_id) })? .context("project has no worktrees")?; let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha); let file_name = file .path .file_name() .map(|name| name.to_string()) .unwrap_or_else(|| file.path.display(PathStyle::Posix).to_string()); let display_name: Arc<str> = Arc::from(format!("{short_sha} - {file_name}").into_boxed_str()); let file = Arc::new(GitBlob { path: file.path.clone(), is_deleted, worktree_id, display_name, }) as Arc<dyn language::File>; let buffer = build_buffer(new_text, file, &language_registry, cx).await?; let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx).snapshot(); let path = snapshot.file().unwrap().path().clone(); let hunks: Vec<_> = buffer_diff.read(cx).hunks(&snapshot, cx).collect(); let excerpt_ranges = if hunks.is_empty() { vec![language::Point::zero()..snapshot.max_point()] } else { hunks .into_iter() .map(|hunk| { let start = hunk.range.start.max(language::Point::new( hunk.range.start.row.saturating_sub(3), 0, )); let end_row = (hunk.range.end.row + 3).min(snapshot.max_point().row); let end = language::Point::new(end_row, snapshot.line_len(end_row)); start..end }) .collect() }; let _is_newly_added = multibuffer.set_excerpts_for_path( PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path), buffer, excerpt_ranges, 0, cx, ); multibuffer.add_diff(buffer_diff, cx); }); })?; } let message_buffer = cx.new(|cx| { let mut buffer = Buffer::local(commit_message, cx); buffer.set_capability(Capability::ReadOnly, cx); buffer })?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { let range = ExcerptRange { context: Anchor::MIN..Anchor::MAX, primary: Anchor::MIN..Anchor::MAX, }; multibuffer.insert_excerpts_after( ExcerptId::min(), message_buffer.clone(), [range], cx, ) }); this.editor.update(cx, |editor, cx| { editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx); editor.insert_blocks( [BlockProperties { placement: BlockPlacement::Above(editor::Anchor::min()), height: Some(1), style: BlockStyle::Sticky, render: Arc::new(|_| gpui::Empty.into_any_element()), priority: 0, }] .into_iter() .chain( editor .buffer() .read(cx) .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx) .map(|anchor| BlockProperties { placement: BlockPlacement::Below(anchor), height: Some(1), style: BlockStyle::Sticky, render: Arc::new(|_| gpui::Empty.into_any_element()), priority: 0, }), ), None, cx, ) }); })?; anyhow::Ok(()) }) .detach(); let snapshot = repository.read(cx).snapshot(); let remote_url = snapshot .remote_upstream_url .as_ref() .or(snapshot.remote_origin_url.as_ref()); let remote = remote_url.and_then(|url| { let provider_registry = GitHostingProviderRegistry::default_global(cx); parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote { host, owner: parsed.owner.into(), repo: parsed.repo.into(), }) }); Self { commit, editor, multibuffer, stash, repository, remote, } } fn render_commit_avatar( &self, sha: &SharedString, size: impl Into<gpui::AbsoluteLength>, window: &mut Window, cx: &mut App, ) -> AnyElement { let size = size.into(); let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars()); if let Some(remote) = remote { let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone()); if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) { return Avatar::new(url.to_string()) .size(size) .into_element() .into_any(); } } v_flex() .w(size) .h(size) .border_1() .border_color(cx.theme().colors().border) .rounded_full() .justify_center() .items_center() .child( Icon::new(IconName::Person) .color(Color::Muted) .size(IconSize::Medium) .into_element(), ) .into_any() } fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let commit = &self.commit; let author_name = commit.author_name.clone(); let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp) .unwrap_or_else(|_| time::OffsetDateTime::now_utc()); let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC); let date_string = time_format::format_localized_timestamp( commit_date, time::OffsetDateTime::now_utc(), local_offset, time_format::TimestampFormat::MediumAbsolute, ); let github_url = self.remote.as_ref().map(|remote| { format!( "{}/{}/{}/commit/{}", remote.host.base_url(), remote.owner, remote.repo, commit.sha ) }); v_flex() .p_4() .pl_0() .gap_4() .border_b_1() .border_color(cx.theme().colors().border) .child( h_flex() .items_start() .child( h_flex() .w(self.editor.read(cx).last_gutter_dimensions().full_width()) .justify_center() .child(self.render_commit_avatar( &commit.sha, gpui::rems(3.0), window, cx, )), ) .child( v_flex() .gap_1() .child( h_flex() .gap_3() .items_baseline() .child(Label::new(author_name).color(Color::Default)) .child( Label::new(format!("commit {}", commit.sha)) .color(Color::Muted), ), ) .child(Label::new(date_string).color(Color::Muted)), ) .child(div().flex_grow()) .children(github_url.map(|url| { Button::new("view_on_github", "View on GitHub") .icon(IconName::Github) .style(ui::ButtonStyle::Subtle) .on_click(move |_, _, cx| cx.open_url(&url)) })), ) } fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Apply", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, not applying")); } Ok(repo.stash_apply(Some(stash), cx)) })?; match result { Ok(task) => task.await?, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err); } }; Self::close_commit_view(commit_view, workspace, cx).await?; anyhow::Ok(()) }, ); } fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Pop", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, pop aborted")); } Ok(repo.stash_pop(Some(stash), cx)) })?; match result { Ok(task) => task.await?, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err); } }; Self::close_commit_view(commit_view, workspace, cx).await?; anyhow::Ok(()) }, ); } fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Drop", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, drop aborted")); } Ok(repo.stash_drop(Some(stash), cx)) })?; match result { Ok(task) => task.await??, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err);Lines 1–500 of 1051. Load more
use anyhow::{Context as _, Result}; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle}; use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer}; use git::repository::{CommitDetails, CommitDiff, RepoPath}; use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url}; use gpui::{ AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions, }; use language::{ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope, TextBuffer, ToPoint, }; use multi_buffer::ExcerptInfo; use multi_buffer::PathKey; use project::{Project, WorktreeId, git_store::Repository}; use std::{ any::{Any, TypeId}, path::PathBuf, sync::Arc, }; use theme::ActiveTheme; use ui::{ Avatar, Button, ButtonCommon, Clickable, Color, Icon, IconName, IconSize, Label, LabelCommon as _, LabelSize, SharedString, div, h_flex, v_flex, }; use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff}; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{BreadcrumbText, ItemEvent, TabContentParams}, notifications::NotifyTaskExt, pane::SaveIntent, searchable::SearchableItemHandle, }; use crate::git_panel::GitPanel; actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| { CommitView::apply_stash(workspace, window, cx); }); workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| { CommitView::remove_stash(workspace, window, cx); }); workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| { CommitView::pop_stash(workspace, window, cx); }); }) .detach(); } pub struct CommitView { commit: CommitDetails, editor: Entity<Editor>, stash: Option<usize>, multibuffer: Entity<MultiBuffer>, repository: Entity<Repository>, remote: Option<GitRemote>, } struct GitBlob { path: RepoPath, worktree_id: WorktreeId, is_deleted: bool, display_name: Arc<str>, } const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; impl CommitView { pub fn open( commit_sha: String, repo: WeakEntity<Repository>, workspace: WeakEntity<Workspace>, stash: Option<usize>, file_filter: Option<RepoPath>, window: &mut Window, cx: &mut App, ) { let commit_diff = repo .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone())) .ok(); let commit_details = repo .update(cx, |repo, _| repo.show(commit_sha.clone())) .ok(); window .spawn(cx, async move |cx| { let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?); let mut commit_diff = commit_diff.log_err()?.log_err()?; let commit_details = commit_details.log_err()?.log_err()?; // Filter to specific file if requested if let Some(ref filter_path) = file_filter { commit_diff.files.retain(|f| &f.path == filter_path); } let repo = repo.upgrade()?; workspace .update_in(cx, |workspace, window, cx| { let project = workspace.project(); let commit_view = cx.new(|cx| { CommitView::new( commit_details, commit_diff, repo, project.clone(), stash, window, cx, ) }); let pane = workspace.active_pane(); pane.update(cx, |pane, cx| { let ix = pane.items().position(|item| { let commit_view = item.downcast::<CommitView>(); commit_view .is_some_and(|view| view.read(cx).commit.sha == commit_sha) }); if let Some(ix) = ix { pane.activate_item(ix, true, true, window, cx); } else { pane.add_item(Box::new(commit_view), true, true, None, window, cx); } }) }) .log_err() }) .detach(); } fn new( commit: CommitDetails, commit_diff: CommitDiff, repository: Entity<Repository>, project: Entity<Project>, stash: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let language_registry = project.read(cx).languages().clone(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly)); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); editor.disable_inline_diagnostics(); editor.set_expand_all_diff_hunks(cx); editor.register_addon(CommitViewAddon { multibuffer: multibuffer.downgrade(), }); editor }); let commit_sha = Arc::<str>::from(commit.sha.as_ref()); let first_worktree_id = project .read(cx) .worktrees(cx) .next() .map(|worktree| worktree.read(cx).id()); let repository_clone = repository.clone(); let commit_message = commit.message.clone(); cx.spawn(async move |this, cx| { for file in commit_diff.files { let is_deleted = file.new_text.is_none(); let new_text = file.new_text.unwrap_or_default(); let old_text = file.old_text; let worktree_id = repository_clone .update(cx, |repository, cx| { repository .repo_path_to_project_path(&file.path, cx) .map(|path| path.worktree_id) .or(first_worktree_id) })? .context("project has no worktrees")?; let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha); let file_name = file .path .file_name() .map(|name| name.to_string()) .unwrap_or_else(|| file.path.display(PathStyle::Posix).to_string()); let display_name: Arc<str> = Arc::from(format!("{short_sha} - {file_name}").into_boxed_str()); let file = Arc::new(GitBlob { path: file.path.clone(), is_deleted, worktree_id, display_name, }) as Arc<dyn language::File>; let buffer = build_buffer(new_text, file, &language_registry, cx).await?; let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx).snapshot(); let path = snapshot.file().unwrap().path().clone(); let hunks: Vec<_> = buffer_diff.read(cx).hunks(&snapshot, cx).collect(); let excerpt_ranges = if hunks.is_empty() { vec![language::Point::zero()..snapshot.max_point()] } else { hunks .into_iter() .map(|hunk| { let start = hunk.range.start.max(language::Point::new( hunk.range.start.row.saturating_sub(3), 0, )); let end_row = (hunk.range.end.row + 3).min(snapshot.max_point().row); let end = language::Point::new(end_row, snapshot.line_len(end_row)); start..end }) .collect() }; let _is_newly_added = multibuffer.set_excerpts_for_path( PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path), buffer, excerpt_ranges, 0, cx, ); multibuffer.add_diff(buffer_diff, cx); }); })?; } let message_buffer = cx.new(|cx| { let mut buffer = Buffer::local(commit_message, cx); buffer.set_capability(Capability::ReadOnly, cx); buffer })?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { let range = ExcerptRange { context: Anchor::MIN..Anchor::MAX, primary: Anchor::MIN..Anchor::MAX, }; multibuffer.insert_excerpts_after( ExcerptId::min(), message_buffer.clone(), [range], cx, ) }); this.editor.update(cx, |editor, cx| { editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx); editor.insert_blocks( [BlockProperties { placement: BlockPlacement::Above(editor::Anchor::min()), height: Some(1), style: BlockStyle::Sticky, render: Arc::new(|_| gpui::Empty.into_any_element()), priority: 0, }] .into_iter() .chain( editor .buffer() .read(cx) .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx) .map(|anchor| BlockProperties { placement: BlockPlacement::Below(anchor), height: Some(1), style: BlockStyle::Sticky, render: Arc::new(|_| gpui::Empty.into_any_element()), priority: 0, }), ), None, cx, ) }); })?; anyhow::Ok(()) }) .detach(); let snapshot = repository.read(cx).snapshot(); let remote_url = snapshot .remote_upstream_url .as_ref() .or(snapshot.remote_origin_url.as_ref()); let remote = remote_url.and_then(|url| { let provider_registry = GitHostingProviderRegistry::default_global(cx); parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote { host, owner: parsed.owner.into(), repo: parsed.repo.into(), }) }); Self { commit, editor, multibuffer, stash, repository, remote, } } fn render_commit_avatar( &self, sha: &SharedString, size: impl Into<gpui::AbsoluteLength>, window: &mut Window, cx: &mut App, ) -> AnyElement { let size = size.into(); let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars()); if let Some(remote) = remote { let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone()); if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) { return Avatar::new(url.to_string()) .size(size) .into_element() .into_any(); } } v_flex() .w(size) .h(size) .border_1() .border_color(cx.theme().colors().border) .rounded_full() .justify_center() .items_center() .child( Icon::new(IconName::Person) .color(Color::Muted) .size(IconSize::Medium) .into_element(), ) .into_any() } fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let commit = &self.commit; let author_name = commit.author_name.clone(); let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp) .unwrap_or_else(|_| time::OffsetDateTime::now_utc()); let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC); let date_string = time_format::format_localized_timestamp( commit_date, time::OffsetDateTime::now_utc(), local_offset, time_format::TimestampFormat::MediumAbsolute, ); let github_url = self.remote.as_ref().map(|remote| { format!( "{}/{}/{}/commit/{}", remote.host.base_url(), remote.owner, remote.repo, commit.sha ) }); v_flex() .p_4() .pl_0() .gap_4() .border_b_1() .border_color(cx.theme().colors().border) .child( h_flex() .items_start() .child( h_flex() .w(self.editor.read(cx).last_gutter_dimensions().full_width()) .justify_center() .child(self.render_commit_avatar( &commit.sha, gpui::rems(3.0), window, cx, )), ) .child( v_flex() .gap_1() .child( h_flex() .gap_3() .items_baseline() .child(Label::new(author_name).color(Color::Default)) .child( Label::new(format!("commit {}", commit.sha)) .color(Color::Muted), ), ) .child(Label::new(date_string).color(Color::Muted)), ) .child(div().flex_grow()) .children(github_url.map(|url| { Button::new("view_on_github", "View on GitHub") .icon(IconName::Github) .style(ui::ButtonStyle::Subtle) .on_click(move |_, _, cx| cx.open_url(&url)) })), ) } fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Apply", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, not applying")); } Ok(repo.stash_apply(Some(stash), cx)) })?; match result { Ok(task) => task.await?, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err); } }; Self::close_commit_view(commit_view, workspace, cx).await?; anyhow::Ok(()) }, ); } fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Pop", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, pop aborted")); } Ok(repo.stash_pop(Some(stash), cx)) })?; match result { Ok(task) => task.await?, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err); } }; Self::close_commit_view(commit_view, workspace, cx).await?; anyhow::Ok(()) }, ); } fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) { Self::stash_action( workspace, "Drop", window, cx, async move |repository, sha, stash, commit_view, workspace, cx| { let result = repository.update(cx, |repo, cx| { if !stash_matches_index(&sha, stash, repo) { return Err(anyhow::anyhow!("Stash has changed, drop aborted")); } Ok(repo.stash_drop(Some(stash), cx)) })?; match result { Ok(task) => task.await??, Err(err) => { Self::close_commit_view(commit_view, workspace, cx).await?; return Err(err);