From d564495dde0c344fa54e2432b91680fc604e495c Mon Sep 17 00:00:00 2001 From: Ruslan Semagin Date: Tue, 7 Jul 2026 16:50:03 +0300 Subject: [PATCH] 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 --- crates/git_ui/src/git_panel.rs | 124 ++++++++++++++++++++++++++------- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b1667adff59..590efaca2b7 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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>, + commit_history_entries: Option>, focused_history_entry: Option, history_keyboard_nav: bool, _commit_message_buffer_subscription: Option, @@ -822,6 +824,25 @@ struct BulkStaging { anchor: RepoPath, } +#[derive(Clone)] +struct CommitHistoryEntry { + sha: Oid, + tag_names: Vec, +} + +impl From<&Arc> for CommitHistoryEntry { + fn from(commit: &Arc) -> 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) { - 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) { - 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) { + fn fetch_commit_history_entries(&mut self, cx: &mut Context) { 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, ) -> Option { - 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>> = 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)