mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-22 07:24:18 +00:00
git_panel: Add commit history view (#56500)
This PR adds a history view to the git panel. It draws from similar implementations of commit listing in other places like the file history view and the git graph. | Changes View | History View | |--------|--------| | <img width="2600" height="2026" alt="Screenshot 2026-05-12 at 3 38@2x" src="https://github.com/user-attachments/assets/00d7d21b-8516-481a-a34e-86589b8ced52" /> | <img width="2688" height="2114" alt="Screenshot 2026-05-12 at 3 39@2x" src="https://github.com/user-attachments/assets/4a87c04a-011b-40fc-a04c-b791720d9abc" /> | Release Notes: - Git: Added a history view to the Git panel that allows to quickly see in a list all the commits for a given branch. --------- Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
parent
c35996dbfb
commit
61da34c69f
3 changed files with 516 additions and 66 deletions
|
|
@ -84,7 +84,7 @@ impl<'a> CommitAvatar<'a> {
|
|||
.child(
|
||||
Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::askpass_modal::AskPassModal;
|
||||
use crate::commit_modal::CommitModal;
|
||||
use crate::commit_tooltip::CommitTooltip;
|
||||
use crate::commit_tooltip::{CommitAvatar, CommitTooltip};
|
||||
use crate::commit_view::CommitView;
|
||||
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
||||
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
||||
|
|
@ -23,18 +23,20 @@ use editor::{EditorStyle, RewrapOptions};
|
|||
use file_icons::FileIcons;
|
||||
use futures::StreamExt as _;
|
||||
use futures::channel::oneshot::Canceled;
|
||||
use git::Oid;
|
||||
use git::commit::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitTemplate,
|
||||
GitCommitter, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
|
||||
UpstreamTrackingStatus, get_git_committer,
|
||||
Branch, CommitData, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions,
|
||||
GitCommitTemplate, GitCommitter, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput,
|
||||
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer,
|
||||
};
|
||||
use git::stash::GitStash;
|
||||
use git::status::{DiffStat, StageStatus};
|
||||
use git::{Amend, Commit, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
|
||||
use git::{
|
||||
ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,
|
||||
StashApply, StashPop, ToggleFillCommitEditor, TrashUntrackedFiles, UnstageAll,
|
||||
ExpandCommitEditor, GitHostingProviderRegistry, GitRemote, RestoreTrackedFiles, StageAll,
|
||||
StashAll, StashApply, StashPop, ToggleFillCommitEditor, TrashUntrackedFiles, UnstageAll,
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AbsoluteLength, Action, Anchor, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, DismissEvent,
|
||||
|
|
@ -55,7 +57,9 @@ use panel::PanelHeader;
|
|||
use project::git_store::GitAccess;
|
||||
use project::{
|
||||
Fs, Project, ProjectPath,
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
git_store::{
|
||||
CommitDataState, GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op,
|
||||
},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
|
|
@ -71,9 +75,9 @@ use strum::{IntoEnumIterator, VariantNames};
|
|||
use theme_settings::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
ButtonLike, Checkbox, ContextMenu, ElevationIndex, IndentGuideColors, PopoverMenu,
|
||||
RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, TintColor, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
ButtonLike, Checkbox, ContextMenu, Divider, ElevationIndex, IndentGuideColors, PopoverMenu,
|
||||
RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab, TintColor, Tooltip,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::paths::PathStyle;
|
||||
use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath};
|
||||
|
|
@ -289,6 +293,12 @@ struct SerializedGitPanel {
|
|||
signoff_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum GitPanelTab {
|
||||
Changes,
|
||||
History,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
enum Section {
|
||||
Conflict,
|
||||
|
|
@ -680,6 +690,11 @@ pub struct GitPanel {
|
|||
commit_template: Option<GitCommitTemplate>,
|
||||
bulk_staging: Option<BulkStaging>,
|
||||
stash_entries: GitStash,
|
||||
active_tab: GitPanelTab,
|
||||
commit_history_scroll_handle: UniformListScrollHandle,
|
||||
commit_history_shas: Vec<Oid>,
|
||||
focused_history_entry: Option<usize>,
|
||||
_repo_subscriptions: Vec<Subscription>,
|
||||
|
||||
_settings_subscription: Subscription,
|
||||
git_access: GitAccess,
|
||||
|
|
@ -871,6 +886,11 @@ impl GitPanel {
|
|||
entry_count: 0,
|
||||
bulk_staging: None,
|
||||
stash_entries: Default::default(),
|
||||
active_tab: GitPanelTab::Changes,
|
||||
commit_history_scroll_handle: UniformListScrollHandle::new(),
|
||||
commit_history_shas: Vec::new(),
|
||||
focused_history_entry: None,
|
||||
_repo_subscriptions: Vec::new(),
|
||||
_settings_subscription,
|
||||
git_access: GitAccess::Yes,
|
||||
};
|
||||
|
|
@ -1119,6 +1139,11 @@ impl GitPanel {
|
|||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.active_tab == GitPanelTab::History {
|
||||
self.select_previous_history_entry(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let item_count = self.entries.len();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
|
|
@ -1180,6 +1205,11 @@ impl GitPanel {
|
|||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.active_tab == GitPanelTab::History {
|
||||
self.select_next_history_entry(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let item_count = self.entries.len();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
|
|
@ -1303,6 +1333,10 @@ impl GitPanel {
|
|||
}
|
||||
|
||||
fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.active_tab == GitPanelTab::History {
|
||||
self.open_selected_history_commit(window, cx);
|
||||
return;
|
||||
}
|
||||
if let Some(GitListEntry::Directory(dir_entry)) = self
|
||||
.selected_entry
|
||||
.and_then(|i| self.entries.get(i))
|
||||
|
|
@ -3473,6 +3507,10 @@ impl GitPanel {
|
|||
fn schedule_update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let handle = cx.entity().downgrade();
|
||||
self.reopen_commit_buffer(window, cx);
|
||||
self.preload_commit_history(cx);
|
||||
if self.active_tab == GitPanelTab::History {
|
||||
self.load_commit_history(cx);
|
||||
}
|
||||
self.update_visible_entries_task = cx.spawn_in(window, async move |_, cx| {
|
||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
if let Some(git_panel) = handle.upgrade() {
|
||||
|
|
@ -4100,7 +4138,7 @@ impl GitPanel {
|
|||
path + file_name + depth * 2
|
||||
}
|
||||
|
||||
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
fn render_ellipsis_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let has_tracked_changes = self.has_tracked_changes();
|
||||
let has_staged_changes = self.has_staged_changes();
|
||||
|
|
@ -4111,8 +4149,7 @@ impl GitPanel {
|
|||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.menu(move |window, cx| {
|
||||
Some(git_panel_context_menu(
|
||||
|
|
@ -4383,9 +4420,9 @@ impl GitPanel {
|
|||
})
|
||||
}
|
||||
|
||||
fn render_panel_header(
|
||||
fn render_changes_header(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if matches!(self.git_access, GitAccess::No) {
|
||||
|
|
@ -4401,24 +4438,24 @@ impl GitPanel {
|
|||
("Stage All", StageAll.boxed_clone(), true, "git add --all")
|
||||
};
|
||||
|
||||
let change_string = match self.changes_count {
|
||||
0 => "No Changes".to_string(),
|
||||
1 => "1 Change".to_string(),
|
||||
count => format!("{} Changes", count),
|
||||
};
|
||||
|
||||
Some(
|
||||
self.panel_header_container(window, cx)
|
||||
.px_2()
|
||||
h_flex()
|
||||
.h(Tab::container_height(cx))
|
||||
.w_full()
|
||||
.px_1()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new("changes", change_string)
|
||||
Button::new("changes", "View Diff")
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.color(Color::Muted)
|
||||
.start_icon(
|
||||
Icon::new(IconName::Diff)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Open Diff",
|
||||
"View Diff",
|
||||
&Diff,
|
||||
&self.focus_handle,
|
||||
))
|
||||
|
|
@ -4431,12 +4468,11 @@ impl GitPanel {
|
|||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_overflow_menu("overflow_menu"))
|
||||
.child(self.render_ellipsis_menu("overflow_menu"))
|
||||
.child(
|
||||
Button::new("stage_unstage_all", text)
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
tooltip,
|
||||
|
|
@ -4888,6 +4924,426 @@ impl GitPanel {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_tab = self.active_tab;
|
||||
|
||||
let tab = |id: ElementId,
|
||||
active: bool,
|
||||
show_changes: bool,
|
||||
label: SharedString,
|
||||
set_active_tab: GitPanelTab| {
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id(id)
|
||||
.h_full()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.justify_center()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.border_b_1()
|
||||
.when(!active, |s| {
|
||||
s.bg(cx.theme().colors().editor_background.opacity(0.6))
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
})
|
||||
.child(Label::new(label).when(!active, |this| this.color(Color::Muted)))
|
||||
.when(show_changes && self.changes_count > 0, |this| {
|
||||
this.child(
|
||||
Label::new(format!("({})", self.changes_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(move |this, _, _, cx| this.set_active_tab(set_active_tab, cx)),
|
||||
)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.relative()
|
||||
.h(Tab::container_height(cx))
|
||||
.w_full()
|
||||
.child(tab(
|
||||
ElementId::Name("changes-tab".into()),
|
||||
active_tab == GitPanelTab::Changes,
|
||||
true,
|
||||
"Changes".into(),
|
||||
GitPanelTab::Changes,
|
||||
))
|
||||
.child(Divider::vertical().color(ui::DividerColor::BorderFaded))
|
||||
.child(tab(
|
||||
ElementId::Name("history-tab".into()),
|
||||
active_tab != GitPanelTab::Changes,
|
||||
false,
|
||||
"History".into(),
|
||||
GitPanelTab::History,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_history_tab(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().flex_1().size_full().overflow_hidden().map(|this| {
|
||||
if let Some(history) = self.render_commit_history(window, cx) {
|
||||
this.child(history)
|
||||
} else {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_center()
|
||||
.child(Label::new("Loading Commit History…").color(Color::Muted)),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn select_next_history_entry(&mut self, cx: &mut Context<Self>) {
|
||||
let count = self.commit_history_shas.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let new_index = match self.focused_history_entry {
|
||||
None => 0,
|
||||
Some(i) => (i + 1).min(count - 1),
|
||||
};
|
||||
self.focused_history_entry = Some(new_index);
|
||||
self.commit_history_scroll_handle
|
||||
.scroll_to_item(new_index, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_previous_history_entry(&mut self, cx: &mut Context<Self>) {
|
||||
let count = self.commit_history_shas.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let new_index = match self.focused_history_entry {
|
||||
None => 0,
|
||||
Some(i) => i.saturating_sub(1),
|
||||
};
|
||||
self.focused_history_entry = Some(new_index);
|
||||
self.commit_history_scroll_handle
|
||||
.scroll_to_item(new_index, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_selected_history_commit(&self, window: &mut Window, cx: &mut App) {
|
||||
let Some(index) = self.focused_history_entry else {
|
||||
return;
|
||||
};
|
||||
let Some(sha) = self.commit_history_shas.get(index) else {
|
||||
return;
|
||||
};
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
CommitView::open(
|
||||
sha.to_string(),
|
||||
active_repository.downgrade(),
|
||||
self.workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn set_active_tab(&mut self, tab: GitPanelTab, cx: &mut Context<Self>) {
|
||||
if self.active_tab == tab {
|
||||
return;
|
||||
}
|
||||
self.active_tab = tab;
|
||||
match tab {
|
||||
GitPanelTab::History => {
|
||||
self.load_commit_history(cx);
|
||||
self.focused_history_entry = Some(0);
|
||||
}
|
||||
GitPanelTab::Changes => {
|
||||
self.commit_history_shas.clear();
|
||||
self.focused_history_entry = None;
|
||||
self._repo_subscriptions.clear();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn preload_commit_history(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(branch) = active_repository.read(cx).branch.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let branch_name = branch.name().to_string();
|
||||
let log_source = LogSource::Branch(branch_name.into());
|
||||
let log_order = LogOrder::DateOrder;
|
||||
|
||||
// Kick off the git log fetch so data is ready when the user switches to History.
|
||||
// graph_data() is idempotent — if already loading/loaded, this is a no-op.
|
||||
active_repository.update(cx, |repository, cx| {
|
||||
repository.graph_data(log_source, log_order, 0..0, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn load_commit_history(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self._repo_subscriptions.is_empty() {
|
||||
self._repo_subscriptions.push(cx.subscribe(
|
||||
active_repository,
|
||||
|this, _repo, event, cx| {
|
||||
if let RepositoryEvent::GraphEvent(_, _) = event {
|
||||
if this.active_tab == GitPanelTab::History {
|
||||
this.fetch_commit_history_shas(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
self._repo_subscriptions
|
||||
.push(cx.observe(active_repository, |_this, _repo, cx| {
|
||||
cx.notify();
|
||||
}));
|
||||
}
|
||||
|
||||
self.fetch_commit_history_shas(cx);
|
||||
}
|
||||
|
||||
fn fetch_commit_history_shas(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(branch) = active_repository.read(cx).branch.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let branch_name = branch.name().to_string();
|
||||
let log_source = LogSource::Branch(branch_name.into());
|
||||
let log_order = LogOrder::DateOrder;
|
||||
|
||||
self.commit_history_shas = active_repository.update(cx, |repository, cx| {
|
||||
let response = repository.graph_data(log_source, log_order, 0..usize::MAX, cx);
|
||||
response.commits.iter().map(|commit| commit.sha).collect()
|
||||
});
|
||||
}
|
||||
|
||||
fn git_remote(&self, cx: &mut App) -> Option<GitRemote> {
|
||||
let repo = self.active_repository.as_ref()?;
|
||||
let remote_url = repo.read(cx).default_remote_url()?;
|
||||
let provider_registry = GitHostingProviderRegistry::default_global(cx);
|
||||
let (provider, parsed) = parse_git_remote_url(provider_registry, &remote_url)?;
|
||||
Some(GitRemote {
|
||||
host: provider,
|
||||
owner: parsed.owner.into(),
|
||||
repo: parsed.repo.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_commit_history(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if self.commit_history_shas.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_repository = self.active_repository.as_ref()?;
|
||||
let workspace = self.workspace.clone();
|
||||
let repo_weak = active_repository.downgrade();
|
||||
let shas = self.commit_history_shas.clone();
|
||||
let item_count = shas.len();
|
||||
let commit_history_scroll_handle = self.commit_history_scroll_handle.clone();
|
||||
let remote = self.git_remote(cx);
|
||||
|
||||
let focused_history_entry = self.focused_history_entry;
|
||||
let is_panel_focused = self.focus_handle.is_focused(window);
|
||||
|
||||
let ahead_count = active_repository
|
||||
.read(cx)
|
||||
.branch
|
||||
.as_ref()
|
||||
.and_then(|b| b.upstream.as_ref())
|
||||
.and_then(|u| u.tracking.status())
|
||||
.map(|s| s.ahead as usize)
|
||||
.unwrap_or(0);
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list("commit_history_list", item_count, {
|
||||
let workspace = workspace;
|
||||
let repo_weak = repo_weak;
|
||||
move |range, window, cx| {
|
||||
let local_offset = time::UtcOffset::current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC);
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
let visible_data: Vec<Option<Arc<CommitData>>> = repo_weak
|
||||
.update(cx, |repository, cx| {
|
||||
shas[range.clone()]
|
||||
.iter()
|
||||
.map(|sha| {
|
||||
match repository.fetch_commit_data(*sha, false, cx) {
|
||||
CommitDataState::Loaded(data) => Some(data.clone()),
|
||||
CommitDataState::Loading(_) => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
shas[range.clone()]
|
||||
.iter()
|
||||
.zip(visible_data)
|
||||
.enumerate()
|
||||
.map(|(ix, (sha, data))| {
|
||||
let index = range.start + ix;
|
||||
let sha_string = sha.to_string();
|
||||
let sha_shared: SharedString = sha_string.clone().into();
|
||||
let short_sha: SharedString =
|
||||
sha_string[..7.min(sha_string.len())].to_string().into();
|
||||
|
||||
let (subject, author_name, author_email, timestamp): (
|
||||
SharedString,
|
||||
SharedString,
|
||||
Option<SharedString>,
|
||||
Option<i64>,
|
||||
) = match &data {
|
||||
Some(data) => (
|
||||
data.subject.clone(),
|
||||
data.author_name.clone(),
|
||||
Some(data.author_email.clone()),
|
||||
Some(data.commit_timestamp),
|
||||
),
|
||||
None => ("Loading…".into(), "".into(), None, None),
|
||||
};
|
||||
|
||||
let relative_time: SharedString = timestamp
|
||||
.and_then(|ts| {
|
||||
time::OffsetDateTime::from_unix_timestamp(ts).ok()
|
||||
})
|
||||
.map(|dt| {
|
||||
time_format::format_localized_timestamp(
|
||||
dt,
|
||||
now,
|
||||
local_offset,
|
||||
time_format::TimestampFormat::Relative,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|| "".into());
|
||||
|
||||
let avatar = CommitAvatar::new(
|
||||
&sha_shared,
|
||||
author_email,
|
||||
remote.as_ref(),
|
||||
)
|
||||
.size(px(14.))
|
||||
.render(window, cx);
|
||||
|
||||
let is_unpushed = index < ahead_count;
|
||||
let is_focused = focused_history_entry == Some(index);
|
||||
let workspace = workspace.clone();
|
||||
let repo = repo_weak.clone();
|
||||
let sha_for_click = sha_string;
|
||||
|
||||
let dot_separator = || {
|
||||
Label::new("•")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.alpha(0.5)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(("commit-history-item", index))
|
||||
.cursor_pointer()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_0p5()
|
||||
.border_1()
|
||||
.border_color(gpui::transparent_black())
|
||||
.when(is_focused && is_panel_focused, |this| {
|
||||
this.border_color(
|
||||
cx.theme().colors().panel_focused_border,
|
||||
)
|
||||
})
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Label::new(subject).truncate())
|
||||
.when(is_unpushed, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::ArrowUp)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(avatar)
|
||||
.when(!author_name.is_empty(), |this| {
|
||||
this.child(
|
||||
Label::new(author_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(dot_separator())
|
||||
})
|
||||
.when(!relative_time.is_empty(), |this| {
|
||||
this.child(
|
||||
Label::new(relative_time)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(dot_separator())
|
||||
})
|
||||
.child(
|
||||
Label::new(short_sha.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"View Commit",
|
||||
None,
|
||||
short_sha.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_, window, cx| {
|
||||
CommitView::open(
|
||||
sha_for_click.clone(),
|
||||
repo.clone(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.size_full()
|
||||
.track_scroll(&commit_history_scroll_handle),
|
||||
)
|
||||
.vertical_scrollbar_for(&commit_history_scroll_handle, window, cx),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = match (self.git_access, &self.active_repository) {
|
||||
(GitAccess::No, Some(repository)) => self.render_unsafe_repo_ui(repository, cx),
|
||||
|
|
@ -6080,28 +6536,35 @@ impl Render for GitPanel {
|
|||
v_flex()
|
||||
.size_full()
|
||||
.when(!self.commit_editor_expanded, |this| {
|
||||
this.children(self.render_panel_header(window, cx))
|
||||
.map(|this| {
|
||||
if let Some(repo) = self.active_repository.clone()
|
||||
&& has_entries
|
||||
{
|
||||
this.child(self.render_entries(
|
||||
has_write_access,
|
||||
repo,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
this.child(self.render_empty_state(cx).into_any_element())
|
||||
}
|
||||
this.child(self.render_tab_bar(cx))
|
||||
})
|
||||
.map(|this| match self.active_tab {
|
||||
GitPanelTab::Changes => this
|
||||
.children(self.render_changes_header(window, cx))
|
||||
.when(!self.commit_editor_expanded, |this| {
|
||||
this.map(|this| {
|
||||
if let Some(repo) = self.active_repository.clone()
|
||||
&& has_entries
|
||||
{
|
||||
this.child(self.render_entries(
|
||||
has_write_access,
|
||||
repo,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
this.child(self.render_empty_state(cx).into_any_element())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.children(self.render_footer(window, cx))
|
||||
.when(self.amend_pending, |this| {
|
||||
this.child(self.render_pending_amend(cx))
|
||||
})
|
||||
.when(!self.amend_pending, |this| {
|
||||
this.children(self.render_previous_commit(window, cx))
|
||||
.children(self.render_footer(window, cx))
|
||||
.when(self.amend_pending, |this| {
|
||||
this.child(self.render_pending_amend(cx))
|
||||
})
|
||||
.when(!self.amend_pending, |this| {
|
||||
this.children(self.render_previous_commit(window, cx))
|
||||
}),
|
||||
GitPanelTab::History => this.child(self.render_history_tab(window, cx)),
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
//! # panel
|
||||
use gpui::actions;
|
||||
use ui::{Tab, prelude::*};
|
||||
use ui::prelude::*;
|
||||
|
||||
actions!(
|
||||
panel,
|
||||
|
|
@ -12,19 +11,7 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
pub trait PanelHeader: workspace::Panel {
|
||||
fn header_height(&self, cx: &mut App) -> Pixels {
|
||||
Tab::container_height(cx)
|
||||
}
|
||||
|
||||
fn panel_header_container(&self, _window: &mut Window, cx: &mut App) -> Div {
|
||||
h_flex()
|
||||
.h(self.header_height(cx))
|
||||
.w_full()
|
||||
.px_1()
|
||||
.flex_none()
|
||||
}
|
||||
}
|
||||
pub trait PanelHeader: workspace::Panel {}
|
||||
|
||||
/// Implement this trait to enable a panel to have tabs.
|
||||
pub trait PanelTabs: PanelHeader {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue