use collections::{BTreeMap, HashMap};use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};use git::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote, parse_git_remote_url, repository::{CommitDiff, InitialGraphCommitData, LogOrder, LogSource},};use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView};use gpui::{ AnyElement, App, Bounds, ClipboardItem, Context, Corner, DefiniteLength, ElementId, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, ParentElement, PathBuilder, Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, anchored, deferred, point, px,};use menu::{SelectNext, SelectPrevious};use project::{ Project, git_store::{CommitDataState, GitStoreEvent, Repository, RepositoryEvent},};use settings::Settings;use smallvec::{SmallVec, smallvec};use std::{ops::Range, rc::Rc, sync::Arc, sync::OnceLock};use theme::{AccentColors, ThemeSettings};use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};use ui::{ CommonAnimationExt as _, ContextMenu, ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, prelude::*,};use workspace::{ Workspace, item::{Item, ItemEvent, SerializableItem},}; const COMMIT_CIRCLE_RADIUS: Pixels = px(4.5);const COMMIT_CIRCLE_STROKE_WIDTH: Pixels = px(1.5);const LANE_WIDTH: Pixels = px(16.0);const LEFT_PADDING: Pixels = px(12.0);const LINE_WIDTH: Pixels = px(1.5); actions!( git_graph, [ /// Opens the commit view for the selected commit. OpenCommitView, ]); pub struct GitGraphFeatureFlag; impl FeatureFlag for GitGraphFeatureFlag { const NAME: &'static str = "git-graph";} fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] { static FORMAT: OnceLock<Vec<BorrowedFormatItem<'static>>> = OnceLock::new(); FORMAT.get_or_init(|| { time::format_description::parse("[day] [month repr:short] [year] [hour]:[minute]") .unwrap_or_default() })} fn format_timestamp(timestamp: i64) -> String { let Ok(datetime) = OffsetDateTime::from_unix_timestamp(timestamp) else { return "Unknown".to_string(); }; let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); let local_datetime = datetime.to_offset(local_offset); local_datetime .format(timestamp_format()) .unwrap_or_default()} fn accent_colors_count(accents: &AccentColors) -> usize { accents.0.len()} #[derive(Copy, Clone, Debug)]struct BranchColor(u8); #[derive(Debug)]enum LaneState { Empty, Active { child: Oid, parent: Oid, color: Option<BranchColor>, starting_row: usize, starting_col: usize, destination_column: Option<usize>, segments: SmallVec<[CommitLineSegment; 1]>, },} impl LaneState { fn to_commit_lines( &mut self, ending_row: usize, lane_column: usize, parent_column: usize, parent_color: BranchColor, ) -> Option<CommitLine> { let state = std::mem::replace(self, LaneState::Empty); match state { LaneState::Active { #[cfg_attr(not(test), allow(unused_variables))] parent, #[cfg_attr(not(test), allow(unused_variables))] child, color, starting_row, starting_col, destination_column, mut segments, } => { let final_destination = destination_column.unwrap_or(parent_column); let final_color = color.unwrap_or(parent_color); Some(CommitLine { #[cfg(test)] child, #[cfg(test)] parent, child_column: starting_col, full_interval: starting_row..ending_row, color_idx: final_color.0 as usize, segments: { match segments.last_mut() { Some(CommitLineSegment::Straight { to_row }) if *to_row == usize::MAX => { if final_destination != lane_column { *to_row = ending_row - 1; let curved_line = CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }; if *to_row == starting_row { let last_index = segments.len() - 1; segments[last_index] = curved_line; } else { segments.push(curved_line); } } else { *to_row = ending_row; } } Some(CommitLineSegment::Curve { on_row, to_column, curve_kind, }) if *on_row == usize::MAX => { if *to_column == usize::MAX { *to_column = final_destination; } if matches!(curve_kind, CurveKind::Merge) { *on_row = starting_row + 1; if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } else { *on_row = ending_row; if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } } Some(CommitLineSegment::Curve { on_row, to_column, .. }) => { if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } _ => {} } segments }, }) } LaneState::Empty => None, } } fn is_empty(&self) -> bool { match self { LaneState::Empty => true, LaneState::Active { .. } => false, } }} struct CommitEntry { data: Arc<InitialGraphCommitData>, lane: usize, color_idx: usize,} type ActiveLaneIdx = usize; enum AllCommitCount { NotLoaded, Loaded(usize),} #[derive(Debug)]enum CurveKind { Merge, Checkout,} #[derive(Debug)]enum CommitLineSegment { Straight { to_row: usize, }, Curve { to_column: usize, on_row: usize, curve_kind: CurveKind, },} #[derive(Debug)]struct CommitLine { #[cfg(test)] child: Oid, #[cfg(test)] parent: Oid, child_column: usize, full_interval: Range<usize>, color_idx: usize, segments: SmallVec<[CommitLineSegment; 1]>,} impl CommitLine { fn get_first_visible_segment_idx(&self, first_visible_row: usize) -> Option<(usize, usize)> { if first_visible_row > self.full_interval.end { return None; } else if first_visible_row <= self.full_interval.start { return Some((0, self.child_column)); } let mut current_column = self.child_column; for (idx, segment) in self.segments.iter().enumerate() { match segment { CommitLineSegment::Straight { to_row } => { if *to_row >= first_visible_row { return Some((idx, current_column)); } } CommitLineSegment::Curve { to_column, on_row, .. } => { if *on_row >= first_visible_row { return Some((idx, current_column)); } current_column = *to_column; } } } None }} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]struct CommitLineKey { child: Oid, parent: Oid,} struct GraphData { lane_states: SmallVec<[LaneState; 8]>, lane_colors: HashMap<ActiveLaneIdx, BranchColor>, parent_to_lanes: HashMap<Oid, SmallVec<[usize; 1]>>, next_color: BranchColor, accent_colors_count: usize, commits: Vec<Rc<CommitEntry>>, max_commit_count: AllCommitCount, max_lanes: usize, lines: Vec<Rc<CommitLine>>, active_commit_lines: HashMap<CommitLineKey, usize>, active_commit_lines_by_parent: HashMap<Oid, SmallVec<[usize; 1]>>,} impl GraphData { fn new(accent_colors_count: usize) -> Self { GraphData { lane_states: SmallVec::default(), lane_colors: HashMap::default(), parent_to_lanes: HashMap::default(), next_color: BranchColor(0), accent_colors_count, commits: Vec::default(), max_commit_count: AllCommitCount::NotLoaded, max_lanes: 0, lines: Vec::default(), active_commit_lines: HashMap::default(), active_commit_lines_by_parent: HashMap::default(), } } fn clear(&mut self) { self.lane_states.clear(); self.lane_colors.clear(); self.parent_to_lanes.clear(); self.commits.clear(); self.lines.clear(); self.active_commit_lines.clear(); self.active_commit_lines_by_parent.clear(); self.next_color = BranchColor(0); self.max_commit_count = AllCommitCount::NotLoaded; self.max_lanes = 0; } fn first_empty_lane_idx(&mut self) -> ActiveLaneIdx { self.lane_states .iter() .position(LaneState::is_empty) .unwrap_or_else(|| { self.lane_states.push(LaneState::Empty); self.lane_states.len() - 1 }) } fn get_lane_color(&mut self, lane_idx: ActiveLaneIdx) -> BranchColor { let accent_colors_count = self.accent_colors_count; *self.lane_colors.entry(lane_idx).or_insert_with(|| { let color_idx = self.next_color; self.next_color = BranchColor((self.next_color.0 + 1) % accent_colors_count as u8); color_idx }) } fn add_commits(&mut self, commits: &[Arc<InitialGraphCommitData>]) { self.commits.reserve(commits.len()); self.lines.reserve(commits.len() / 2); for commit in commits.iter() { let commit_row = self.commits.len(); let commit_lane = self .parent_to_lanes .get(&commit.sha) .and_then(|lanes| lanes.first().copied()); let commit_lane = commit_lane.unwrap_or_else(|| self.first_empty_lane_idx()); let commit_color = self.get_lane_color(commit_lane); if let Some(lanes) = self.parent_to_lanes.remove(&commit.sha) { for lane_column in lanes { let state = &mut self.lane_states[lane_column]; if let LaneState::Active { starting_row, segments, .. } = state { if let Some(CommitLineSegment::Curve { to_column, curve_kind: CurveKind::Merge, .. }) = segments.first_mut() { let curve_row = *starting_row + 1; let would_overlap = if lane_column != commit_lane && curve_row < commit_row { self.commits[curve_row..commit_row] .iter() .any(|c| c.lane == commit_lane) } else { false }; if would_overlap { *to_column = lane_column; } } } if let Some(commit_line) = state.to_commit_lines(commit_row, lane_column, commit_lane, commit_color) { self.lines.push(Rc::new(commit_line)); } } } commit .parents .iter() .enumerate() .for_each(|(parent_idx, parent)| { if parent_idx == 0 { self.lane_states[commit_lane] = LaneState::Active { parent: *parent, child: commit.sha, color: Some(commit_color), starting_col: commit_lane, starting_row: commit_row, destination_column: None, segments: smallvec![CommitLineSegment::Straight { to_row: usize::MAX }], }; self.parent_to_lanes .entry(*parent) .or_default() .push(commit_lane); } else { let new_lane = self.first_empty_lane_idx(); self.lane_states[new_lane] = LaneState::Active { parent: *parent, child: commit.sha, color: None, starting_col: commit_lane, starting_row: commit_row, destination_column: None, segments: smallvec![CommitLineSegment::Curve { to_column: usize::MAX, on_row: usize::MAX, curve_kind: CurveKind::Merge, },], }; self.parent_to_lanes .entry(*parent) .or_default() .push(new_lane); } }); self.max_lanes = self.max_lanes.max(self.lane_states.len()); self.commits.push(Rc::new(CommitEntry { data: commit.clone(), lane: commit_lane, color_idx: commit_color.0 as usize, })); } }} pub fn init(cx: &mut App) { workspace::register_serializable_item::<GitGraph>(cx);Lines 1–500 of 2496. Load more
use collections::{BTreeMap, HashMap}; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use git::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote, parse_git_remote_url, repository::{CommitDiff, InitialGraphCommitData, LogOrder, LogSource}, }; use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView}; use gpui::{ AnyElement, App, Bounds, ClipboardItem, Context, Corner, DefiniteLength, ElementId, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, ParentElement, PathBuilder, Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, anchored, deferred, point, px, }; use menu::{SelectNext, SelectPrevious}; use project::{ Project, git_store::{CommitDataState, GitStoreEvent, Repository, RepositoryEvent}, }; use settings::Settings; use smallvec::{SmallVec, smallvec}; use std::{ops::Range, rc::Rc, sync::Arc, sync::OnceLock}; use theme::{AccentColors, ThemeSettings}; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; use ui::{ CommonAnimationExt as _, ContextMenu, ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, prelude::*, }; use workspace::{ Workspace, item::{Item, ItemEvent, SerializableItem}, }; const COMMIT_CIRCLE_RADIUS: Pixels = px(4.5); const COMMIT_CIRCLE_STROKE_WIDTH: Pixels = px(1.5); const LANE_WIDTH: Pixels = px(16.0); const LEFT_PADDING: Pixels = px(12.0); const LINE_WIDTH: Pixels = px(1.5); actions!( git_graph, [ /// Opens the commit view for the selected commit. OpenCommitView, ] ); pub struct GitGraphFeatureFlag; impl FeatureFlag for GitGraphFeatureFlag { const NAME: &'static str = "git-graph"; } fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] { static FORMAT: OnceLock<Vec<BorrowedFormatItem<'static>>> = OnceLock::new(); FORMAT.get_or_init(|| { time::format_description::parse("[day] [month repr:short] [year] [hour]:[minute]") .unwrap_or_default() }) } fn format_timestamp(timestamp: i64) -> String { let Ok(datetime) = OffsetDateTime::from_unix_timestamp(timestamp) else { return "Unknown".to_string(); }; let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); let local_datetime = datetime.to_offset(local_offset); local_datetime .format(timestamp_format()) .unwrap_or_default() } fn accent_colors_count(accents: &AccentColors) -> usize { accents.0.len() } #[derive(Copy, Clone, Debug)] struct BranchColor(u8); #[derive(Debug)] enum LaneState { Empty, Active { child: Oid, parent: Oid, color: Option<BranchColor>, starting_row: usize, starting_col: usize, destination_column: Option<usize>, segments: SmallVec<[CommitLineSegment; 1]>, }, } impl LaneState { fn to_commit_lines( &mut self, ending_row: usize, lane_column: usize, parent_column: usize, parent_color: BranchColor, ) -> Option<CommitLine> { let state = std::mem::replace(self, LaneState::Empty); match state { LaneState::Active { #[cfg_attr(not(test), allow(unused_variables))] parent, #[cfg_attr(not(test), allow(unused_variables))] child, color, starting_row, starting_col, destination_column, mut segments, } => { let final_destination = destination_column.unwrap_or(parent_column); let final_color = color.unwrap_or(parent_color); Some(CommitLine { #[cfg(test)] child, #[cfg(test)] parent, child_column: starting_col, full_interval: starting_row..ending_row, color_idx: final_color.0 as usize, segments: { match segments.last_mut() { Some(CommitLineSegment::Straight { to_row }) if *to_row == usize::MAX => { if final_destination != lane_column { *to_row = ending_row - 1; let curved_line = CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }; if *to_row == starting_row { let last_index = segments.len() - 1; segments[last_index] = curved_line; } else { segments.push(curved_line); } } else { *to_row = ending_row; } } Some(CommitLineSegment::Curve { on_row, to_column, curve_kind, }) if *on_row == usize::MAX => { if *to_column == usize::MAX { *to_column = final_destination; } if matches!(curve_kind, CurveKind::Merge) { *on_row = starting_row + 1; if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } else { *on_row = ending_row; if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } } Some(CommitLineSegment::Curve { on_row, to_column, .. }) => { if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } _ => {} } segments }, }) } LaneState::Empty => None, } } fn is_empty(&self) -> bool { match self { LaneState::Empty => true, LaneState::Active { .. } => false, } } } struct CommitEntry { data: Arc<InitialGraphCommitData>, lane: usize, color_idx: usize, } type ActiveLaneIdx = usize; enum AllCommitCount { NotLoaded, Loaded(usize), } #[derive(Debug)] enum CurveKind { Merge, Checkout, } #[derive(Debug)] enum CommitLineSegment { Straight { to_row: usize, }, Curve { to_column: usize, on_row: usize, curve_kind: CurveKind, }, } #[derive(Debug)] struct CommitLine { #[cfg(test)] child: Oid, #[cfg(test)] parent: Oid, child_column: usize, full_interval: Range<usize>, color_idx: usize, segments: SmallVec<[CommitLineSegment; 1]>, } impl CommitLine { fn get_first_visible_segment_idx(&self, first_visible_row: usize) -> Option<(usize, usize)> { if first_visible_row > self.full_interval.end { return None; } else if first_visible_row <= self.full_interval.start { return Some((0, self.child_column)); } let mut current_column = self.child_column; for (idx, segment) in self.segments.iter().enumerate() { match segment { CommitLineSegment::Straight { to_row } => { if *to_row >= first_visible_row { return Some((idx, current_column)); } } CommitLineSegment::Curve { to_column, on_row, .. } => { if *on_row >= first_visible_row { return Some((idx, current_column)); } current_column = *to_column; } } } None } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct CommitLineKey { child: Oid, parent: Oid, } struct GraphData { lane_states: SmallVec<[LaneState; 8]>, lane_colors: HashMap<ActiveLaneIdx, BranchColor>, parent_to_lanes: HashMap<Oid, SmallVec<[usize; 1]>>, next_color: BranchColor, accent_colors_count: usize, commits: Vec<Rc<CommitEntry>>, max_commit_count: AllCommitCount, max_lanes: usize, lines: Vec<Rc<CommitLine>>, active_commit_lines: HashMap<CommitLineKey, usize>, active_commit_lines_by_parent: HashMap<Oid, SmallVec<[usize; 1]>>, } impl GraphData { fn new(accent_colors_count: usize) -> Self { GraphData { lane_states: SmallVec::default(), lane_colors: HashMap::default(), parent_to_lanes: HashMap::default(), next_color: BranchColor(0), accent_colors_count, commits: Vec::default(), max_commit_count: AllCommitCount::NotLoaded, max_lanes: 0, lines: Vec::default(), active_commit_lines: HashMap::default(), active_commit_lines_by_parent: HashMap::default(), } } fn clear(&mut self) { self.lane_states.clear(); self.lane_colors.clear(); self.parent_to_lanes.clear(); self.commits.clear(); self.lines.clear(); self.active_commit_lines.clear(); self.active_commit_lines_by_parent.clear(); self.next_color = BranchColor(0); self.max_commit_count = AllCommitCount::NotLoaded; self.max_lanes = 0; } fn first_empty_lane_idx(&mut self) -> ActiveLaneIdx { self.lane_states .iter() .position(LaneState::is_empty) .unwrap_or_else(|| { self.lane_states.push(LaneState::Empty); self.lane_states.len() - 1 }) } fn get_lane_color(&mut self, lane_idx: ActiveLaneIdx) -> BranchColor { let accent_colors_count = self.accent_colors_count; *self.lane_colors.entry(lane_idx).or_insert_with(|| { let color_idx = self.next_color; self.next_color = BranchColor((self.next_color.0 + 1) % accent_colors_count as u8); color_idx }) } fn add_commits(&mut self, commits: &[Arc<InitialGraphCommitData>]) { self.commits.reserve(commits.len()); self.lines.reserve(commits.len() / 2); for commit in commits.iter() { let commit_row = self.commits.len(); let commit_lane = self .parent_to_lanes .get(&commit.sha) .and_then(|lanes| lanes.first().copied()); let commit_lane = commit_lane.unwrap_or_else(|| self.first_empty_lane_idx()); let commit_color = self.get_lane_color(commit_lane); if let Some(lanes) = self.parent_to_lanes.remove(&commit.sha) { for lane_column in lanes { let state = &mut self.lane_states[lane_column]; if let LaneState::Active { starting_row, segments, .. } = state { if let Some(CommitLineSegment::Curve { to_column, curve_kind: CurveKind::Merge, .. }) = segments.first_mut() { let curve_row = *starting_row + 1; let would_overlap = if lane_column != commit_lane && curve_row < commit_row { self.commits[curve_row..commit_row] .iter() .any(|c| c.lane == commit_lane) } else { false }; if would_overlap { *to_column = lane_column; } } } if let Some(commit_line) = state.to_commit_lines(commit_row, lane_column, commit_lane, commit_color) { self.lines.push(Rc::new(commit_line)); } } } commit .parents .iter() .enumerate() .for_each(|(parent_idx, parent)| { if parent_idx == 0 { self.lane_states[commit_lane] = LaneState::Active { parent: *parent, child: commit.sha, color: Some(commit_color), starting_col: commit_lane, starting_row: commit_row, destination_column: None, segments: smallvec![CommitLineSegment::Straight { to_row: usize::MAX }], }; self.parent_to_lanes .entry(*parent) .or_default() .push(commit_lane); } else { let new_lane = self.first_empty_lane_idx(); self.lane_states[new_lane] = LaneState::Active { parent: *parent, child: commit.sha, color: None, starting_col: commit_lane, starting_row: commit_row, destination_column: None, segments: smallvec![CommitLineSegment::Curve { to_column: usize::MAX, on_row: usize::MAX, curve_kind: CurveKind::Merge, },], }; self.parent_to_lanes .entry(*parent) .or_default() .push(new_lane); } }); self.max_lanes = self.max_lanes.max(self.lane_states.len()); self.commits.push(Rc::new(CommitEntry { data: commit.clone(), lane: commit_lane, color_idx: commit_color.0 as usize, })); } } } pub fn init(cx: &mut App) { workspace::register_serializable_item::<GitGraph>(cx);