git_ui: Show tags in Git panel history (#60534)

# Objective

- Make Git Panel History show commit tags so release/version markers are
visible without opening the full Git graph or commit details.

  ## Solution

- Store commit history entries with both the commit SHA and tag names
from existing git graph data.
  - Render tag names as muted chips next to the commit subject.
- Limit visible tags to 3 per commit and show `+N` for additional tags.

  ## Testing

  - Ran `cargo check -p git_ui`.
  - Manually verified on Linux with `script/zed-local --stateful .`.
  - Confirmed tags appear in Git Panel History.

  ## Self-Review Checklist:

  - [x] I've reviewed my own diff for quality, security, and reliability
  - [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and [icon]

(https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
  - [ ] Tests cover the new/changed behavior
  - [x] Performance impact has been considered and is acceptable

  ## Showcase

Git Panel History now shows commit tags inline as muted chips next to
the commit subject.

  ---

  Release Notes:

  - Added tag labels to the Git Panel commit history.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Ruslan Semagin 2026-07-07 16:50:03 +03:00 committed by GitHub
parent fc827a218e
commit d564495dde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -27,8 +27,9 @@ use git::Oid;
use git::commit::ParsedCommitMessage;
use git::repository::{
Branch, CommitData, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions,
GitCommitTemplate, GitCommitter, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput,
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer,
GitCommitTemplate, GitCommitter, InitialGraphCommitData, LogOrder, LogSource, PushOptions,
Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
get_git_committer,
};
use git::stash::GitStash;
use git::status::{DiffStat, StageStatus};
@ -80,7 +81,7 @@ use strum::{IntoEnumIterator, VariantNames};
use theme_settings::ThemeSettings;
use time::OffsetDateTime;
use ui::{
ButtonLike, Checkbox, ContextMenu, ContextMenuEntry, Divider, ElevationIndex,
ButtonLike, Checkbox, Chip, ContextMenu, ContextMenuEntry, Divider, ElevationIndex,
IndentGuideColors, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, ScrollAxes,
Scrollbars, SplitButton, Tab, TintColor, Tooltip, WithScrollbar, prelude::*,
};
@ -98,6 +99,7 @@ const GIT_PANEL_KEY: &str = "GitPanel";
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel
const TREE_INDENT: f32 = 16.0;
const MAX_HISTORY_TAG_CHIPS: usize = 3;
// Horizontal offset that aligns the tree indent guides with the row icon column.
const INDENT_GUIDE_LEFT_OFFSET: gpui::Pixels = gpui::px(19.);
@ -804,7 +806,7 @@ pub struct GitPanel {
stash_entries: GitStash,
active_tab: GitPanelTab,
commit_history_scroll_handle: UniformListScrollHandle,
commit_history_shas: Option<Vec<Oid>>,
commit_history_entries: Option<Rc<[CommitHistoryEntry]>>,
focused_history_entry: Option<usize>,
history_keyboard_nav: bool,
_commit_message_buffer_subscription: Option<Subscription>,
@ -822,6 +824,25 @@ struct BulkStaging {
anchor: RepoPath,
}
#[derive(Clone)]
struct CommitHistoryEntry {
sha: Oid,
tag_names: Vec<SharedString>,
}
impl From<&Arc<InitialGraphCommitData>> for CommitHistoryEntry {
fn from(commit: &Arc<InitialGraphCommitData>) -> Self {
Self {
sha: commit.sha,
tag_names: commit
.tag_names()
.into_iter()
.map(|tag_name| SharedString::from(tag_name.to_string()))
.collect(),
}
}
}
const MAX_PANEL_EDITOR_LINES: usize = 6;
pub(crate) fn commit_message_editor(
@ -1087,7 +1108,7 @@ impl GitPanel {
stash_entries: Default::default(),
active_tab: GitPanelTab::Changes,
commit_history_scroll_handle: UniformListScrollHandle::new(),
commit_history_shas: None,
commit_history_entries: None,
focused_history_entry: None,
history_keyboard_nav: false,
_commit_message_buffer_subscription: None,
@ -5678,10 +5699,10 @@ impl GitPanel {
v_flex().flex_1().size_full().overflow_hidden().map(|this| {
let has_repo = self.active_repository.is_some();
let has_commits = self
.commit_history_shas
.commit_history_entries
.as_ref()
.map_or(false, |shas| !shas.is_empty());
let is_loading = self.commit_history_shas.is_none() && has_repo;
.map_or(false, |entries| !entries.is_empty());
let is_loading = self.commit_history_entries.is_none() && has_repo;
if is_loading {
this.child(
h_flex()
@ -5711,7 +5732,10 @@ impl GitPanel {
}
fn select_next_history_entry(&mut self, cx: &mut Context<Self>) {
let count = self.commit_history_shas.as_ref().map_or(0, Vec::len);
let count = self
.commit_history_entries
.as_ref()
.map_or(0, |entries| entries.len());
if count == 0 {
return;
}
@ -5727,7 +5751,10 @@ impl GitPanel {
}
fn select_previous_history_entry(&mut self, cx: &mut Context<Self>) {
let count = self.commit_history_shas.as_ref().map_or(0, Vec::len);
let count = self
.commit_history_entries
.as_ref()
.map_or(0, |entries| entries.len());
if count == 0 {
return;
}
@ -5746,14 +5773,18 @@ impl GitPanel {
let Some(index) = self.focused_history_entry else {
return;
};
let Some(sha) = self.commit_history_shas.as_ref().and_then(|s| s.get(index)) else {
let Some(entry) = self
.commit_history_entries
.as_ref()
.and_then(|entries| entries.get(index))
else {
return;
};
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
CommitView::open(
sha.to_string(),
entry.sha.to_string(),
active_repository.downgrade(),
self.workspace.clone(),
None,
@ -5794,7 +5825,7 @@ impl GitPanel {
}
GitPanelTab::Changes => {
self.focus_handle.focus(window, cx);
self.commit_history_shas.take();
self.commit_history_entries.take();
self.focused_history_entry = None;
self._repo_subscriptions.clear();
}
@ -5833,7 +5864,7 @@ impl GitPanel {
|this, _repo, event, cx| {
if let RepositoryEvent::GraphEvent(_, _) = event {
if this.active_tab == GitPanelTab::History {
this.fetch_commit_history_shas(cx);
this.fetch_commit_history_entries(cx);
}
}
},
@ -5844,10 +5875,10 @@ impl GitPanel {
}));
}
self.fetch_commit_history_shas(cx);
self.fetch_commit_history_entries(cx);
}
fn fetch_commit_history_shas(&mut self, cx: &mut Context<Self>) {
fn fetch_commit_history_entries(&mut self, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
@ -5860,9 +5891,13 @@ impl GitPanel {
let log_source = LogSource::Branch(branch_name.into());
let log_order = LogOrder::DateOrder;
self.commit_history_shas = Some(active_repository.update(cx, |repository, cx| {
self.commit_history_entries = Some(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()
response
.commits
.iter()
.map(CommitHistoryEntry::from)
.collect()
}));
}
@ -5883,11 +5918,11 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
let shas = self.commit_history_shas.clone()?;
let entries = self.commit_history_entries.clone()?;
let active_repository = self.active_repository.as_ref()?;
let workspace = self.workspace.clone();
let repo_weak = active_repository.downgrade();
let item_count = shas.len();
let item_count = entries.len();
let commit_history_scroll_handle = self.commit_history_scroll_handle.clone();
let remote = self.git_remote(cx);
@ -5921,10 +5956,11 @@ impl GitPanel {
let visible_data: Vec<Option<Arc<CommitData>>> = repo_weak
.update(cx, |repository, cx| {
shas[range.clone()]
entries[range.clone()]
.iter()
.map(|sha| {
match repository.fetch_commit_data(*sha, false, cx) {
.map(|entry| {
match repository.fetch_commit_data(entry.sha, false, cx)
{
CommitDataState::Loaded(data) => Some(data.clone()),
CommitDataState::Loading(_) => None,
}
@ -5933,16 +5969,17 @@ impl GitPanel {
})
.unwrap_or_default();
shas[range.clone()]
entries[range.clone()]
.iter()
.zip(visible_data)
.enumerate()
.map(|(ix, (sha, data))| {
.map(|(ix, (entry, data))| {
let index = range.start + ix;
let sha_string = sha.to_string();
let sha_string = entry.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 tag_names = entry.tag_names.clone();
let (subject, author_name, author_email, timestamp): (
SharedString,
@ -6017,7 +6054,42 @@ impl GitPanel {
h_flex()
.gap_1()
.w_full()
.min_w_0()
.child(Label::new(subject).truncate())
.children((!tag_names.is_empty()).then(|| {
let hidden_tag_count = tag_names
.len()
.saturating_sub(MAX_HISTORY_TAG_CHIPS);
h_flex()
.gap_1()
.min_w_0()
.children(
tag_names
.iter()
.take(MAX_HISTORY_TAG_CHIPS)
.cloned()
.map(|tag_name| {
Chip::new(tag_name.clone())
.truncate()
.tooltip(Tooltip::text(
tag_name,
))
}),
)
.when(hidden_tag_count > 0, |this| {
let hidden_tag_names = tag_names
[MAX_HISTORY_TAG_CHIPS..]
.join(", ");
this.child(
Chip::new(format!(
"+{hidden_tag_count}"
))
.tooltip(Tooltip::text(
hidden_tag_names,
)),
)
})
}))
.when(is_unpushed, |this| {
this.child(
Icon::new(IconName::ArrowUp)