Allow viewing past commits in Zed (#27636)
This PR adds functionality for loading the diff for an arbitrary git commit, and displaying it in a tab. To retrieve the diff for the commit, I'm using a single `git cat-file --batch` invocation to efficiently load both the old and new versions of each file that was changed in the commit. Todo * Features * [x] Open the commit view when clicking the most recent commit message in the commit panel * [x] Open the commit view when clicking a SHA in a git blame column * [x] Open the commit view when clicking a SHA in a commit tooltip * [x] Make it work over RPC * [x] Allow buffer search in commit view * [x] Command palette action to open the commit for the current blame line * Styling * [x] Add a header that shows the author, timestamp, and the full commit message * [x] Remove stage/unstage buttons in commit view * [x] Truncate the commit message in the tab * Bugs * [x] Dedup commit tabs within a pane * [x] Add a tooltip to the tab Release Notes: - Added the ability to show past commits in Zed. You can view the most recent commit by clicking its message in the commit panel. And when viewing a git blame, you can show any commit by clicking its sha.
This commit is contained in:
parent
33912011b7
commit
8546dc101d
28 changed files with 1742 additions and 603 deletions
|
@ -21,6 +21,7 @@ anyhow.workspace = true
|
|||
askpass.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -36,6 +37,7 @@ language_model.workspace = true
|
|||
linkify.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
notifications.workspace = true
|
||||
|
|
234
crates/git_ui/src/blame_ui.rs
Normal file
234
crates/git_ui/src/blame_ui.rs
Normal file
|
@ -0,0 +1,234 @@
|
|||
use crate::{commit_tooltip::CommitTooltip, commit_view::CommitView};
|
||||
use editor::{BlameRenderer, Editor};
|
||||
use git::{
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
repository::CommitSummary,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, ClipboardItem, Element as _, Entity, Hsla,
|
||||
InteractiveElement as _, MouseButton, Pixels, StatefulInteractiveElement as _, Styled as _,
|
||||
Subscription, TextStyle, WeakEntity, Window, div,
|
||||
};
|
||||
use project::{git_store::Repository, project_settings::ProjectSettings};
|
||||
use settings::Settings as _;
|
||||
use ui::{
|
||||
ActiveTheme, Color, ContextMenu, FluentBuilder as _, Icon, IconName, ParentElement as _, h_flex,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
|
||||
|
||||
pub struct GitBlameRenderer;
|
||||
|
||||
impl BlameRenderer for GitBlameRenderer {
|
||||
fn max_author_length(&self) -> usize {
|
||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED
|
||||
}
|
||||
|
||||
fn render_blame_entry(
|
||||
&self,
|
||||
style: &TextStyle,
|
||||
blame_entry: BlameEntry,
|
||||
details: Option<ParsedCommitMessage>,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: Entity<Editor>,
|
||||
ix: usize,
|
||||
sha_color: Hsla,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
let short_commit_id = blame_entry.sha.display_short();
|
||||
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
||||
let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.font_family(style.font().family)
|
||||
.line_height(style.line_height)
|
||||
.id(("blame", ix))
|
||||
.text_color(cx.theme().status().hint)
|
||||
.pr_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(div().text_color(sha_color).child(short_commit_id))
|
||||
.child(name),
|
||||
)
|
||||
.child(relative_timestamp)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Right, {
|
||||
let blame_entry = blame_entry.clone();
|
||||
let details = details.clone();
|
||||
move |event, window, cx| {
|
||||
deploy_blame_entry_context_menu(
|
||||
&blame_entry,
|
||||
details.as_ref(),
|
||||
editor.clone(),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let blame_entry = blame_entry.clone();
|
||||
let repository = repository.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |_, window, cx| {
|
||||
CommitView::open(
|
||||
CommitSummary {
|
||||
sha: blame_entry.sha.to_string().into(),
|
||||
subject: blame_entry.summary.clone().unwrap_or_default().into(),
|
||||
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
|
||||
has_parent: true,
|
||||
},
|
||||
repository.downgrade(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
cx.new(|cx| {
|
||||
CommitTooltip::blame_entry(
|
||||
&blame_entry,
|
||||
details.clone(),
|
||||
repository.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
&self,
|
||||
style: &TextStyle,
|
||||
blame_entry: BlameEntry,
|
||||
details: Option<ParsedCommitMessage>,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: Entity<Editor>,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||
let summary_enabled = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.show_inline_commit_summary();
|
||||
|
||||
let text = match blame_entry.summary.as_ref() {
|
||||
Some(summary) if summary_enabled => {
|
||||
format!("{}, {} - {}", author, relative_timestamp, summary)
|
||||
}
|
||||
_ => format!("{}, {}", author, relative_timestamp),
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("inline-blame")
|
||||
.w_full()
|
||||
.font_family(style.font().family)
|
||||
.text_color(cx.theme().status().hint)
|
||||
.line_height(style.line_height)
|
||||
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
||||
.child(text)
|
||||
.gap_2()
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
let tooltip = cx.new(|cx| {
|
||||
CommitTooltip::blame_entry(
|
||||
&blame_entry,
|
||||
details.clone(),
|
||||
repository.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.git_blame_inline_tooltip = Some(tooltip.downgrade().into())
|
||||
});
|
||||
tooltip.into()
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn open_blame_commit(
|
||||
&self,
|
||||
blame_entry: BlameEntry,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
CommitView::open(
|
||||
CommitSummary {
|
||||
sha: blame_entry.sha.to_string().into(),
|
||||
subject: blame_entry.summary.clone().unwrap_or_default().into(),
|
||||
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
|
||||
has_parent: true,
|
||||
},
|
||||
repository.downgrade(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_blame_entry_context_menu(
|
||||
blame_entry: &BlameEntry,
|
||||
details: Option<&ParsedCommitMessage>,
|
||||
editor: Entity<Editor>,
|
||||
position: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
|
||||
let sha = format!("{}", blame_entry.sha);
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.entry("Copy commit SHA", None, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
|
||||
})
|
||||
.when_some(
|
||||
details.and_then(|details| details.permalink.clone()),
|
||||
|this, url| {
|
||||
this.entry("Open permalink", None, move |_, cx| {
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
editor.update(cx, move |editor, cx| {
|
||||
editor.deploy_mouse_context_menu(position, context_menu, window, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
time::UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
time_format::TimestampFormat::Relative,
|
||||
)
|
||||
}
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
363
crates/git_ui/src/commit_tooltip.rs
Normal file
363
crates/git_ui/src/commit_tooltip.rs
Normal file
|
@ -0,0 +1,363 @@
|
|||
use crate::commit_view::CommitView;
|
||||
use editor::hover_markdown_style;
|
||||
use futures::Future;
|
||||
use git::blame::BlameEntry;
|
||||
use git::repository::CommitSummary;
|
||||
use git::{GitRemote, blame::ParsedCommitMessage};
|
||||
use gpui::{
|
||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use project::git_store::Repository;
|
||||
use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::ThemeSettings;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommitDetails {
|
||||
pub sha: SharedString,
|
||||
pub author_name: SharedString,
|
||||
pub author_email: SharedString,
|
||||
pub commit_time: OffsetDateTime,
|
||||
pub message: Option<ParsedCommitMessage>,
|
||||
}
|
||||
|
||||
struct CommitAvatar<'a> {
|
||||
commit: &'a CommitDetails,
|
||||
}
|
||||
|
||||
impl<'a> CommitAvatar<'a> {
|
||||
fn new(details: &'a CommitDetails) -> Self {
|
||||
Self { commit: details }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CommitAvatar<'a> {
|
||||
fn render(
|
||||
&'a self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<CommitTooltip>,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let remote = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|details| details.remote.clone())
|
||||
.filter(|remote| remote.host_supports_avatars())?;
|
||||
|
||||
let avatar_url = CommitAvatarAsset::new(remote, self.commit.sha.clone());
|
||||
|
||||
let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
|
||||
// Loading or no avatar found
|
||||
None | Some(None) => Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.into_element()
|
||||
.into_any(),
|
||||
// Found
|
||||
Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
|
||||
};
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CommitAvatarAsset {
|
||||
sha: SharedString,
|
||||
remote: GitRemote,
|
||||
}
|
||||
|
||||
impl Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitAvatarAsset {
|
||||
fn new(remote: GitRemote, sha: SharedString) -> Self {
|
||||
Self { remote, sha }
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for CommitAvatarAsset {
|
||||
type Source = Self;
|
||||
type Output = Option<SharedString>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let client = cx.http_client();
|
||||
|
||||
async move {
|
||||
source
|
||||
.remote
|
||||
.avatar_url(source.sha, client)
|
||||
.await
|
||||
.map(|url| SharedString::from(url.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommitTooltip {
|
||||
commit: CommitDetails,
|
||||
scroll_handle: ScrollHandle,
|
||||
markdown: Entity<Markdown>,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl CommitTooltip {
|
||||
pub fn blame_entry(
|
||||
blame: &BlameEntry,
|
||||
details: Option<ParsedCommitMessage>,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let commit_time = blame
|
||||
.committer_time
|
||||
.and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok())
|
||||
.unwrap_or(OffsetDateTime::now_utc());
|
||||
|
||||
Self::new(
|
||||
CommitDetails {
|
||||
sha: blame.sha.to_string().into(),
|
||||
commit_time,
|
||||
author_name: blame
|
||||
.author
|
||||
.clone()
|
||||
.unwrap_or("<no name>".to_string())
|
||||
.into(),
|
||||
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
|
||||
message: details,
|
||||
},
|
||||
repository,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
commit: CommitDetails,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut style = hover_markdown_style(window, cx);
|
||||
if let Some(code_block) = &style.code_block.text {
|
||||
style.base_text_style.refine(code_block);
|
||||
}
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|message| message.message.clone())
|
||||
.unwrap_or_default(),
|
||||
style,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Self {
|
||||
commit,
|
||||
repository,
|
||||
workspace,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
markdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommitTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let avatar = CommitAvatar::new(&self.commit).render(window, cx);
|
||||
|
||||
let author = self.commit.author_name.clone();
|
||||
|
||||
let author_email = self.commit.author_email.clone();
|
||||
|
||||
let short_commit_id = self
|
||||
.commit
|
||||
.sha
|
||||
.get(0..8)
|
||||
.map(|sha| sha.to_string().into())
|
||||
.unwrap_or_else(|| self.commit.sha.clone());
|
||||
let full_sha = self.commit.sha.to_string().clone();
|
||||
let absolute_timestamp = format_local_timestamp(
|
||||
self.commit.commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
);
|
||||
|
||||
let message = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|_| self.markdown.clone().into_any_element())
|
||||
.unwrap_or("<no commit message>".into_any());
|
||||
|
||||
let pull_request = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|details| details.pull_request.clone());
|
||||
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
|
||||
let repo = self.repository.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let commit_summary = CommitSummary {
|
||||
sha: self.commit.sha.clone(),
|
||||
subject: self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map_or(Default::default(), |message| {
|
||||
message
|
||||
.message
|
||||
.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
.into()
|
||||
}),
|
||||
commit_timestamp: self.commit.commit_time.unix_timestamp(),
|
||||
has_parent: false,
|
||||
};
|
||||
|
||||
tooltip_container(window, cx, move |this, _, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.w(gpui::rems(30.))
|
||||
.gap_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1p5()
|
||||
.gap_x_2()
|
||||
.overflow_x_hidden()
|
||||
.flex_wrap()
|
||||
.children(avatar)
|
||||
.child(author)
|
||||
.when(!author_email.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(author_email),
|
||||
)
|
||||
})
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("inline-blame-commit-message")
|
||||
.child(message)
|
||||
.max_h(message_max_height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.pt_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(absolute_timestamp)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.when_some(pull_request, |this, pr| {
|
||||
this.child(
|
||||
Button::new(
|
||||
"pull-request-button",
|
||||
format!("#{}", pr.number),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::PullRequest)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(pr.url.as_str())
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
Button::new(
|
||||
"commit-sha-button",
|
||||
short_commit_id.clone(),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::FileGit)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(
|
||||
move |_, window, cx| {
|
||||
CommitView::open(
|
||||
commit_summary.clone(),
|
||||
repo.downgrade(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.stop_propagation();
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("copy-sha-button", IconName::Copy)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(
|
||||
ClipboardItem::new_string(full_sha.clone()),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
format,
|
||||
)
|
||||
}
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative)
|
||||
}
|
527
crates/git_ui/src/commit_view.rs
Normal file
527
crates/git_ui/src/commit_view.rs
Normal file
|
@ -0,0 +1,527 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||
Point, Rope, TextBuffer,
|
||||
};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, WorktreeId, git_store::Repository};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ffi::OsStr,
|
||||
fmt::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{Color, Icon, IconName, Label, LabelCommon as _};
|
||||
use util::{ResultExt, truncate_and_trailoff};
|
||||
use workspace::{
|
||||
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
item::{BreadcrumbText, ItemEvent, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct CommitView {
|
||||
commit: CommitDetails,
|
||||
editor: Entity<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
}
|
||||
|
||||
struct GitBlob {
|
||||
path: RepoPath,
|
||||
worktree_id: WorktreeId,
|
||||
is_deleted: bool,
|
||||
}
|
||||
|
||||
struct CommitMetadataFile {
|
||||
title: Arc<Path>,
|
||||
worktree_id: WorktreeId,
|
||||
}
|
||||
|
||||
const COMMIT_METADATA_NAMESPACE: &'static str = "0";
|
||||
const FILE_NAMESPACE: &'static str = "1";
|
||||
|
||||
impl CommitView {
|
||||
pub fn open(
|
||||
commit: CommitSummary,
|
||||
repo: WeakEntity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let commit_diff = repo
|
||||
.update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string()))
|
||||
.ok();
|
||||
let commit_details = repo
|
||||
.update(cx, |repo, _| repo.show(commit.sha.to_string()))
|
||||
.ok();
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
|
||||
let commit_diff = commit_diff.log_err()?.log_err()?;
|
||||
let commit_details = commit_details.log_err()?.log_err()?;
|
||||
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(),
|
||||
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
|
||||
.map_or(false, |view| view.read(cx).commit.sha == commit.sha)
|
||||
});
|
||||
if let Some(ix) = ix {
|
||||
pane.activate_item(ix, true, true, window, cx);
|
||||
return;
|
||||
} 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>,
|
||||
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
|
||||
});
|
||||
|
||||
let first_worktree_id = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).id());
|
||||
|
||||
let mut metadata_buffer_id = None;
|
||||
if let Some(worktree_id) = first_worktree_id {
|
||||
let file = Arc::new(CommitMetadataFile {
|
||||
title: PathBuf::from(format!("commit {}", commit.sha)).into(),
|
||||
worktree_id,
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = TextBuffer::new_normalized(
|
||||
0,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
LineEnding::default(),
|
||||
format_commit(&commit).into(),
|
||||
);
|
||||
metadata_buffer_id = Some(buffer.remote_id());
|
||||
Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
|
||||
});
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
|
||||
buffer.clone(),
|
||||
vec![Point::zero()..buffer.read(cx).max_point()],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![0..0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cx.spawn(async move |this, mut 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
|
||||
.update(cx, |repository, cx| {
|
||||
repository
|
||||
.repo_path_to_project_path(&file.path, cx)
|
||||
.map(|path| path.worktree_id)
|
||||
.or(first_worktree_id)
|
||||
})?
|
||||
.ok_or_else(|| anyhow!("project has no worktrees"))?;
|
||||
let file = Arc::new(GitBlob {
|
||||
path: file.path.clone(),
|
||||
is_deleted,
|
||||
worktree_id,
|
||||
}) as Arc<dyn language::File>;
|
||||
|
||||
let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?;
|
||||
let buffer_diff =
|
||||
build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = buffer_diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
let path = snapshot.file().unwrap().path().clone();
|
||||
let _is_newly_added = multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(FILE_NAMESPACE, path),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
commit,
|
||||
editor,
|
||||
multibuffer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl language::File for GitBlob {
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
None
|
||||
}
|
||||
|
||||
fn disk_state(&self) -> DiskState {
|
||||
if self.is_deleted {
|
||||
DiskState::Deleted
|
||||
} else {
|
||||
DiskState::New
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path.0
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &App) -> PathBuf {
|
||||
self.path.to_path_buf()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
|
||||
self.path.file_name().unwrap()
|
||||
}
|
||||
|
||||
fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||
self.worktree_id
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn to_proto(&self, _cx: &App) -> language::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_private(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl language::File for CommitMetadataFile {
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
None
|
||||
}
|
||||
|
||||
fn disk_state(&self) -> DiskState {
|
||||
DiskState::New
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.title
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &App) -> PathBuf {
|
||||
self.title.as_ref().into()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
|
||||
self.title.file_name().unwrap()
|
||||
}
|
||||
|
||||
fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||
self.worktree_id
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn to_proto(&self, _: &App) -> language::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_private(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_buffer(
|
||||
mut text: String,
|
||||
blob: Arc<dyn File>,
|
||||
language_registry: &Arc<language::LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<Buffer>> {
|
||||
let line_ending = LineEnding::detect(&text);
|
||||
LineEnding::normalize(&mut text);
|
||||
let text = Rope::from(text);
|
||||
let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
|
||||
let language = if let Some(language) = language {
|
||||
language_registry
|
||||
.load_language(&language)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|e| e.log_err())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = TextBuffer::new_normalized(
|
||||
0,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
line_ending,
|
||||
text,
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
|
||||
buffer.set_language(language, cx);
|
||||
buffer
|
||||
})?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
mut old_text: Option<String>,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
if let Some(old_text) = &mut old_text {
|
||||
LineEnding::normalize(old_text);
|
||||
}
|
||||
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text.as_deref().unwrap_or("").into(),
|
||||
buffer.language().cloned(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let diff_snapshot = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
buffer.text.clone(),
|
||||
old_text.map(Arc::new),
|
||||
base_buffer,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||
diff.set_snapshot(diff_snapshot, &buffer.text, None, cx);
|
||||
diff
|
||||
})
|
||||
}
|
||||
|
||||
fn format_commit(commit: &CommitDetails) -> String {
|
||||
let mut result = String::new();
|
||||
writeln!(&mut result, "commit {}", commit.sha).unwrap();
|
||||
writeln!(
|
||||
&mut result,
|
||||
"Author: {} <{}>",
|
||||
commit.committer_name, commit.committer_email
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
&mut result,
|
||||
"Date: {}",
|
||||
time_format::format_local_timestamp(
|
||||
time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
|
||||
time::OffsetDateTime::now_utc(),
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
result.push('\n');
|
||||
for line in commit.message.split('\n') {
|
||||
if line.is_empty() {
|
||||
result.push('\n');
|
||||
} else {
|
||||
writeln!(&mut result, " {}", line).unwrap();
|
||||
}
|
||||
}
|
||||
if result.ends_with("\n\n") {
|
||||
result.pop();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for CommitView {}
|
||||
|
||||
impl Focusable for CommitView {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for CommitView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::GitBranch).color(Color::Muted))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
|
||||
let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
|
||||
let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
|
||||
Label::new(format!("{short_sha} - {subject}",))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
|
||||
let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
|
||||
let subject = self.commit.message.split('\n').next().unwrap();
|
||||
Some(format!("{short_sha} - {subject}").into())
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Commit View Opened")
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommitView {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
self.editor.clone()
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
use crate::askpass_modal::AskPassModal;
|
||||
use crate::commit_modal::CommitModal;
|
||||
use crate::commit_tooltip::CommitTooltip;
|
||||
use crate::commit_view::CommitView;
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::project_diff::Diff;
|
||||
use crate::project_diff::{self, Diff, ProjectDiff};
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
|
||||
use crate::{ProjectDiff, picker_prompt, project_diff};
|
||||
use crate::{branch_picker, render_remote_button};
|
||||
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
};
|
||||
|
@ -13,13 +13,13 @@ use anyhow::Result;
|
|||
use askpass::AskPassDelegate;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::commit_tooltip::CommitTooltip;
|
||||
|
||||
use editor::{
|
||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
scroll::ScrollbarAutoHide,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use git::blame::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
|
||||
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||
|
@ -3001,6 +3001,7 @@ impl GitPanel {
|
|||
let active_repository = self.active_repository.as_ref()?;
|
||||
let branch = active_repository.read(cx).current_branch()?;
|
||||
let commit = branch.most_recent_commit.as_ref()?.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let this = cx.entity();
|
||||
Some(
|
||||
|
@ -3023,14 +3024,31 @@ impl GitPanel {
|
|||
.truncate(),
|
||||
)
|
||||
.id("commit-msg-hover")
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
GitPanelMessageTooltip::new(
|
||||
this.clone(),
|
||||
commit.sha.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into()
|
||||
.on_click({
|
||||
let commit = commit.clone();
|
||||
let repo = active_repository.downgrade();
|
||||
move |_, window, cx| {
|
||||
CommitView::open(
|
||||
commit.clone(),
|
||||
repo.clone(),
|
||||
workspace.clone().clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.hoverable_tooltip({
|
||||
let repo = active_repository.clone();
|
||||
move |window, cx| {
|
||||
GitPanelMessageTooltip::new(
|
||||
this.clone(),
|
||||
commit.sha.clone(),
|
||||
repo.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
|
@ -3938,31 +3956,35 @@ impl GitPanelMessageTooltip {
|
|||
fn new(
|
||||
git_panel: Entity<GitPanel>,
|
||||
sha: SharedString,
|
||||
repository: Entity<Repository>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let details = git_panel
|
||||
.update(cx, |git_panel, cx| {
|
||||
git_panel.load_commit_details(sha.to_string(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
|
||||
(
|
||||
git_panel.load_commit_details(sha.to_string(), cx),
|
||||
git_panel.workspace.clone(),
|
||||
)
|
||||
})?;
|
||||
let details = details.await?;
|
||||
|
||||
let commit_details = editor::commit_tooltip::CommitDetails {
|
||||
let commit_details = crate::commit_tooltip::CommitDetails {
|
||||
sha: details.sha.clone(),
|
||||
author_name: details.committer_name.clone(),
|
||||
author_email: details.committer_email.clone(),
|
||||
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
|
||||
message: Some(editor::commit_tooltip::ParsedCommitMessage {
|
||||
message: Some(ParsedCommitMessage {
|
||||
message: details.message.clone(),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
this.update_in(cx, |this: &mut GitPanelMessageTooltip, window, cx| {
|
||||
this.commit_tooltip =
|
||||
Some(cx.new(move |cx| CommitTooltip::new(commit_details, window, cx)));
|
||||
this.commit_tooltip = Some(cx.new(move |cx| {
|
||||
CommitTooltip::new(commit_details, repository, workspace, window, cx)
|
||||
}));
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::any::Any;
|
|||
use ::settings::Settings;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use commit_modal::CommitModal;
|
||||
mod blame_ui;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
||||
|
@ -17,6 +18,8 @@ use workspace::Workspace;
|
|||
mod askpass_modal;
|
||||
pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
pub mod commit_tooltip;
|
||||
mod commit_view;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod onboarding;
|
||||
|
@ -30,6 +33,8 @@ actions!(git, [ResetOnboarding]);
|
|||
pub fn init(cx: &mut App) {
|
||||
GitPanelSettings::register(cx);
|
||||
|
||||
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||
ProjectDiff::register(workspace, cx);
|
||||
CommitModal::register(workspace);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue