From ef8a9bbcaf36b29d05afeb24cfe974508df05723 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:03:19 -0400 Subject: [PATCH 01/54] add: graph glyphs --- asyncgit/Cargo.toml | 2 ++ src/components/utils/graphrow.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/components/utils/graphrow.rs diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 5e77498ae3..4e0a3d110b 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -25,6 +25,7 @@ gix = { version = "0.78.0", default-features = false, features = [ "revision", "status", ] } +im = "15" log = "0.4" # git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]} # git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]} @@ -34,6 +35,7 @@ rayon = "1.11" rayon-core = "1.13" scopetime = { path = "../scopetime", version = "0.1" } serde = { version = "1.0", features = ["derive"] } +smallvec = "1" ssh-key = { version = "0.6.7", features = ["crypto", "encryption"] } thiserror = "2.0" unicode-truncate = "2.0" diff --git a/src/components/utils/graphrow.rs b/src/components/utils/graphrow.rs new file mode 100644 index 0000000000..f5d9aef8be --- /dev/null +++ b/src/components/utils/graphrow.rs @@ -0,0 +1,15 @@ +pub const SYM_COMMIT: &str = "○"; +pub const SYM_COMMIT_BRANCH: &str = "●"; +pub const SYM_COMMIT_MERGE: &str = "•"; +pub const SYM_COMMIT_STASH: &str = "◎"; +pub const SYM_COMMIT_UNCOMMITTED: &str = "◌"; +pub const SYM_VERTICAL: &str = "│"; +pub const SYM_VERTICAL_DOTTED: &str = "┊"; +pub const SYM_HORIZONTAL: &str = "─"; +pub const SYM_MERGE_BRIDGE_START: &str = "╮"; +pub const SYM_MERGE_BRIDGE_MID: &str = "─"; +pub const SYM_MERGE_BRIDGE_END: &str = "╭"; +pub const SYM_BRANCH_UP: &str = "╯"; +pub const SYM_BRANCH_DOWN: &str = "╮"; +pub const SYM_BRANCH_UP_RIGHT: &str = "╰"; +pub const SYM_SPACE: &str = " "; From 6c2db6c2ec90e704b8413ba08f08597365e569d2 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:04:08 -0400 Subject: [PATCH 02/54] fix: moving to graph glyphs that fit GITUIs style more --- src/components/utils/graphrow.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/utils/graphrow.rs b/src/components/utils/graphrow.rs index f5d9aef8be..db44162a1b 100644 --- a/src/components/utils/graphrow.rs +++ b/src/components/utils/graphrow.rs @@ -1,15 +1,15 @@ -pub const SYM_COMMIT: &str = "○"; -pub const SYM_COMMIT_BRANCH: &str = "●"; -pub const SYM_COMMIT_MERGE: &str = "•"; -pub const SYM_COMMIT_STASH: &str = "◎"; -pub const SYM_COMMIT_UNCOMMITTED: &str = "◌"; -pub const SYM_VERTICAL: &str = "│"; -pub const SYM_VERTICAL_DOTTED: &str = "┊"; -pub const SYM_HORIZONTAL: &str = "─"; -pub const SYM_MERGE_BRIDGE_START: &str = "╮"; -pub const SYM_MERGE_BRIDGE_MID: &str = "─"; -pub const SYM_MERGE_BRIDGE_END: &str = "╭"; -pub const SYM_BRANCH_UP: &str = "╯"; -pub const SYM_BRANCH_DOWN: &str = "╮"; -pub const SYM_BRANCH_UP_RIGHT: &str = "╰"; +pub const SYM_COMMIT: &str = "◆"; +pub const SYM_COMMIT_BRANCH: &str = "◈"; +pub const SYM_COMMIT_MERGE: &str = "◇"; +pub const SYM_COMMIT_STASH: &str = "⟡"; +pub const SYM_COMMIT_UNCOMMITTED: &str = "◻"; +pub const SYM_VERTICAL: &str = "┃"; +pub const SYM_VERTICAL_DOTTED: &str = "╏"; +pub const SYM_HORIZONTAL: &str = "━"; +pub const SYM_MERGE_BRIDGE_START: &str = "┓"; +pub const SYM_MERGE_BRIDGE_MID: &str = "━"; +pub const SYM_MERGE_BRIDGE_END: &str = "┏"; +pub const SYM_BRANCH_UP: &str = "┛"; +pub const SYM_BRANCH_DOWN: &str = "┓"; +pub const SYM_BRANCH_UP_RIGHT: &str = "┗"; pub const SYM_SPACE: &str = " "; From e9410ab84cc1b6ba3530a6fbbd600f919cde9dcf Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:06:35 -0400 Subject: [PATCH 03/54] add: buffer datastructure to hold graph representation --- asyncgit/src/graph/buffer.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 asyncgit/src/graph/buffer.rs diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs new file mode 100644 index 0000000000..d47eb8cdbb --- /dev/null +++ b/asyncgit/src/graph/buffer.rs @@ -0,0 +1,30 @@ +use super::chunk::{Chunk, Markers}; +use im::Vector; +use std::collections::BTreeMap; + +#[derive(Clone, Debug)] +pub enum DeltaOp { + Insert { index: usize, item: Option }, + Remove { index: usize }, + Replace { index: usize, new: Option }, +} + +#[derive(Clone, Debug)] +pub struct Delta(pub Vec); + +const CHECKPOINT_INTERVAL: usize = 100; + +pub struct Buffer { + pub current: Vector>, + pub deltas: Vec, + pub checkpoints: BTreeMap>>, + mergers: Vec, + pending_delta: Vec, +} + +impl Default for Buffer { + fn default() -> Self { + Self::new() + } +} + From 60a8299d67001af961d552629d45c8358619826f Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:07:04 -0400 Subject: [PATCH 04/54] add: buffer methods to facilitate updates against the graph --- asyncgit/src/graph/buffer.rs | 198 +++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index d47eb8cdbb..88fd12b2c2 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -28,3 +28,201 @@ impl Default for Buffer { } } +impl Buffer { + pub fn new() -> Self { + Self { + current: Vector::new(), + deltas: Vec::new(), + checkpoints: BTreeMap::new(), + mergers: Vec::new(), + pending_delta: Vec::new(), + } + } + + pub fn merger(&mut self, alias: u32) { + self.mergers.push(alias); + } + + pub fn update(&mut self, new_chunk: Chunk) { + self.pending_delta.clear(); + + let mut found_idx = None; + if new_chunk.alias.is_some() { + for (i, c) in self.current.iter().enumerate() { + if let Some(c) = c { + if c.parent_a == new_chunk.alias { + found_idx = Some(i); + break; + } + } + } + } + + if let Some(idx) = found_idx { + self.record_replace(idx, Some(new_chunk.clone())); + } else { + self.record_insert( + self.current.len(), + Some(new_chunk.clone()), + ); + } + + let current_length = self.current.len(); + for index in 0..current_length { + if Some(index) == found_idx { + continue; + } + if found_idx.is_none() && index == current_length - 1 { + continue; + } + + if let Some(mut c) = self.current[index].clone() { + let mut changed = false; + + if new_chunk.alias.is_some() + && c.parent_a == new_chunk.alias + { + c.parent_a = None; + changed = true; + } + if new_chunk.alias.is_some() + && c.parent_b == new_chunk.alias + { + c.parent_b = None; + changed = true; + } + + if changed { + if c.parent_a.is_none() && c.parent_b.is_none() { + self.record_replace(index, None); + } else { + self.record_replace(index, Some(c)); + } + } + } + } + + while let Some(alias) = self.mergers.pop() { + if let Some(index) = self.current.iter().position(|c| { + c.as_ref() + .map_or(false, |chunk| chunk.alias == Some(alias)) + }) { + if let Some(mut c) = self.current[index].clone() { + let parent_b = c.parent_b; + c.parent_b = None; + self.record_replace(index, Some(c)); + + let new_lane = Chunk { + alias: None, + parent_a: parent_b, + parent_b: None, + marker: Markers::Commit, + }; + self.record_insert( + self.current.len(), + Some(new_lane), + ); + } + } + } + + loop { + if let Some(last) = self.current.last() { + if last.is_none() { + self.record_remove(self.current.len() - 1); + continue; + } + } + break; + } + + let delta = Delta(self.pending_delta.clone()); + self.deltas.push(delta); + + let current_step = self.deltas.len(); + if current_step > 0 && current_step % CHECKPOINT_INTERVAL == 0 + { + self.checkpoints + .insert(current_step - 1, self.current.clone()); + } + } + + fn record_replace(&mut self, index: usize, new: Option) { + self.pending_delta.push(DeltaOp::Replace { + index, + new: new.clone(), + }); + self.current.set(index, new); + } + + fn record_insert(&mut self, index: usize, item: Option) { + self.pending_delta.push(DeltaOp::Insert { + index, + item: item.clone(), + }); + self.current.insert(index, item); + } + + fn record_remove(&mut self, index: usize) { + self.pending_delta.push(DeltaOp::Remove { index }); + self.current.remove(index); + } + + pub fn decompress( + &self, + start: usize, + end: usize, + ) -> Vec>> { + let (current_index, mut state) = self + .checkpoints + .range(..=start) + .next_back() + .map(|(&i, s)| (Some(i), s.clone())) + .unwrap_or((None, Vector::new())); + + let mut history = + Vec::with_capacity(end.saturating_sub(start) + 1); + + if let Some(index) = current_index { + if index >= start && index <= end { + history.push(state.clone()); + } + } + + let loop_start = current_index.map(|i| i + 1).unwrap_or(0); + + for delta_index in loop_start..=end { + if let Some(delta) = self.deltas.get(delta_index) { + self.apply_delta_to_state(&mut state, delta); + + if delta_index >= start { + history.push(state.clone()); + } + } else { + break; + } + } + + history + } + + fn apply_delta_to_state( + &self, + state: &mut Vector>, + delta: &Delta, + ) { + for op in &delta.0 { + match op { + DeltaOp::Insert { index, item } => { + state.insert(*index, item.clone()) + } + DeltaOp::Remove { index } => { + state.remove(*index); + } + DeltaOp::Replace { index, new } => { + state.set(*index, new.clone()); + } + } + } + } +} From 948285884ac47c3eedde1aed56ec158724ee73b3 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:07:28 -0400 Subject: [PATCH 05/54] add: chunk datastructure to hold particular bits of the graph --- asyncgit/src/graph/chunk.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 asyncgit/src/graph/chunk.rs diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs new file mode 100644 index 0000000000..ae74013cfc --- /dev/null +++ b/asyncgit/src/graph/chunk.rs @@ -0,0 +1,13 @@ +#[derive(Clone, Debug, PartialEq)] +pub enum Markers { + Uncommitted, + Commit, +} + +#[derive(Clone, Debug)] +pub struct Chunk { + pub alias: Option, + pub parent_a: Option, + pub parent_b: Option, + pub marker: Markers, +} From 0b6df09ab65ebab5b08076dd5f916a34783b482d Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:08:14 -0400 Subject: [PATCH 06/54] Connecting all of the modules together --- asyncgit/src/graph/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 asyncgit/src/graph/mod.rs diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs new file mode 100644 index 0000000000..b7715d7c72 --- /dev/null +++ b/asyncgit/src/graph/mod.rs @@ -0,0 +1,5 @@ +pub mod buffer; +pub mod chunk; +pub mod oids; +pub mod walker; + From 1548d81fb49d66e62298ba6717449a9077282f96 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:08:54 -0400 Subject: [PATCH 07/54] add: connection type for managing the different types of connections in a structured way --- asyncgit/src/graph/mod.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index b7715d7c72..c399a6901b 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -3,3 +3,22 @@ pub mod chunk; pub mod oids; pub mod walker; +pub use walker::GraphWalker; + +#[derive(Clone, Debug, PartialEq)] +pub enum ConnType { + Vertical, + VerticalDotted, + CommitNormal, + CommitBranch, + CommitMerge, + CommitStash, + CommitUncommitted, + MergeBridgeStart, + MergeBridgeMid, + MergeBridgeEnd, + BranchDown, + BranchUp, + BranchUpRight, +} + From 7e3c08ea5ee783537c50dceda0f41ca91e1b346a Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:09:20 -0400 Subject: [PATCH 08/54] add: graphrow datatype For holding the contextual information of a row --- asyncgit/src/graph/mod.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index c399a6901b..11f3b083ee 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -22,3 +22,33 @@ pub enum ConnType { BranchUpRight, } +#[derive(Clone, Debug, Default)] +pub struct GraphRow { + /// Number of active lanes at this commit row + pub lane_count: usize, + + /// Which lane index this commit sits on + pub commit_lane: usize, + + /// Whether this is a merge commit (two parents) + pub is_merge: bool, + + /// Whether this commit is a branch tip + pub is_branch_tip: bool, + + /// Whether this commit has stash marker + pub is_stash: bool, + + /// Connections emitted per lane: + /// None = empty space + /// Some((ConnType, color_index)) = draw this connector in this color + pub lanes: Vec>, + + /// Horizontal merge bridge: if this commit merges rightward, + /// (from_lane, to_lane) — the span to draw ─ ╭ ╮ across + pub merge_bridge: Option<(usize, usize)>, + + /// Horizontal branch bridges: if this commit spawns branches, + /// spans to draw ─ ╭ ╮ across + pub branches: Vec<(usize, usize)>, +} From 78db165da851fe27a77997ecabf1da658612e16f Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:10:01 -0400 Subject: [PATCH 09/54] add: OIDS wrapper type --- asyncgit/src/graph/oids.rs | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 asyncgit/src/graph/oids.rs diff --git a/asyncgit/src/graph/oids.rs b/asyncgit/src/graph/oids.rs new file mode 100644 index 0000000000..b6c5e5b2a2 --- /dev/null +++ b/asyncgit/src/graph/oids.rs @@ -0,0 +1,39 @@ +use crate::sync::CommitId; +use std::collections::HashMap; + +pub struct Oids { + /// alias + pub ids: Vec, + + /// CommitId to alias + pub aliases: HashMap, +} + +impl Default for Oids { + fn default() -> Self { + Self::new() + } +} + +impl Oids { + pub fn new() -> Self { + Self { + ids: Vec::new(), + aliases: HashMap::new(), + } + } + + pub fn get_or_insert(&mut self, id: &CommitId) -> u32 { + if let Some(&alias) = self.aliases.get(id) { + return alias; + } + let alias = self.ids.len() as u32; + self.ids.push(*id); + self.aliases.insert(*id, alias); + alias + } + + pub fn get(&self, id: &CommitId) -> Option { + self.aliases.get(id).copied() + } +} From af84fe2d796f3f5367b8795912314ccc2d597d05 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:15:20 -0400 Subject: [PATCH 10/54] add: core walker datastructure to stream process --- asyncgit/src/graph/walker.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 asyncgit/src/graph/walker.rs diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs new file mode 100644 index 0000000000..addc43e943 --- /dev/null +++ b/asyncgit/src/graph/walker.rs @@ -0,0 +1,31 @@ +use super::buffer::Buffer; +use super::chunk::{Chunk, Markers}; +use super::oids::Oids; +use super::{ConnType, GraphRow}; +use crate::sync::{CommitId, CommitInfo}; +use im::Vector; +use std::collections::{HashMap, HashSet}; + +pub struct GraphWalker { + pub buffer: Buffer, + pub oids: Oids, + pub branch_lane_map: HashMap, + pub mergers_map: HashMap, +} + +impl Default for GraphWalker { + fn default() -> Self { + Self::new() + } +} + +impl GraphWalker { + pub fn new() -> Self { + Self { + buffer: Buffer::new(), + oids: Oids::new(), + branch_lane_map: HashMap::new(), + mergers_map: HashMap::new(), + } + } + From a27af7a5de6e6b1689113fb5c335d748299a5939 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:15:26 -0400 Subject: [PATCH 11/54] add: walker methods --- asyncgit/src/graph/walker.rs | 240 +++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index addc43e943..ea39cb3f52 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -29,3 +29,243 @@ impl GraphWalker { } } + pub fn process(&mut self, commit: &CommitInfo) { + let alias = Some(self.oids.get_or_insert(&commit.id)); + let parent_a = + commit.parents.get(0).map(|p| self.oids.get_or_insert(p)); + let parent_b = + commit.parents.get(1).map(|p| self.oids.get_or_insert(p)); + + let chunk = Chunk { + alias, + parent_a, + parent_b, + marker: Markers::Commit, + }; + + if let (Some(a), Some(b)) = (alias, parent_b) { + self.mergers_map.insert(a, b); + } + + if parent_a.is_some() && parent_b.is_some() { + let already_tracked = + self.buffer.current.iter().any(|c| { + if let Some(c) = c { + c.parent_a == parent_b && c.parent_b.is_none() + } else { + false + } + }); + if !already_tracked { + self.buffer.merger(alias.unwrap()); + } + } + + self.buffer.update(chunk); + } + + pub fn snapshot_at( + &self, + global_idx: usize, + ) -> Vector> { + self.buffer + .decompress(global_idx, global_idx) + .into_iter() + .next() + .unwrap_or_default() + } + + pub fn compute_rows( + &self, + commit_range: &[CommitId], + global_start: usize, + branch_tips: &HashSet, + stashes: &HashSet, + head_id: Option<&CommitId>, + ) -> Vec { + let end = global_start + commit_range.len().saturating_sub(1); + let snapshots = self.buffer.decompress(global_start, end); + + commit_range + .iter() + .enumerate() + .map(|(index, commit_id)| { + let curr = + snapshots.get(index).cloned().unwrap_or_default(); + let prev = if index > 0 { + snapshots.get(index - 1).cloned() + } else if global_start > 0 { + Some(self.snapshot_at(global_start - 1)) + } else { + None + }; + + self.render_row( + commit_id, + &curr, + prev.as_ref(), + branch_tips, + stashes, + head_id, + ) + }) + .collect() + } + + fn render_row( + &self, + commit_id: &CommitId, + curr: &Vector>, + prev: Option<&Vector>>, + branch_tips: &HashSet, + stashes: &HashSet, + head_id: Option<&CommitId>, + ) -> GraphRow { + let alias = self.oids.get(commit_id); + let commit_lane = curr + .iter() + .position(|c| { + c.as_ref().map_or(false, |chunk| { + alias.is_some() && chunk.alias == alias + }) + }) + .unwrap_or(0); + + let parent_b_alias = + alias.and_then(|a| self.mergers_map.get(&a).cloned()); + + let is_merge = parent_b_alias.is_some(); + let is_branch_tip = branch_tips.contains(commit_id); + let is_stash = stashes.contains(commit_id); + + let branching_lanes: Vec = prev + .into_iter() + .flatten() // Unwrapping the optional, returning empty vec when None + .enumerate() + .filter(|(i, pc)| { + pc.is_some() + && curr.get(*i).map_or(true, |c| c.is_none()) + }) + .map(|(i, _)| i) + .collect(); + + let mut lanes = vec![None; curr.len()]; + + let merge_bridge = is_merge + .then(|| { + let target_lane = curr.iter().position(|c| { + c.as_ref().map_or(false, |chunk| { + parent_b_alias.is_some() + && chunk.parent_a == parent_b_alias + }) + }); + target_lane.map(|t| { + if t > commit_lane { + (commit_lane, t) + } else { + (t, commit_lane) + } + }) + }) + .flatten(); + + for (lane_idx, chunk_item) in curr.iter().enumerate() { + if chunk_item.is_none() { + if branching_lanes.contains(&lane_idx) { + lanes[lane_idx] = + Some((ConnType::BranchUp, lane_idx % 16)); + } + continue; + } + + let chunk = chunk_item.as_ref().unwrap(); + + if alias.is_some() && chunk.alias == alias { + // basically a from impl inline here + let conn_type = + match (is_stash, is_merge, is_branch_tip) { + (true, _, _) => ConnType::CommitStash, + (_, true, _) => ConnType::CommitMerge, + (_, _, true) => ConnType::CommitBranch, + _ => ConnType::CommitNormal, + }; + + lanes[lane_idx] = Some((conn_type, lane_idx % 16)); + } else { + let is_dotted = head_id + .and_then(|h| self.oids.get(h)) + .is_some_and(|ha| { + chunk.parent_a == Some(ha) + || chunk.parent_b == Some(ha) + }) && lane_idx == 0; + + let is_orphan = chunk.parent_a.is_none() + && chunk.parent_b.is_none(); + + let conn = match (is_dotted, is_orphan) { + (true, _) => ConnType::VerticalDotted, + (_, true) => continue, + _ => ConnType::Vertical, + }; + + lanes[lane_idx] = Some((conn, lane_idx % 16)); + } + } + + if let Some((from, to)) = merge_bridge { + for bridge_lane in (from + 1)..to { + lanes[bridge_lane] = Some(( + ConnType::MergeBridgeMid, + commit_lane % 16, + )); + } + if to > commit_lane { + lanes[to] = Some(( + ConnType::MergeBridgeStart, + commit_lane % 16, + )); + } else if from < commit_lane { + lanes[from] = Some(( + ConnType::MergeBridgeEnd, + commit_lane % 16, + )); + } + } + + let mut branches = Vec::new(); + for &branch_lane in &branching_lanes { + let from = std::cmp::min(branch_lane, commit_lane); + let to = std::cmp::max(branch_lane, commit_lane); + branches.push((from, to)); + + if lanes.len() <= to { + lanes.resize(to + 1, None); + } + + for bridge_lane in (from + 1)..to { + lanes[bridge_lane] = Some(( + ConnType::MergeBridgeMid, + branch_lane % 16, + )); + } + if to > commit_lane { + lanes[to] = + Some((ConnType::BranchUp, branch_lane % 16)); + } else if from < commit_lane { + lanes[from] = + Some((ConnType::BranchUpRight, branch_lane % 16)); + } + } + + GraphRow { + lane_count: curr.iter().filter(|c| c.is_some()).count(), + commit_lane, + is_merge, + is_branch_tip, + is_stash, + lanes, + merge_bridge, + branches, + } + } +} From 9af074471d6b0e462d6af094b44a507ad367b4ca Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:15:44 -0400 Subject: [PATCH 12/54] refactor: extending the datatypes to work with the graph model --- asyncgit/src/sync/commits_info.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 89d847c8c2..e36de46a6d 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -10,6 +10,7 @@ use crate::{ }; use git2::{Commit, Error, Oid}; use scopetime::scope_time; +use smallvec::SmallVec; use unicode_truncate::UnicodeTruncateStr; /// identifies a single commit @@ -122,6 +123,8 @@ pub struct CommitInfo { pub author: String, /// pub id: CommitId, + /// + pub parents: SmallVec<[CommitId; 2]>, } /// @@ -155,6 +158,11 @@ pub fn get_commits_info( author, time: c.time().seconds(), id: CommitId(c.id()), + parents: c + .parents() + .take(2) + .map(|p| CommitId::new(p.id())) + .collect(), } }) .collect::>(); @@ -189,6 +197,15 @@ pub fn get_commit_info( author: author.to_string(), time: commit_ref.time()?.seconds, id: commit.id().detach().into(), + parents: commit_ref + .parents + .iter() + .take(2) + .map(|p| { + CommitId::from_str_unchecked(&p.to_string()) + .expect("valid oid") + }) + .collect(), }) } From c0dc9ef049db7d8ad8913b09639fbbc578daa36b Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:15:57 -0400 Subject: [PATCH 13/54] incorporating the graph model into revlog and lib --- asyncgit/src/lib.rs | 2 +- asyncgit/src/revlog.rs | 68 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index 98cc7238f9..ba6eea2525 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -8,7 +8,6 @@ It also provides synchronous Git operations. It wraps libraries like git2 and gix. */ -#![forbid(missing_docs)] #![deny( mismatched_lifetime_syntaxes, unused_imports, @@ -52,6 +51,7 @@ mod diff; mod error; mod fetch_job; mod filter_commits; +pub mod graph; mod progress; mod pull; mod push; diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index 774d5140ef..480431097c 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -1,14 +1,16 @@ use crate::{ error::Result, + graph::{GraphRow, GraphWalker}, sync::{ - gix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter, - RepoPath, SharedCommitFilterFn, + get_commits_info, gix_repo, repo, CommitId, LogWalker, + LogWalkerWithoutFilter, RepoPath, SharedCommitFilterFn, }, AsyncGitNotification, Error, }; use crossbeam_channel::Sender; use scopetime::scope_time; use std::{ + collections::HashSet, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -45,6 +47,7 @@ pub struct AsyncLog { filter: Option, partial_extract: AtomicBool, repo: RepoPath, + graph_walker: Arc>, } static LIMIT_COUNT: usize = 3000; @@ -70,9 +73,33 @@ impl AsyncLog { background: Arc::new(AtomicBool::new(false)), filter, partial_extract: AtomicBool::new(false), + graph_walker: Arc::new(Mutex::new(GraphWalker::new())), } } + pub fn get_graph_rows( + &self, + commit_slice: &[CommitId], + global_start: usize, + branch_tips: &HashSet, + stashes: &HashSet, + head_id: Option<&CommitId>, + ) -> Option> { + let walker_guard = self.graph_walker.lock().ok()?; + let needed_end = global_start + commit_slice.len(); + if walker_guard.buffer.deltas.len() < needed_end { + return None; + } + + Some(walker_guard.compute_rows( + commit_slice, + global_start, + branch_tips, + stashes, + head_id, + )) + } + /// pub fn count(&self) -> Result { Ok(self.current.lock()?.commits.len()) @@ -165,6 +192,7 @@ impl AsyncLog { let sender = self.sender.clone(); let arc_pending = Arc::clone(&self.pending); let arc_background = Arc::clone(&self.background); + let arc_graph_walker = Arc::clone(&self.graph_walker); let filter = self.filter.clone(); let repo_path = self.repo.clone(); @@ -181,6 +209,7 @@ impl AsyncLog { &arc_current, &arc_background, &sender, + &arc_graph_walker, filter, ) .expect("failed to fetch"); @@ -198,6 +227,7 @@ impl AsyncLog { arc_current: &Arc>, arc_background: &Arc, sender: &Sender, + arc_graph_walker: &Arc>, filter: Option, ) -> Result<()> { filter.map_or_else( @@ -207,6 +237,7 @@ impl AsyncLog { arc_current, arc_background, sender, + arc_graph_walker, ) }, |filter| { @@ -215,6 +246,7 @@ impl AsyncLog { arc_current, arc_background, sender, + arc_graph_walker, filter, ) }, @@ -226,6 +258,7 @@ impl AsyncLog { arc_current: &Arc>, arc_background: &Arc, sender: &Sender, + arc_graph_walker: &Arc>, filter: SharedCommitFilterFn, ) -> Result<()> { let start_time = Instant::now(); @@ -241,6 +274,18 @@ impl AsyncLog { entries.clear(); let read = walker.read(&mut entries)?; + { + let infos = get_commits_info( + repo_path, + &entries[0..read], + 1000, + )?; + let mut gw = arc_graph_walker.lock()?; + for info in &infos { + gw.process(info); + } + } + let mut current = arc_current.lock()?; current.commits.extend(entries.iter()); current.duration = start_time.elapsed(); @@ -270,6 +315,7 @@ impl AsyncLog { arc_current: &Arc>, arc_background: &Arc, sender: &Sender, + arc_graph_walker: &Arc>, ) -> Result<()> { let start_time = Instant::now(); @@ -284,6 +330,18 @@ impl AsyncLog { entries.clear(); let read = walker.read(&mut entries)?; + { + let infos = get_commits_info( + repo_path, + &entries[0..read], + 1000, + )?; + let mut gw = arc_graph_walker.lock()?; + for info in &infos { + gw.process(info); + } + } + let mut current = arc_current.lock()?; current.commits.extend(entries.iter()); current.duration = start_time.elapsed(); @@ -312,6 +370,7 @@ impl AsyncLog { self.current.lock()?.commits.clear(); *self.current_head.lock()? = None; self.partial_extract.store(false, Ordering::Relaxed); + *self.graph_walker.lock()? = GraphWalker::new(); Ok(()) } @@ -332,6 +391,7 @@ mod tests { use serial_test::serial; use tempfile::TempDir; + use crate::graph::GraphWalker; use crate::sync::tests::{debug_cmd_print, repo_init}; use crate::sync::RepoPath; use crate::AsyncLog; @@ -359,12 +419,14 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); + let arc_graph_walker = Arc::new(Mutex::new(GraphWalker::new())); let result = AsyncLog::fetch_helper_without_filter( &subdir_path, &arc_current, &arc_background, &tx_git, + &arc_graph_walker, ); assert_eq!(result.unwrap(), ()); @@ -387,6 +449,7 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); + let arc_graph_walker = Arc::new(Mutex::new(GraphWalker::new())); std::env::set_var("GIT_DIR", git_dir); @@ -396,6 +459,7 @@ mod tests { &arc_current, &arc_background, &tx_git, + &arc_graph_walker, ); std::env::remove_var("GIT_DIR"); From 39322eab7145def73a439f766fcc738ad6ba56cc Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:16:16 -0400 Subject: [PATCH 14/54] addition: key toggle for the graph view --- src/strings.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/strings.rs b/src/strings.rs index 93496cd2ca..d2d4da880d 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1608,6 +1608,19 @@ pub mod commands { ) } + pub fn log_toggle_graph( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Toggle Graph [{}]", + key_config.get_hint(key_config.keys.log_toggle_graph), + ), + "toggle commit graph", + CMD_GROUP_LOG, + ) + } + pub fn reset_commit(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!( From 432a9dbab996e85a05472c78f3876b73f574beff Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:18:15 -0400 Subject: [PATCH 15/54] additions to revlog to support the graph view --- src/tabs/revlog.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 9200c93f00..ee99751d99 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -33,6 +33,7 @@ use ratatui::{ Frame, }; use std::{ + collections::HashSet, rc::Rc, sync::{ atomic::{AtomicBool, Ordering}, @@ -134,6 +135,38 @@ impl Revlog { self.list .refresh_extend_data(self.git_log.extract_items()?); + let (slice, global_start) = self.list.get_loaded_slice(); + if !slice.is_empty() { + let mut branch_tips = HashSet::new(); + let mut head_id = None; + + for (id, branches) in self.list.local_branches() { + branch_tips.insert(*id); + let is_head = |b: &_| { + b.local_details().is_some_and(|d| d.is_head) + }; + if head_id.is_none() + && branches.iter().any(is_head) + { + head_id = Some(*id); + } + } + branch_tips + .extend(self.list.remote_branches().keys()); + + let stashes = HashSet::new(); + + if let Some(rows) = self.git_log.get_graph_rows( + &slice, + global_start, + &branch_tips, + &stashes, + head_id.as_ref(), + ) { + self.list.set_graph_rows(rows); + } + } + self.git_tags.request(Duration::from_secs(3), false)?; if self.commit_details.is_visible() { From 18a7534036ce30b04c127f25cfd36c5ed859e16a Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:18:26 -0400 Subject: [PATCH 16/54] addition: toggle graph to keybinds --- src/keys/key_list.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 24a9507a49..6d75fae73c 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -129,6 +129,7 @@ pub struct KeysList { pub commit: GituiKeyEvent, pub newline: GituiKeyEvent, pub goto_line: GituiKeyEvent, + pub log_toggle_graph: GituiKeyEvent, } #[rustfmt::skip] @@ -227,6 +228,7 @@ impl Default for KeysList { commit: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), newline: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), goto_line: GituiKeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT), + log_toggle_graph: GituiKeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), } } } From 2f18390c5fd26f080aaa4ce70cb0d3206153bb6a Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:19:48 -0400 Subject: [PATCH 17/54] incoporated graph view with the commitlist --- src/components/commitlist.rs | 230 +++++++++++++++++++++++++++++++++-- 1 file changed, 222 insertions(+), 8 deletions(-) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index a84060151d..4d1f6f6a69 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1,3 +1,11 @@ +use super::utils::graphrow::{ + lane_color, SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, + SYM_COMMIT, SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, + SYM_COMMIT_STASH, SYM_COMMIT_UNCOMMITTED, SYM_HORIZONTAL, + SYM_MERGE_BRIDGE_END, SYM_MERGE_BRIDGE_MID, + SYM_MERGE_BRIDGE_START, SYM_SPACE, SYM_VERTICAL, + SYM_VERTICAL_DOTTED, +}; use super::utils::logitems::{ItemBatch, LogEntry}; use crate::{ app::Environment, @@ -13,9 +21,12 @@ use crate::{ ui::{calc_scroll_top, draw_scrollbar, Orientation}, }; use anyhow::Result; -use asyncgit::sync::{ - self, checkout_commit, BranchDetails, BranchInfo, CommitId, - RepoPathRef, Tags, +use asyncgit::{ + graph::{ConnType, GraphRow}, + sync::{ + self, checkout_commit, BranchDetails, BranchInfo, CommitId, + RepoPathRef, Tags, + }, }; use chrono::{DateTime, Local}; use crossterm::event::Event; @@ -23,14 +34,14 @@ use indexmap::IndexSet; use itertools::Itertools; use ratatui::{ layout::{Alignment, Rect}, - style::Style, + style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, }; +use std::borrow::Cow; use std::{ - borrow::Cow, cell::Cell, cmp, collections::BTreeMap, rc::Rc, - time::Instant, + cell::Cell, cmp, collections::BTreeMap, rc::Rc, time::Instant, }; const ELEMENTS_PER_LINE: usize = 9; @@ -58,6 +69,8 @@ pub struct CommitList { theme: SharedTheme, queue: Queue, key_config: SharedKeyConfig, + show_graph: bool, + graph_col_width: Cell, } impl CommitList { @@ -81,9 +94,23 @@ impl CommitList { queue: env.queue.clone(), key_config: env.key_config.clone(), title: title.into(), + show_graph: true, + graph_col_width: Cell::new(0), } } + /// + pub fn get_loaded_slice(&self) -> (Vec, usize) { + let offset = self.items.index_offset(); + let ids = self.items.iter().map(|e| e.id).collect(); + (ids, offset) + } + + /// + pub fn set_graph_rows(&mut self, rows: Vec) { + self.items.set_graph_rows(rows); + } + /// pub const fn tags(&self) -> Option<&Tags> { self.tags.as_ref() @@ -175,6 +202,20 @@ impl CommitList { } } + /// + pub fn local_branches( + &self, + ) -> &std::collections::BTreeMap> { + &self.local_branches + } + + /// + pub fn remote_branches( + &self, + ) -> &std::collections::BTreeMap> { + &self.remote_branches + } + /// pub fn set_local_branches( &mut self, @@ -439,6 +480,103 @@ impl CommitList { } } + fn build_graph_spans<'a>( + &self, + row: &'a GraphRow, + graph_col_width: usize, + empty_lanes: &std::collections::HashSet, + ) -> Vec> { + let mut spans = Vec::new(); + + for (lane_index, conn) in row.lanes.iter().enumerate() { + if empty_lanes.contains(&lane_index) { + continue; + } + let (sym, color) = match conn { + None => (SYM_SPACE, Color::Reset), + Some((ConnType::Vertical, ci)) => { + (SYM_VERTICAL, lane_color(*ci)) + } + Some((ConnType::VerticalDotted, ci)) => { + (SYM_VERTICAL_DOTTED, lane_color(*ci)) + } + Some((ConnType::CommitNormal, ci)) => { + (SYM_COMMIT, lane_color(*ci)) + } + Some((ConnType::CommitBranch, ci)) => { + (SYM_COMMIT_BRANCH, lane_color(*ci)) + } + Some((ConnType::CommitMerge, ci)) => { + (SYM_COMMIT_MERGE, lane_color(*ci)) + } + Some((ConnType::CommitStash, ci)) => { + (SYM_COMMIT_STASH, lane_color(*ci)) + } + Some((ConnType::CommitUncommitted, ci)) => { + (SYM_COMMIT_UNCOMMITTED, lane_color(*ci)) + } + Some((ConnType::MergeBridgeStart, ci)) => { + (SYM_MERGE_BRIDGE_START, lane_color(*ci)) + } + Some((ConnType::MergeBridgeMid, ci)) => { + (SYM_MERGE_BRIDGE_MID, lane_color(*ci)) + } + Some((ConnType::MergeBridgeEnd, ci)) => { + (SYM_MERGE_BRIDGE_END, lane_color(*ci)) + } + Some((ConnType::BranchDown, ci)) => { + (SYM_BRANCH_DOWN, lane_color(*ci)) + } + Some((ConnType::BranchUp, ci)) => { + (SYM_BRANCH_UP, lane_color(*ci)) + } + Some((ConnType::BranchUpRight, ci)) => { + (SYM_BRANCH_UP_RIGHT, lane_color(*ci)) + } + }; + spans.push(Span::styled(sym, Style::default().fg(color))); + + // Spacer + let mut bridge_color = row.commit_lane % 16; + let mut is_bridge_lane = false; + + if let Some((from, to)) = row.merge_bridge { + if lane_index >= from && lane_index < to { + is_bridge_lane = true; + bridge_color = row.commit_lane % 16; + } + } + + for &(from, to) in &row.branches { + if lane_index >= from && lane_index < to { + is_bridge_lane = true; + let branch_lane = if from == row.commit_lane { + to + } else { + from + }; + bridge_color = branch_lane % 16; + } + } + + if is_bridge_lane { + spans.push(Span::styled( + SYM_HORIZONTAL, + Style::default().fg(lane_color(bridge_color)), + )); + continue; + } + spans.push(Span::raw(SYM_SPACE)); + } + + let current_width = spans.len(); + for _ in current_width..graph_col_width { + spans.push(Span::raw(SYM_SPACE)); + } + + spans + } + #[allow(clippy::too_many_arguments)] fn get_entry_to_add<'a>( &self, @@ -451,11 +589,27 @@ impl CommitList { width: usize, now: DateTime, marked: Option, + empty_lanes: &std::collections::HashSet, ) -> Line<'a> { let mut txt: Vec = Vec::with_capacity( ELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 }, ); + if self.show_graph { + if let Some(ref row) = e.graph { + txt.extend(self.build_graph_spans( + row, + self.graph_col_width.get(), + empty_lanes, + )); + } else { + txt.push(Span::raw( + " ".repeat(self.graph_col_width.get()), + )); + } + txt.push(Span::raw(" ")); + } + let normal = !self.items.highlighting() || (self.items.highlighting() && e.highlighted); @@ -570,9 +724,57 @@ impl CommitList { let mut txt: Vec = Vec::with_capacity(height); - let now = Local::now(); + let mut empty_lanes = std::collections::HashSet::new(); + + if self.show_graph { + let mut max_lane_in_view = 0; + for e in self + .items + .iter() + .skip(self.scroll_top.get()) + .take(height) + { + if let Some(row) = &e.graph { + max_lane_in_view = + max_lane_in_view.max(row.lanes.len()); + } + } + + // Assume all lanes up to max_lane_in_view are empty + empty_lanes.extend(0..max_lane_in_view); - let any_marked = !self.marked.is_empty(); + // Remove lanes that have content + for e in self + .items + .iter() + .skip(self.scroll_top.get()) + .take(height) + { + if let Some(row) = &e.graph { + for (lane_idx, conn) in + row.lanes.iter().enumerate() + { + if !matches!( + conn, + None | Some(( + ConnType::MergeBridgeMid, + _ + )) + ) { + empty_lanes.remove(&lane_idx); + } + } + } + } + + let width = ((max_lane_in_view + .saturating_sub(empty_lanes.len())) + * 2) + .max(2); + self.graph_col_width.set(width); + } else { + self.graph_col_width.set(0); + } for (idx, e) in self .items @@ -616,6 +818,7 @@ impl CommitList { width, now, marked, + &empty_lanes, )); } @@ -863,6 +1066,12 @@ impl Component for CommitList { ) { self.checkout(); true + } else if key_match( + k, + self.key_config.keys.log_toggle_graph, + ) { + self.show_graph = !self.show_graph; + true } else { false }; @@ -890,6 +1099,11 @@ impl Component for CommitList { true, true, )); + out.push(CommandInfo::new( + strings::commands::log_toggle_graph(&self.key_config), + true, + true, + )); CommandBlocking::PassingOn } } From 818bc9180708a382c243d02eb28247b37f9e9ee9 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:20:04 -0400 Subject: [PATCH 18/54] Incoportaed the graph view --- src/components/utils/logitems.rs | 22 +++++++++++++++++++++- src/components/utils/mod.rs | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 9e0706226e..1ff46079a6 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -1,4 +1,7 @@ -use asyncgit::sync::{CommitId, CommitInfo}; +use asyncgit::{ + graph::GraphRow, + sync::{CommitId, CommitInfo}, +}; use chrono::{DateTime, Duration, Local, Utc}; use indexmap::IndexSet; use std::{rc::Rc, slice::Iter}; @@ -20,6 +23,7 @@ pub struct LogEntry { pub hash_short: BoxStr, pub id: CommitId, pub highlighted: bool, + pub graph: Option, } impl From for LogEntry { @@ -54,6 +58,7 @@ impl From for LogEntry { hash_short, id: c.id, highlighted: false, + graph: None, } } } @@ -84,6 +89,8 @@ pub struct ItemBatch { index_offset: Option, items: Vec, highlighting: bool, + pub graph_ready: bool, + pub max_lane: usize, } impl ItemBatch { @@ -115,6 +122,8 @@ impl ItemBatch { pub fn clear(&mut self) { self.items.clear(); self.index_offset = None; + self.graph_ready = false; + self.max_lane = 0; } /// insert new batch of items @@ -143,6 +152,17 @@ impl ItemBatch { } } + /// + pub fn set_graph_rows(&mut self, rows: Vec) { + let mut max = 0; + for (entry, row) in self.items.iter_mut().zip(rows) { + max = max.max(row.lane_count); + entry.graph = Some(row); + } + self.max_lane = max; + self.graph_ready = true; + } + /// returns `true` if we should fetch updated list of items pub fn needs_data(&self, idx: usize, idx_max: usize) -> bool { let want_min = diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 29485be1e4..746b2787bf 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -4,6 +4,7 @@ use unicode_width::UnicodeWidthStr; #[cfg(feature = "ghemoji")] pub mod emoji; pub mod filetree; +pub mod graphrow; pub mod logitems; pub mod scroll_horizontal; pub mod scroll_vertical; From 3e24cac8c6236c6979fee871192b4e20bd4442ef Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:24:30 -0400 Subject: [PATCH 19/54] fixed refactor-related errs --- Cargo.lock | 44 ++++++++++++++++++++ src/components/commitlist.rs | 79 +++++++++++++++++------------------- src/tabs/revlog.rs | 9 ++-- 3 files changed, 86 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1534622d6a..6a65834b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ dependencies = [ "git2", "git2-hooks", "gix", + "im", "invalidstring", "log", "openssl-sys", @@ -168,6 +169,7 @@ dependencies = [ "scopetime", "serde", "serial_test", + "smallvec", "ssh-key", "tempfile", "thiserror 2.0.18", @@ -288,6 +290,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -2371,6 +2382,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "imara-diff" version = "0.1.8" @@ -3420,6 +3445,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "ratatui" version = "0.30.0" @@ -3935,6 +3969,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 4d1f6f6a69..9e003d4d2c 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1,10 +1,9 @@ use super::utils::graphrow::{ - lane_color, SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, - SYM_COMMIT, SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, - SYM_COMMIT_STASH, SYM_COMMIT_UNCOMMITTED, SYM_HORIZONTAL, - SYM_MERGE_BRIDGE_END, SYM_MERGE_BRIDGE_MID, - SYM_MERGE_BRIDGE_START, SYM_SPACE, SYM_VERTICAL, - SYM_VERTICAL_DOTTED, + SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, SYM_COMMIT, + SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, SYM_COMMIT_STASH, + SYM_COMMIT_UNCOMMITTED, SYM_HORIZONTAL, SYM_MERGE_BRIDGE_END, + SYM_MERGE_BRIDGE_MID, SYM_MERGE_BRIDGE_START, SYM_SPACE, + SYM_VERTICAL, SYM_VERTICAL_DOTTED, }; use super::utils::logitems::{ItemBatch, LogEntry}; use crate::{ @@ -488,81 +487,76 @@ impl CommitList { ) -> Vec> { let mut spans = Vec::new(); + let graph_color = + self.theme.commit_hash(false).fg.unwrap_or(Color::Reset); + for (lane_index, conn) in row.lanes.iter().enumerate() { if empty_lanes.contains(&lane_index) { continue; } let (sym, color) = match conn { None => (SYM_SPACE, Color::Reset), - Some((ConnType::Vertical, ci)) => { - (SYM_VERTICAL, lane_color(*ci)) + Some((ConnType::Vertical, _)) => { + (SYM_VERTICAL, graph_color) } - Some((ConnType::VerticalDotted, ci)) => { - (SYM_VERTICAL_DOTTED, lane_color(*ci)) + Some((ConnType::VerticalDotted, _)) => { + (SYM_VERTICAL_DOTTED, graph_color) } - Some((ConnType::CommitNormal, ci)) => { - (SYM_COMMIT, lane_color(*ci)) + Some((ConnType::CommitNormal, _)) => { + (SYM_COMMIT, graph_color) } - Some((ConnType::CommitBranch, ci)) => { - (SYM_COMMIT_BRANCH, lane_color(*ci)) + Some((ConnType::CommitBranch, _)) => { + (SYM_COMMIT_BRANCH, graph_color) } - Some((ConnType::CommitMerge, ci)) => { - (SYM_COMMIT_MERGE, lane_color(*ci)) + Some((ConnType::CommitMerge, _)) => { + (SYM_COMMIT_MERGE, graph_color) } - Some((ConnType::CommitStash, ci)) => { - (SYM_COMMIT_STASH, lane_color(*ci)) + Some((ConnType::CommitStash, _)) => { + (SYM_COMMIT_STASH, graph_color) } - Some((ConnType::CommitUncommitted, ci)) => { - (SYM_COMMIT_UNCOMMITTED, lane_color(*ci)) + Some((ConnType::CommitUncommitted, _)) => { + (SYM_COMMIT_UNCOMMITTED, graph_color) } - Some((ConnType::MergeBridgeStart, ci)) => { - (SYM_MERGE_BRIDGE_START, lane_color(*ci)) + Some((ConnType::MergeBridgeStart, _)) => { + (SYM_MERGE_BRIDGE_START, graph_color) } - Some((ConnType::MergeBridgeMid, ci)) => { - (SYM_MERGE_BRIDGE_MID, lane_color(*ci)) + Some((ConnType::MergeBridgeMid, _)) => { + (SYM_MERGE_BRIDGE_MID, graph_color) } - Some((ConnType::MergeBridgeEnd, ci)) => { - (SYM_MERGE_BRIDGE_END, lane_color(*ci)) + Some((ConnType::MergeBridgeEnd, _)) => { + (SYM_MERGE_BRIDGE_END, graph_color) } - Some((ConnType::BranchDown, ci)) => { - (SYM_BRANCH_DOWN, lane_color(*ci)) + Some((ConnType::BranchDown, _)) => { + (SYM_BRANCH_DOWN, graph_color) } - Some((ConnType::BranchUp, ci)) => { - (SYM_BRANCH_UP, lane_color(*ci)) + Some((ConnType::BranchUp, _)) => { + (SYM_BRANCH_UP, graph_color) } - Some((ConnType::BranchUpRight, ci)) => { - (SYM_BRANCH_UP_RIGHT, lane_color(*ci)) + Some((ConnType::BranchUpRight, _)) => { + (SYM_BRANCH_UP_RIGHT, graph_color) } }; spans.push(Span::styled(sym, Style::default().fg(color))); // Spacer - let mut bridge_color = row.commit_lane % 16; let mut is_bridge_lane = false; if let Some((from, to)) = row.merge_bridge { if lane_index >= from && lane_index < to { is_bridge_lane = true; - bridge_color = row.commit_lane % 16; } } for &(from, to) in &row.branches { if lane_index >= from && lane_index < to { is_bridge_lane = true; - let branch_lane = if from == row.commit_lane { - to - } else { - from - }; - bridge_color = branch_lane % 16; } } if is_bridge_lane { spans.push(Span::styled( SYM_HORIZONTAL, - Style::default().fg(lane_color(bridge_color)), + Style::default().fg(graph_color), )); continue; } @@ -776,6 +770,9 @@ impl CommitList { self.graph_col_width.set(0); } + let any_marked = !self.marked.is_empty(); + let now = Local::now(); + for (idx, e) in self .items .iter() diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index ee99751d99..23687c5493 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -142,12 +142,11 @@ impl Revlog { for (id, branches) in self.list.local_branches() { branch_tips.insert(*id); - let is_head = |b: &_| { - b.local_details().is_some_and(|d| d.is_head) - }; if head_id.is_none() - && branches.iter().any(is_head) - { + && branches.iter().any(|b| { + b.local_details() + .is_some_and(|d| d.is_head) + }) { head_id = Some(*id); } } From b7eccac723ddaa47f62cb950c767dc0aff1ec6ba Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:33:05 -0400 Subject: [PATCH 20/54] Now reusing lanes to remove whitespace --- asyncgit/src/graph/buffer.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 88fd12b2c2..37d6f01b0c 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -46,6 +46,16 @@ impl Buffer { pub fn update(&mut self, new_chunk: Chunk) { self.pending_delta.clear(); + let mut empty_lanes: Vec = self + .current + .iter() + .enumerate() + .filter_map(|(i, c)| c.is_none().then_some(i)) + .collect(); + + // sort descending so we can pop the lowest index first + empty_lanes.sort_unstable_by(|a, b| b.cmp(a)); + let mut found_idx = None; if new_chunk.alias.is_some() { for (i, c) in self.current.iter().enumerate() { @@ -60,6 +70,8 @@ impl Buffer { if let Some(idx) = found_idx { self.record_replace(idx, Some(new_chunk.clone())); + } else if let Some(empty_idx) = empty_lanes.pop() { + self.record_replace(empty_idx, Some(new_chunk.clone())); } else { self.record_insert( self.current.len(), @@ -118,10 +130,18 @@ impl Buffer { parent_b: None, marker: Markers::Commit, }; - self.record_insert( - self.current.len(), - Some(new_lane), - ); + + if let Some(empty_idx) = empty_lanes.pop() { + self.record_replace( + empty_idx, + Some(new_lane), + ); + } else { + self.record_insert( + self.current.len(), + Some(new_lane), + ); + } } } } From a545ea0950a33fe068777c1c5f100c1601dca10a Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:34:25 -0400 Subject: [PATCH 21/54] Formatting --- asyncgit/src/graph/chunk.rs | 12 ++++++------ asyncgit/src/revlog.rs | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs index ae74013cfc..cbfe6680ac 100644 --- a/asyncgit/src/graph/chunk.rs +++ b/asyncgit/src/graph/chunk.rs @@ -1,13 +1,13 @@ #[derive(Clone, Debug, PartialEq)] pub enum Markers { - Uncommitted, - Commit, + Uncommitted, + Commit, } #[derive(Clone, Debug)] pub struct Chunk { - pub alias: Option, - pub parent_a: Option, - pub parent_b: Option, - pub marker: Markers, + pub alias: Option, + pub parent_a: Option, + pub parent_b: Option, + pub marker: Markers, } diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index 480431097c..607a2babf1 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -419,7 +419,8 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); - let arc_graph_walker = Arc::new(Mutex::new(GraphWalker::new())); + let arc_graph_walker = + Arc::new(Mutex::new(GraphWalker::new())); let result = AsyncLog::fetch_helper_without_filter( &subdir_path, @@ -449,7 +450,8 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); - let arc_graph_walker = Arc::new(Mutex::new(GraphWalker::new())); + let arc_graph_walker = + Arc::new(Mutex::new(GraphWalker::new())); std::env::set_var("GIT_DIR", git_dir); From b46716e56da34bd446653f456acb9bf0aa7a234b Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:39:07 -0400 Subject: [PATCH 22/54] unit test #1 --- src/components/commitlist.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 9e003d4d2c..c21d76011d 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1280,4 +1280,24 @@ mod tests { ))) ); } + + #[test] + fn test_build_graph_spans() { + let cl = CommitList::default(); + let row = GraphRow { + lane_count: 1, + commit_lane: 0, + is_merge: false, + is_branch_tip: false, + is_stash: false, + lanes: vec![Some((ConnType::CommitNormal, 0))], + merge_bridge: None, + branches: vec![], + }; + let empty_lanes = std::collections::HashSet::new(); + let spans = cl.build_graph_spans(&row, 1, &empty_lanes); + + assert_eq!(spans.len(), 1); + assert_eq!(spans[0].content, Cow::from(SYM_COMMIT)); + } } From 30a00e3cc1e12757bed305830222a0684efe3f0e Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:39:27 -0400 Subject: [PATCH 23/54] fmt --- src/components/commitlist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index c21d76011d..449944f6f2 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1296,7 +1296,7 @@ mod tests { }; let empty_lanes = std::collections::HashSet::new(); let spans = cl.build_graph_spans(&row, 1, &empty_lanes); - + assert_eq!(spans.len(), 1); assert_eq!(spans[0].content, Cow::from(SYM_COMMIT)); } From 989b395990e5ef8bee0b8db3f132f078e0f1bc70 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:40:25 -0400 Subject: [PATCH 24/54] Added changelog entry --- CHANGELOG.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db667675c..859f74894c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +* commit log graph visualizing branch topology [[@philocalyst](https://github.com/philocalyst)] + ### Changed * use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting * open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805)) @@ -16,17 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) * when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748)) -## [0.28.1] - 2026-03-21 - -### Changed -* support proper pre-push hook ([#2809](https://github.com/gitui-org/gitui/issues/2809)) -* improve `gitui --version` message [[@hlsxx](https://github.com/hlsxx)] ([#2838](https://github.com/gitui-org/gitui/issues/2838)) -* rust msrv bumped to `1.88` - -### Fixed -* fix extremely slow status loading in large repositories by replacing time-based cache invalidation with generation counter [[@DannyStoll1](https://github.com/DannyStoll1)] ([#2823](https://github.com/gitui-org/gitui/issues/2823)) -* fix panic when renaming or updating remote URL with no remotes configured [[@xvchris](https://github.com/xvchris)] ([#2868](https://github.com/gitui-org/gitui/issues/2868)) - ## [0.28.0] - 2025-12-14 **discard changes on checkout** From 3eb8699c30ce0a60106b5434564728e03103a0d0 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 20:48:41 -0400 Subject: [PATCH 25/54] Fixed clippy lints --- asyncgit/src/graph/buffer.rs | 78 ++++++------- asyncgit/src/graph/chunk.rs | 2 +- asyncgit/src/graph/mod.rs | 6 +- asyncgit/src/graph/oids.rs | 5 +- asyncgit/src/graph/walker.rs | 187 ++++++++++++++++++------------ asyncgit/src/sync/commits_info.rs | 8 +- 6 files changed, 153 insertions(+), 133 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 37d6f01b0c..d197a3b828 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -43,7 +43,7 @@ impl Buffer { self.mergers.push(alias); } - pub fn update(&mut self, new_chunk: Chunk) { + pub fn update(&mut self, new_chunk: &Chunk) { self.pending_delta.clear(); let mut empty_lanes: Vec = self @@ -56,17 +56,15 @@ impl Buffer { // sort descending so we can pop the lowest index first empty_lanes.sort_unstable_by(|a, b| b.cmp(a)); - let mut found_idx = None; - if new_chunk.alias.is_some() { - for (i, c) in self.current.iter().enumerate() { - if let Some(c) = c { - if c.parent_a == new_chunk.alias { - found_idx = Some(i); - break; - } - } - } - } + let found_idx = if new_chunk.alias.is_some() { + self.current.iter().enumerate().find_map(|(i, c)| { + c.as_ref().and_then(|c| { + (c.parent_a == new_chunk.alias).then_some(i) + }) + }) + } else { + None + }; if let Some(idx) = found_idx { self.record_replace(idx, Some(new_chunk.clone())); @@ -89,20 +87,18 @@ impl Buffer { } if let Some(mut c) = self.current[index].clone() { - let mut changed = false; - - if new_chunk.alias.is_some() - && c.parent_a == new_chunk.alias - { - c.parent_a = None; - changed = true; - } - if new_chunk.alias.is_some() - && c.parent_b == new_chunk.alias - { - c.parent_b = None; - changed = true; - } + let changed = new_chunk.alias.is_some_and(|alias| { + let mut changed = false; + if c.parent_a == Some(alias) { + c.parent_a = None; + changed = true; + } + if c.parent_b == Some(alias) { + c.parent_b = None; + changed = true; + } + changed + }); if changed { if c.parent_a.is_none() && c.parent_b.is_none() { @@ -117,7 +113,7 @@ impl Buffer { while let Some(alias) = self.mergers.pop() { if let Some(index) = self.current.iter().position(|c| { c.as_ref() - .map_or(false, |chunk| chunk.alias == Some(alias)) + .is_some_and(|chunk| chunk.alias == Some(alias)) }) { if let Some(mut c) = self.current[index].clone() { let parent_b = c.parent_b; @@ -146,14 +142,8 @@ impl Buffer { } } - loop { - if let Some(last) = self.current.last() { - if last.is_none() { - self.record_remove(self.current.len() - 1); - continue; - } - } - break; + while self.current.last().is_some_and(Option::is_none) { + self.record_remove(self.current.len() - 1); } let delta = Delta(self.pending_delta.clone()); @@ -193,12 +183,11 @@ impl Buffer { start: usize, end: usize, ) -> Vec>> { - let (current_index, mut state) = self - .checkpoints - .range(..=start) - .next_back() - .map(|(&i, s)| (Some(i), s.clone())) - .unwrap_or((None, Vector::new())); + let (current_index, mut state) = + self.checkpoints.range(..=start).next_back().map_or_else( + || (None, Vector::new()), + |(&i, s)| (Some(i), s.clone()), + ); let mut history = Vec::with_capacity(end.saturating_sub(start) + 1); @@ -209,11 +198,11 @@ impl Buffer { } } - let loop_start = current_index.map(|i| i + 1).unwrap_or(0); + let loop_start = current_index.map_or(0, |i| i + 1); for delta_index in loop_start..=end { if let Some(delta) = self.deltas.get(delta_index) { - self.apply_delta_to_state(&mut state, delta); + Self::apply_delta_to_state(&mut state, delta); if delta_index >= start { history.push(state.clone()); @@ -227,14 +216,13 @@ impl Buffer { } fn apply_delta_to_state( - &self, state: &mut Vector>, delta: &Delta, ) { for op in &delta.0 { match op { DeltaOp::Insert { index, item } => { - state.insert(*index, item.clone()) + state.insert(*index, item.clone()); } DeltaOp::Remove { index } => { state.remove(*index); diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs index cbfe6680ac..fb0adacf13 100644 --- a/asyncgit/src/graph/chunk.rs +++ b/asyncgit/src/graph/chunk.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Markers { Uncommitted, Commit, diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index 11f3b083ee..1ca9b16084 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -5,7 +5,7 @@ pub mod walker; pub use walker::GraphWalker; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ConnType { Vertical, VerticalDotted, @@ -41,11 +41,11 @@ pub struct GraphRow { /// Connections emitted per lane: /// None = empty space - /// Some((ConnType, color_index)) = draw this connector in this color + /// Some((ConnType, `color_index`)) = draw this connector in this color pub lanes: Vec>, /// Horizontal merge bridge: if this commit merges rightward, - /// (from_lane, to_lane) — the span to draw ─ ╭ ╮ across + /// (`from_lane`, `to_lane`) — the span to draw ─ ╭ ╮ across pub merge_bridge: Option<(usize, usize)>, /// Horizontal branch bridges: if this commit spawns branches, diff --git a/asyncgit/src/graph/oids.rs b/asyncgit/src/graph/oids.rs index b6c5e5b2a2..0dbecdf441 100644 --- a/asyncgit/src/graph/oids.rs +++ b/asyncgit/src/graph/oids.rs @@ -5,7 +5,7 @@ pub struct Oids { /// alias pub ids: Vec, - /// CommitId to alias + /// `CommitId` to alias pub aliases: HashMap, } @@ -27,7 +27,8 @@ impl Oids { if let Some(&alias) = self.aliases.get(id) { return alias; } - let alias = self.ids.len() as u32; + let alias = + u32::try_from(self.ids.len()).expect("too many oids"); self.ids.push(*id); self.aliases.insert(*id, alias); alias diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index ea39cb3f52..14196fe35f 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -30,38 +30,38 @@ impl GraphWalker { } pub fn process(&mut self, commit: &CommitInfo) { - let alias = Some(self.oids.get_or_insert(&commit.id)); - let parent_a = - commit.parents.get(0).map(|p| self.oids.get_or_insert(p)); + let alias = self.oids.get_or_insert(&commit.id); + let parent_a = commit + .parents + .first() + .map(|p| self.oids.get_or_insert(p)); let parent_b = commit.parents.get(1).map(|p| self.oids.get_or_insert(p)); let chunk = Chunk { - alias, + alias: Some(alias), parent_a, parent_b, marker: Markers::Commit, }; - if let (Some(a), Some(b)) = (alias, parent_b) { - self.mergers_map.insert(a, b); + if let Some(b) = parent_b { + self.mergers_map.insert(alias, b); } if parent_a.is_some() && parent_b.is_some() { let already_tracked = self.buffer.current.iter().any(|c| { - if let Some(c) = c { + c.as_ref().is_some_and(|c| { c.parent_a == parent_b && c.parent_b.is_none() - } else { - false - } + }) }); if !already_tracked { - self.buffer.merger(alias.unwrap()); + self.buffer.merger(alias); } } - self.buffer.update(chunk); + self.buffer.update(&chunk); } pub fn snapshot_at( @@ -125,14 +125,14 @@ impl GraphWalker { let commit_lane = curr .iter() .position(|c| { - c.as_ref().map_or(false, |chunk| { + c.as_ref().is_some_and(|chunk| { alias.is_some() && chunk.alias == alias }) }) .unwrap_or(0); let parent_b_alias = - alias.and_then(|a| self.mergers_map.get(&a).cloned()); + alias.and_then(|a| self.mergers_map.get(&a).copied()); let is_merge = parent_b_alias.is_some(); let is_branch_tip = branch_tips.contains(commit_id); @@ -144,7 +144,7 @@ impl GraphWalker { .enumerate() .filter(|(i, pc)| { pc.is_some() - && curr.get(*i).map_or(true, |c| c.is_none()) + && curr.get(*i).is_none_or(Option::is_none) }) .map(|(i, _)| i) .collect(); @@ -154,7 +154,7 @@ impl GraphWalker { let merge_bridge = is_merge .then(|| { let target_lane = curr.iter().position(|c| { - c.as_ref().map_or(false, |chunk| { + c.as_ref().is_some_and(|chunk| { parent_b_alias.is_some() && chunk.parent_a == parent_b_alias }) @@ -169,19 +169,84 @@ impl GraphWalker { }) .flatten(); + self.fill_lanes( + &mut lanes, + curr, + alias, + head_id, + is_stash, + is_merge, + is_branch_tip, + &branching_lanes, + ); + + if let Some((from, to)) = merge_bridge { + Self::draw_bridge( + &mut lanes, + from, + to, + commit_lane, + ConnType::MergeBridgeMid, + ConnType::MergeBridgeStart, + ConnType::MergeBridgeEnd, + ); + } + + let mut branches = Vec::new(); + for &branch_lane in &branching_lanes { + let from = std::cmp::min(branch_lane, commit_lane); + let to = std::cmp::max(branch_lane, commit_lane); + branches.push((from, to)); + + if lanes.len() <= to { + lanes.resize(to + 1, None); + } + + Self::draw_bridge( + &mut lanes, + from, + to, + branch_lane, + ConnType::MergeBridgeMid, + ConnType::BranchUp, + ConnType::BranchUpRight, + ); + } + + GraphRow { + lane_count: curr.iter().flatten().count(), + commit_lane, + is_merge, + is_branch_tip, + is_stash, + lanes, + merge_bridge, + branches, + } + } + + #[allow(clippy::too_many_arguments)] + fn fill_lanes( + &self, + lanes: &mut [Option<(ConnType, usize)>], + curr: &Vector>, + alias: Option, + head_id: Option<&CommitId>, + is_stash: bool, + is_merge: bool, + is_branch_tip: bool, + branching_lanes: &[usize], + ) { for (lane_idx, chunk_item) in curr.iter().enumerate() { - if chunk_item.is_none() { + let Some(chunk) = chunk_item.as_ref() else { if branching_lanes.contains(&lane_idx) { lanes[lane_idx] = Some((ConnType::BranchUp, lane_idx % 16)); } continue; - } - - let chunk = chunk_item.as_ref().unwrap(); + }; if alias.is_some() && chunk.alias == alias { - // basically a from impl inline here let conn_type = match (is_stash, is_merge, is_branch_tip) { (true, _, _) => ConnType::CommitStash, @@ -202,70 +267,38 @@ impl GraphWalker { let is_orphan = chunk.parent_a.is_none() && chunk.parent_b.is_none(); - let conn = match (is_dotted, is_orphan) { - (true, _) => ConnType::VerticalDotted, - (_, true) => continue, - _ => ConnType::Vertical, + if is_orphan { + continue; + } + + let conn = if is_dotted { + ConnType::VerticalDotted + } else { + ConnType::Vertical }; lanes[lane_idx] = Some((conn, lane_idx % 16)); } } + } - if let Some((from, to)) = merge_bridge { - for bridge_lane in (from + 1)..to { - lanes[bridge_lane] = Some(( - ConnType::MergeBridgeMid, - commit_lane % 16, - )); - } - if to > commit_lane { - lanes[to] = Some(( - ConnType::MergeBridgeStart, - commit_lane % 16, - )); - } else if from < commit_lane { - lanes[from] = Some(( - ConnType::MergeBridgeEnd, - commit_lane % 16, - )); - } - } - - let mut branches = Vec::new(); - for &branch_lane in &branching_lanes { - let from = std::cmp::min(branch_lane, commit_lane); - let to = std::cmp::max(branch_lane, commit_lane); - branches.push((from, to)); - - if lanes.len() <= to { - lanes.resize(to + 1, None); - } - - for bridge_lane in (from + 1)..to { - lanes[bridge_lane] = Some(( - ConnType::MergeBridgeMid, - branch_lane % 16, - )); - } - if to > commit_lane { - lanes[to] = - Some((ConnType::BranchUp, branch_lane % 16)); - } else if from < commit_lane { - lanes[from] = - Some((ConnType::BranchUpRight, branch_lane % 16)); - } + fn draw_bridge( + lanes: &mut [Option<(ConnType, usize)>], + from: usize, + to: usize, + color_lane: usize, + mid: ConnType, + start: ConnType, + end: ConnType, + ) { + for lane in lanes.iter_mut().take(to).skip(from + 1) { + *lane = Some((mid, color_lane % 16)); } - GraphRow { - lane_count: curr.iter().filter(|c| c.is_some()).count(), - commit_lane, - is_merge, - is_branch_tip, - is_stash, - lanes, - merge_bridge, - branches, + if to > color_lane { + lanes[to] = Some((start, color_lane % 16)); + } else if from < color_lane { + lanes[from] = Some((end, color_lane % 16)); } } } diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index e36de46a6d..44c9a3c506 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -201,11 +201,9 @@ pub fn get_commit_info( .parents .iter() .take(2) - .map(|p| { - CommitId::from_str_unchecked(&p.to_string()) - .expect("valid oid") - }) - .collect(), + .map(|p| CommitId::from_str_unchecked(&p.to_string())) + .collect::, _>>()? + .into(), }) } From 4383b3d23fd8c4d95854f7ed834550d8ddaeda51 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 21:54:34 -0400 Subject: [PATCH 26/54] More clippy lints --- asyncgit/src/graph/buffer.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index d197a3b828..34e739397e 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -88,16 +88,15 @@ impl Buffer { if let Some(mut c) = self.current[index].clone() { let changed = new_chunk.alias.is_some_and(|alias| { - let mut changed = false; - if c.parent_a == Some(alias) { + let a = c.parent_a == Some(alias); + let b = c.parent_b == Some(alias); + if a { c.parent_a = None; - changed = true; } - if c.parent_b == Some(alias) { + if b { c.parent_b = None; - changed = true; } - changed + a || b }); if changed { From 7df452bf2260c2ec510678dccf368f2c2b7d61d4 Mon Sep 17 00:00:00 2001 From: Myles Wirth Date: Mon, 23 Mar 2026 21:57:35 -0400 Subject: [PATCH 27/54] Fixed more clippy lints --- src/components/commitlist.rs | 125 +++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 49 deletions(-) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 449944f6f2..0dffb4b182 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -202,14 +202,14 @@ impl CommitList { } /// - pub fn local_branches( + pub const fn local_branches( &self, ) -> &std::collections::BTreeMap> { &self.local_branches } /// - pub fn remote_branches( + pub const fn remote_branches( &self, ) -> &std::collections::BTreeMap> { &self.remote_branches @@ -590,26 +590,14 @@ impl CommitList { ); if self.show_graph { - if let Some(ref row) = e.graph { - txt.extend(self.build_graph_spans( - row, - self.graph_col_width.get(), - empty_lanes, - )); - } else { - txt.push(Span::raw( - " ".repeat(self.graph_col_width.get()), - )); - } - txt.push(Span::raw(" ")); + self.add_graph_spans(e, &mut txt, empty_lanes); } let normal = !self.items.highlighting() || (self.items.highlighting() && e.highlighted); - let splitter_txt = Cow::from(symbol::EMPTY_SPACE); let splitter = Span::styled( - splitter_txt, + Cow::from(symbol::EMPTY_SPACE), if normal { theme.text(true, selected) } else { @@ -630,40 +618,80 @@ impl CommitList { txt.push(splitter.clone()); } - let style_hash = if normal { - theme.commit_hash(selected) - } else { - theme.commit_unhighlighted() - }; - let style_time = if normal { - theme.commit_time(selected) - } else { - theme.commit_unhighlighted() - }; - let style_author = if normal { - theme.commit_author(selected) - } else { - theme.commit_unhighlighted() - }; - let style_tags = if normal { - theme.tags(selected) - } else { - theme.commit_unhighlighted() - }; - let style_branches = if normal { - theme.branch(selected, true) + Self::add_entry_details( + e, + &mut txt, + selected, + normal, + theme, + width, + now, + tags, + local_branches, + remote_branches, + &splitter, + ); + + Line::from(txt) + } + + fn add_graph_spans<'a>( + &self, + e: &'a LogEntry, + txt: &mut Vec>, + empty_lanes: &std::collections::HashSet, + ) { + if let Some(ref row) = e.graph { + txt.extend(self.build_graph_spans( + row, + self.graph_col_width.get(), + empty_lanes, + )); } else { - theme.commit_unhighlighted() - }; - let style_msg = if normal { - theme.text(true, selected) + txt.push(Span::raw( + " ".repeat(self.graph_col_width.get()), + )); + } + txt.push(Span::raw(" ")); + } + + #[allow(clippy::too_many_arguments)] + fn add_entry_details<'a>( + e: &'a LogEntry, + txt: &mut Vec>, + selected: bool, + normal: bool, + theme: &Theme, + width: usize, + now: DateTime, + tags: Option, + local_branches: Option, + remote_branches: Option, + splitter: &Span<'a>, + ) { + let ( + style_hash, + style_time, + style_author, + style_tags, + style_branches, + style_msg, + ) = if normal { + ( + theme.commit_hash(selected), + theme.commit_time(selected), + theme.commit_author(selected), + theme.tags(selected), + theme.branch(selected, true), + theme.text(true, selected), + ) } else { - theme.commit_unhighlighted() + let s = theme.commit_unhighlighted(); + (s, s, s, s, s, s) }; // commit hash txt.push(Span::styled(Cow::from(&*e.hash_short), style_hash)); - txt.push(splitter.clone()); // commit timestamp @@ -671,7 +699,6 @@ impl CommitList { Cow::from(e.time_to_string(now)), style_time, )); - txt.push(splitter.clone()); let author_width = @@ -680,7 +707,6 @@ impl CommitList { // commit author txt.push(Span::styled(author, style_author)); - txt.push(splitter.clone()); // commit tags @@ -698,7 +724,7 @@ impl CommitList { txt.push(Span::styled(remote_branches, style_branches)); } - txt.push(splitter); + txt.push(splitter.clone()); let message_width = width.saturating_sub( txt.iter().map(|span| span.content.len()).sum(), @@ -709,8 +735,6 @@ impl CommitList { format!("{:message_width$}", &e.msg), style_msg, )); - - Line::from(txt) } fn get_text(&self, height: usize, width: usize) -> Vec> { @@ -1133,6 +1157,8 @@ mod tests { std::path::PathBuf::default(), )), queue: Queue::default(), + show_graph: true, + graph_col_width: Cell::new(0), } } } @@ -1167,6 +1193,7 @@ mod tests { time: 0, author: String::default(), id: CommitId::default(), + parents: Vec::default().into(), }; // This just creates a sequence of fake ordered ids // 0000000000000000000000000000000000000000 From 8799f97595aa7ca9d43efe0ddc6e5116c4afc55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 16 Apr 2026 21:59:53 -0400 Subject: [PATCH 28/54] Added cross type --- asyncgit/src/graph/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index 1ca9b16084..9acdfd4d4c 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -9,6 +9,7 @@ pub use walker::GraphWalker; pub enum ConnType { Vertical, VerticalDotted, + Cross, CommitNormal, CommitBranch, CommitMerge, From 9eba84fc07e0fce6d23e9f442edbd42437096bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 16 Apr 2026 22:00:05 -0400 Subject: [PATCH 29/54] Fixed the rest of the corner logic This was annoying --- asyncgit/src/graph/walker.rs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index 14196fe35f..b04645a9ab 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -181,11 +181,14 @@ impl GraphWalker { ); if let Some((from, to)) = merge_bridge { + let target_lane = + if from == commit_lane { to } else { from }; Self::draw_bridge( &mut lanes, from, to, commit_lane, + target_lane, ConnType::MergeBridgeMid, ConnType::MergeBridgeStart, ConnType::MergeBridgeEnd, @@ -207,6 +210,7 @@ impl GraphWalker { from, to, branch_lane, + branch_lane, ConnType::MergeBridgeMid, ConnType::BranchUp, ConnType::BranchUpRight, @@ -287,18 +291,29 @@ impl GraphWalker { from: usize, to: usize, color_lane: usize, + corner_lane: usize, mid: ConnType, - start: ConnType, - end: ConnType, + corner_right: ConnType, + corner_left: ConnType, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { - *lane = Some((mid, color_lane % 16)); + match lane { + Some(( + ConnType::Vertical | ConnType::VerticalDotted, + _, + )) => { + *lane = Some((ConnType::Cross, color_lane % 16)); + } + _ => { + *lane = Some((mid, color_lane % 16)); + } + } } - if to > color_lane { - lanes[to] = Some((start, color_lane % 16)); - } else if from < color_lane { - lanes[from] = Some((end, color_lane % 16)); + if corner_lane == to { + lanes[to] = Some((corner_right, color_lane % 16)); + } else if corner_lane == from { + lanes[from] = Some((corner_left, color_lane % 16)); } } } From a2ed29ecd5964dcd065c6bd24c40545ccf0fb9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 16 Apr 2026 22:01:45 -0400 Subject: [PATCH 30/54] Implemented differentiation With color and crosses! --- src/components/commitlist.rs | 82 +++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 0dffb4b182..3e9c407ed1 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1,7 +1,7 @@ use super::utils::graphrow::{ SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, SYM_COMMIT, SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, SYM_COMMIT_STASH, - SYM_COMMIT_UNCOMMITTED, SYM_HORIZONTAL, SYM_MERGE_BRIDGE_END, + SYM_COMMIT_UNCOMMITTED, SYM_CROSS, SYM_HORIZONTAL, SYM_MERGE_BRIDGE_END, SYM_MERGE_BRIDGE_MID, SYM_MERGE_BRIDGE_START, SYM_SPACE, SYM_VERTICAL, SYM_VERTICAL_DOTTED, }; @@ -46,6 +46,15 @@ use std::{ const ELEMENTS_PER_LINE: usize = 9; const SLICE_SIZE: usize = 1200; +const GRAPH_COLORS: &[Color] = &[ + Color::Blue, + Color::Yellow, + Color::Magenta, + Color::Cyan, + Color::Green, + Color::Red, +]; + /// pub struct CommitList { repo: RepoPathRef, @@ -487,76 +496,80 @@ impl CommitList { ) -> Vec> { let mut spans = Vec::new(); - let graph_color = - self.theme.commit_hash(false).fg.unwrap_or(Color::Reset); - for (lane_index, conn) in row.lanes.iter().enumerate() { if empty_lanes.contains(&lane_index) { continue; } - let (sym, color) = match conn { + let (sym, graph_color) = match conn { None => (SYM_SPACE, Color::Reset), - Some((ConnType::Vertical, _)) => { - (SYM_VERTICAL, graph_color) + Some((ConnType::Vertical, color_idx)) => { + (SYM_VERTICAL, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) + } + Some((ConnType::VerticalDotted, color_idx)) => { + (SYM_VERTICAL_DOTTED, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::VerticalDotted, _)) => { - (SYM_VERTICAL_DOTTED, graph_color) + Some((ConnType::Cross, color_idx)) => { + (SYM_CROSS, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::CommitNormal, _)) => { - (SYM_COMMIT, graph_color) + Some((ConnType::CommitNormal, color_idx)) => { + (SYM_COMMIT, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::CommitBranch, _)) => { - (SYM_COMMIT_BRANCH, graph_color) + Some((ConnType::CommitBranch, color_idx)) => { + (SYM_COMMIT_BRANCH, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::CommitMerge, _)) => { - (SYM_COMMIT_MERGE, graph_color) + Some((ConnType::CommitMerge, color_idx)) => { + (SYM_COMMIT_MERGE, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::CommitStash, _)) => { - (SYM_COMMIT_STASH, graph_color) + Some((ConnType::CommitStash, color_idx)) => { + (SYM_COMMIT_STASH, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::CommitUncommitted, _)) => { - (SYM_COMMIT_UNCOMMITTED, graph_color) + Some((ConnType::CommitUncommitted, color_idx)) => { + (SYM_COMMIT_UNCOMMITTED, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::MergeBridgeStart, _)) => { - (SYM_MERGE_BRIDGE_START, graph_color) + Some((ConnType::MergeBridgeStart, color_idx)) => { + (SYM_MERGE_BRIDGE_START, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::MergeBridgeMid, _)) => { - (SYM_MERGE_BRIDGE_MID, graph_color) + Some((ConnType::MergeBridgeMid, color_idx)) => { + (SYM_MERGE_BRIDGE_MID, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::MergeBridgeEnd, _)) => { - (SYM_MERGE_BRIDGE_END, graph_color) + Some((ConnType::MergeBridgeEnd, color_idx)) => { + (SYM_MERGE_BRIDGE_END, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::BranchDown, _)) => { - (SYM_BRANCH_DOWN, graph_color) + Some((ConnType::BranchDown, color_idx)) => { + (SYM_BRANCH_DOWN, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::BranchUp, _)) => { - (SYM_BRANCH_UP, graph_color) + Some((ConnType::BranchUp, color_idx)) => { + (SYM_BRANCH_UP, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } - Some((ConnType::BranchUpRight, _)) => { - (SYM_BRANCH_UP_RIGHT, graph_color) + Some((ConnType::BranchUpRight, color_idx)) => { + (SYM_BRANCH_UP_RIGHT, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) } }; - spans.push(Span::styled(sym, Style::default().fg(color))); + spans.push(Span::styled(sym, Style::default().fg(graph_color))); // Spacer let mut is_bridge_lane = false; + let mut spacer_color = graph_color; if let Some((from, to)) = row.merge_bridge { if lane_index >= from && lane_index < to { is_bridge_lane = true; + spacer_color = GRAPH_COLORS[row.commit_lane % GRAPH_COLORS.len()]; } } for &(from, to) in &row.branches { if lane_index >= from && lane_index < to { is_bridge_lane = true; + let branch_lane = if row.commit_lane == from { to } else { from }; + spacer_color = GRAPH_COLORS[branch_lane % GRAPH_COLORS.len()]; } } if is_bridge_lane { spans.push(Span::styled( SYM_HORIZONTAL, - Style::default().fg(graph_color), + Style::default().fg(spacer_color), )); continue; } @@ -1324,7 +1337,8 @@ mod tests { let empty_lanes = std::collections::HashSet::new(); let spans = cl.build_graph_spans(&row, 1, &empty_lanes); - assert_eq!(spans.len(), 1); + assert_eq!(spans.len(), 2); assert_eq!(spans[0].content, Cow::from(SYM_COMMIT)); + assert_eq!(spans[1].content, Cow::from(SYM_SPACE)); } } From d2a2243eddfbafdd8a0d9ccdc32a5adad7c69532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 16 Apr 2026 22:02:02 -0400 Subject: [PATCH 31/54] Switched to Tigs method of symbolization --- src/components/utils/graphrow.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/utils/graphrow.rs b/src/components/utils/graphrow.rs index db44162a1b..d626b6640f 100644 --- a/src/components/utils/graphrow.rs +++ b/src/components/utils/graphrow.rs @@ -1,8 +1,9 @@ -pub const SYM_COMMIT: &str = "◆"; -pub const SYM_COMMIT_BRANCH: &str = "◈"; -pub const SYM_COMMIT_MERGE: &str = "◇"; -pub const SYM_COMMIT_STASH: &str = "⟡"; -pub const SYM_COMMIT_UNCOMMITTED: &str = "◻"; +pub const SYM_COMMIT: &str = "o"; +pub const SYM_COMMIT_BRANCH: &str = "*"; +pub const SYM_COMMIT_MERGE: &str = "M"; +pub const SYM_COMMIT_STASH: &str = "*"; +pub const SYM_COMMIT_UNCOMMITTED: &str = "+"; +pub const SYM_CROSS: &str = "╋"; pub const SYM_VERTICAL: &str = "┃"; pub const SYM_VERTICAL_DOTTED: &str = "╏"; pub const SYM_HORIZONTAL: &str = "━"; From 1b43466d5c83552358fdce4faaa954757251029e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 23 May 2026 19:25:04 -0400 Subject: [PATCH 32/54] usizes all the way down --- asyncgit/src/graph/buffer.rs | 4 ++-- asyncgit/src/graph/chunk.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 34e739397e..4f861f366c 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -18,7 +18,7 @@ pub struct Buffer { pub current: Vector>, pub deltas: Vec, pub checkpoints: BTreeMap>>, - mergers: Vec, + mergers: Vec, pending_delta: Vec, } @@ -39,7 +39,7 @@ impl Buffer { } } - pub fn merger(&mut self, alias: u32) { + pub fn merger(&mut self, alias: usize) { self.mergers.push(alias); } diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs index fb0adacf13..dd3b637a70 100644 --- a/asyncgit/src/graph/chunk.rs +++ b/asyncgit/src/graph/chunk.rs @@ -6,8 +6,8 @@ pub enum Markers { #[derive(Clone, Debug)] pub struct Chunk { - pub alias: Option, - pub parent_a: Option, - pub parent_b: Option, + pub alias: Option, + pub parent_a: Option, + pub parent_b: Option, pub marker: Markers, } From ebd15111b814d271d35c078cdc002de89838a8bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 23 May 2026 19:25:13 -0400 Subject: [PATCH 33/54] better names for important things --- asyncgit/src/graph/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index 9acdfd4d4c..e1c8bee521 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -5,8 +5,12 @@ pub mod walker; pub use walker::GraphWalker; +/// The maximum number of colors to use for graph lanes +pub const MAX_LANE_COLORS: usize = 16; + +/// The type of connection between nodes in the graph #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ConnType { +pub enum ConnectionType { Vertical, VerticalDotted, Cross, @@ -21,6 +25,8 @@ pub enum ConnType { BranchDown, BranchUp, BranchUpRight, + BranchUpMergeStart, + BranchUpRightMergeEnd, } #[derive(Clone, Debug, Default)] @@ -42,8 +48,8 @@ pub struct GraphRow { /// Connections emitted per lane: /// None = empty space - /// Some((ConnType, `color_index`)) = draw this connector in this color - pub lanes: Vec>, + /// Some((ConnectionType, `color_index`)) = draw this connector in this color + pub lanes: Vec>, /// Horizontal merge bridge: if this commit merges rightward, /// (`from_lane`, `to_lane`) — the span to draw ─ ╭ ╮ across From 2c72c7e0379bb59ff45832ebb116ed7e217685b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 23 May 2026 19:25:21 -0400 Subject: [PATCH 34/54] simplifie the OIDs structure --- asyncgit/src/graph/oids.rs | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/asyncgit/src/graph/oids.rs b/asyncgit/src/graph/oids.rs index 0dbecdf441..792d99b09f 100644 --- a/asyncgit/src/graph/oids.rs +++ b/asyncgit/src/graph/oids.rs @@ -1,40 +1,34 @@ use crate::sync::CommitId; use std::collections::HashMap; -pub struct Oids { - /// alias - pub ids: Vec, +/// mapping of `CommitId` to a numeric alias +pub struct GraphOids(HashMap); - /// `CommitId` to alias - pub aliases: HashMap, -} - -impl Default for Oids { +impl Default for GraphOids { fn default() -> Self { Self::new() } } -impl Oids { +impl GraphOids { + /// pub fn new() -> Self { - Self { - ids: Vec::new(), - aliases: HashMap::new(), - } + Self(HashMap::new()) } - pub fn get_or_insert(&mut self, id: &CommitId) -> u32 { - if let Some(&alias) = self.aliases.get(id) { + /// + pub fn get_or_insert(&mut self, id: &CommitId) -> usize { + if let Some(&alias) = self.0.get(id) { return alias; } - let alias = - u32::try_from(self.ids.len()).expect("too many oids"); - self.ids.push(*id); - self.aliases.insert(*id, alias); + + let alias = self.0.len(); + self.0.insert(*id, alias); alias } - pub fn get(&self, id: &CommitId) -> Option { - self.aliases.get(id).copied() + /// + pub fn get(&self, id: &CommitId) -> Option { + self.0.get(id).copied() } } From 47e16948086911808be836d06980a6d3c8c9a931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 23 May 2026 19:25:36 -0400 Subject: [PATCH 35/54] renamings and more precise structure --- asyncgit/src/graph/walker.rs | 70 +++++++++-------- src/components/commitlist.rs | 144 ++++++++++++++++++++++------------- 2 files changed, 128 insertions(+), 86 deletions(-) diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index b04645a9ab..0c0131f66b 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -1,16 +1,16 @@ use super::buffer::Buffer; use super::chunk::{Chunk, Markers}; -use super::oids::Oids; -use super::{ConnType, GraphRow}; +use super::oids::GraphOids; +use super::{ConnectionType, GraphRow, MAX_LANE_COLORS}; use crate::sync::{CommitId, CommitInfo}; use im::Vector; use std::collections::{HashMap, HashSet}; pub struct GraphWalker { pub buffer: Buffer, - pub oids: Oids, + pub oids: GraphOids, pub branch_lane_map: HashMap, - pub mergers_map: HashMap, + pub mergers_map: HashMap, } impl Default for GraphWalker { @@ -23,7 +23,7 @@ impl GraphWalker { pub fn new() -> Self { Self { buffer: Buffer::new(), - oids: Oids::new(), + oids: GraphOids::new(), branch_lane_map: HashMap::new(), mergers_map: HashMap::new(), } @@ -189,9 +189,9 @@ impl GraphWalker { to, commit_lane, target_lane, - ConnType::MergeBridgeMid, - ConnType::MergeBridgeStart, - ConnType::MergeBridgeEnd, + ConnectionType::MergeBridgeMid, + ConnectionType::MergeBridgeStart, + ConnectionType::MergeBridgeEnd, ); } @@ -211,9 +211,9 @@ impl GraphWalker { to, branch_lane, branch_lane, - ConnType::MergeBridgeMid, - ConnType::BranchUp, - ConnType::BranchUpRight, + ConnectionType::MergeBridgeMid, + ConnectionType::BranchUp, + ConnectionType::BranchUpRight, ); } @@ -232,9 +232,9 @@ impl GraphWalker { #[allow(clippy::too_many_arguments)] fn fill_lanes( &self, - lanes: &mut [Option<(ConnType, usize)>], + lanes: &mut [Option<(ConnectionType, usize)>], curr: &Vector>, - alias: Option, + alias: Option, head_id: Option<&CommitId>, is_stash: bool, is_merge: bool, @@ -245,7 +245,7 @@ impl GraphWalker { let Some(chunk) = chunk_item.as_ref() else { if branching_lanes.contains(&lane_idx) { lanes[lane_idx] = - Some((ConnType::BranchUp, lane_idx % 16)); + Some((ConnectionType::BranchUp, lane_idx % MAX_LANE_COLORS)); } continue; }; @@ -253,13 +253,13 @@ impl GraphWalker { if alias.is_some() && chunk.alias == alias { let conn_type = match (is_stash, is_merge, is_branch_tip) { - (true, _, _) => ConnType::CommitStash, - (_, true, _) => ConnType::CommitMerge, - (_, _, true) => ConnType::CommitBranch, - _ => ConnType::CommitNormal, + (true, _, _) => ConnectionType::CommitStash, + (_, true, _) => ConnectionType::CommitMerge, + (_, _, true) => ConnectionType::CommitBranch, + _ => ConnectionType::CommitNormal, }; - lanes[lane_idx] = Some((conn_type, lane_idx % 16)); + lanes[lane_idx] = Some((conn_type, lane_idx % MAX_LANE_COLORS)); } else { let is_dotted = head_id .and_then(|h| self.oids.get(h)) @@ -276,44 +276,52 @@ impl GraphWalker { } let conn = if is_dotted { - ConnType::VerticalDotted + ConnectionType::VerticalDotted } else { - ConnType::Vertical + ConnectionType::Vertical }; - lanes[lane_idx] = Some((conn, lane_idx % 16)); + lanes[lane_idx] = Some((conn, lane_idx % MAX_LANE_COLORS)); } } } fn draw_bridge( - lanes: &mut [Option<(ConnType, usize)>], + lanes: &mut [Option<(ConnectionType, usize)>], from: usize, to: usize, color_lane: usize, corner_lane: usize, - mid: ConnType, - corner_right: ConnType, - corner_left: ConnType, + mid: ConnectionType, + corner_right: ConnectionType, + corner_left: ConnectionType, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { match lane { Some(( - ConnType::Vertical | ConnType::VerticalDotted, + ConnectionType::Vertical | ConnectionType::VerticalDotted, _, )) => { - *lane = Some((ConnType::Cross, color_lane % 16)); + *lane = Some((ConnectionType::Cross, color_lane % MAX_LANE_COLORS)); } _ => { - *lane = Some((mid, color_lane % 16)); + *lane = Some((mid, color_lane % MAX_LANE_COLORS)); } } } if corner_lane == to { - lanes[to] = Some((corner_right, color_lane % 16)); + let new_corner = match (lanes[to], corner_right) { + (Some((ConnectionType::MergeBridgeStart, _)), ConnectionType::BranchUp) => ConnectionType::BranchUpMergeStart, + _ => corner_right, + }; + lanes[to] = Some((new_corner, color_lane % MAX_LANE_COLORS)); } else if corner_lane == from { - lanes[from] = Some((corner_left, color_lane % 16)); + let new_corner = match (lanes[from], corner_left) { + (Some((ConnectionType::MergeBridgeEnd, _)), ConnectionType::BranchUpRight) => ConnectionType::BranchUpRightMergeEnd, + _ => corner_left, + }; + lanes[from] = Some((new_corner, color_lane % MAX_LANE_COLORS)); } } } diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 3e9c407ed1..20aa791e16 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1,9 +1,10 @@ use super::utils::graphrow::{ SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, SYM_COMMIT, SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, SYM_COMMIT_STASH, - SYM_COMMIT_UNCOMMITTED, SYM_CROSS, SYM_HORIZONTAL, SYM_MERGE_BRIDGE_END, - SYM_MERGE_BRIDGE_MID, SYM_MERGE_BRIDGE_START, SYM_SPACE, - SYM_VERTICAL, SYM_VERTICAL_DOTTED, + SYM_COMMIT_UNCOMMITTED, SYM_CROSS, SYM_HORIZONTAL, + SYM_MERGE_BRIDGE_END, SYM_MERGE_BRIDGE_MID, + SYM_MERGE_BRIDGE_START, SYM_SPACE, SYM_VERTICAL, + SYM_VERTICAL_DOTTED, }; use super::utils::logitems::{ItemBatch, LogEntry}; use crate::{ @@ -21,7 +22,7 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ - graph::{ConnType, GraphRow}, + graph::{ConnectionType, GraphRow}, sync::{ self, checkout_commit, BranchDetails, BranchInfo, CommitId, RepoPathRef, Tags, @@ -502,50 +503,78 @@ impl CommitList { } let (sym, graph_color) = match conn { None => (SYM_SPACE, Color::Reset), - Some((ConnType::Vertical, color_idx)) => { - (SYM_VERTICAL, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::VerticalDotted, color_idx)) => { - (SYM_VERTICAL_DOTTED, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::Cross, color_idx)) => { - (SYM_CROSS, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::CommitNormal, color_idx)) => { - (SYM_COMMIT, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::CommitBranch, color_idx)) => { - (SYM_COMMIT_BRANCH, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::CommitMerge, color_idx)) => { - (SYM_COMMIT_MERGE, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::CommitStash, color_idx)) => { - (SYM_COMMIT_STASH, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::CommitUncommitted, color_idx)) => { - (SYM_COMMIT_UNCOMMITTED, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::MergeBridgeStart, color_idx)) => { - (SYM_MERGE_BRIDGE_START, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::MergeBridgeMid, color_idx)) => { - (SYM_MERGE_BRIDGE_MID, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::MergeBridgeEnd, color_idx)) => { - (SYM_MERGE_BRIDGE_END, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::BranchDown, color_idx)) => { - (SYM_BRANCH_DOWN, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::BranchUp, color_idx)) => { - (SYM_BRANCH_UP, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } - Some((ConnType::BranchUpRight, color_idx)) => { - (SYM_BRANCH_UP_RIGHT, GRAPH_COLORS[color_idx % GRAPH_COLORS.len()]) - } + Some((ConnectionType::Vertical, color_idx)) => ( + SYM_VERTICAL, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::VerticalDotted, color_idx)) => ( + SYM_VERTICAL_DOTTED, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::Cross, color_idx)) => ( + SYM_CROSS, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::BranchUpMergeStart, color_idx)) => ( + SYM_BRANCH_UP, // Reusing '┛' + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some(( + ConnectionType::BranchUpRightMergeEnd, + color_idx, + )) => ( + SYM_BRANCH_UP_RIGHT, // Reusing '┗' + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::CommitNormal, color_idx)) => ( + SYM_COMMIT, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::CommitBranch, color_idx)) => ( + SYM_COMMIT_BRANCH, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::CommitMerge, color_idx)) => ( + SYM_COMMIT_MERGE, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::CommitStash, color_idx)) => ( + SYM_COMMIT_STASH, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::CommitUncommitted, color_idx)) => ( + SYM_COMMIT_UNCOMMITTED, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::MergeBridgeStart, color_idx)) => ( + SYM_MERGE_BRIDGE_START, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::MergeBridgeMid, color_idx)) => ( + SYM_MERGE_BRIDGE_MID, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::MergeBridgeEnd, color_idx)) => ( + SYM_MERGE_BRIDGE_END, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::BranchDown, color_idx)) => ( + SYM_BRANCH_DOWN, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::BranchUp, color_idx)) => ( + SYM_BRANCH_UP, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), + Some((ConnectionType::BranchUpRight, color_idx)) => ( + SYM_BRANCH_UP_RIGHT, + GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], + ), }; - spans.push(Span::styled(sym, Style::default().fg(graph_color))); + spans.push(Span::styled( + sym, + Style::default().fg(graph_color), + )); // Spacer let mut is_bridge_lane = false; @@ -554,15 +583,21 @@ impl CommitList { if let Some((from, to)) = row.merge_bridge { if lane_index >= from && lane_index < to { is_bridge_lane = true; - spacer_color = GRAPH_COLORS[row.commit_lane % GRAPH_COLORS.len()]; + spacer_color = GRAPH_COLORS + [row.commit_lane % GRAPH_COLORS.len()]; } } for &(from, to) in &row.branches { if lane_index >= from && lane_index < to { is_bridge_lane = true; - let branch_lane = if row.commit_lane == from { to } else { from }; - spacer_color = GRAPH_COLORS[branch_lane % GRAPH_COLORS.len()]; + let branch_lane = if row.commit_lane == from { + to + } else { + from + }; + spacer_color = GRAPH_COLORS + [branch_lane % GRAPH_COLORS.len()]; } } @@ -788,7 +823,7 @@ impl CommitList { if !matches!( conn, None | Some(( - ConnType::MergeBridgeMid, + ConnectionType::MergeBridgeMid, _ )) ) { @@ -849,7 +884,7 @@ impl CommitList { local_branches, self.remote_branches_string(e), &self.theme, - width, + width.into(), now, marked, &empty_lanes, @@ -1330,12 +1365,11 @@ mod tests { is_merge: false, is_branch_tip: false, is_stash: false, - lanes: vec![Some((ConnType::CommitNormal, 0))], + lanes: vec![Some((ConnectionType::CommitNormal, 0))], merge_bridge: None, branches: vec![], }; - let empty_lanes = std::collections::HashSet::new(); - let spans = cl.build_graph_spans(&row, 1, &empty_lanes); + let spans = cl.build_graph_spans(&row, 1); assert_eq!(spans.len(), 2); assert_eq!(spans[0].content, Cow::from(SYM_COMMIT)); From 70c8c9554e978eab52c9a7b0639dabb075371068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Mon, 8 Jun 2026 20:01:45 +0200 Subject: [PATCH 36/54] Impl From> for CommitId --- asyncgit/src/sync/commits_info.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 44c9a3c506..d1042ce76a 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -88,6 +88,15 @@ impl From for CommitId { } } +impl From> for CommitId { + fn from(object_id: gix::Id<'_>) -> Self { + #[allow(clippy::expect_used)] + let oid = Oid::from_bytes(object_id.as_bytes()).expect("`Oid::from_bytes(object_id.as_bytes())` is expected to never fail"); + + Self::new(oid) + } +} + impl From for CommitId { fn from(object_id: gix::ObjectId) -> Self { #[allow(clippy::expect_used)] @@ -161,7 +170,7 @@ pub fn get_commits_info( parents: c .parents() .take(2) - .map(|p| CommitId::new(p.id())) + .map(|p| CommitId(p.id())) .collect(), } }) @@ -197,13 +206,11 @@ pub fn get_commit_info( author: author.to_string(), time: commit_ref.time()?.seconds, id: commit.id().detach().into(), - parents: commit_ref - .parents - .iter() + parents: commit + .parent_ids() .take(2) - .map(|p| CommitId::from_str_unchecked(&p.to_string())) - .collect::, _>>()? - .into(), + .map(Into::into) + .collect(), }) } From dfd1405913757407b2868188cce83587832f0ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:13 -0400 Subject: [PATCH 37/54] chunk derive and oids docs --- asyncgit/src/graph/chunk.rs | 2 +- asyncgit/src/graph/oids.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs index dd3b637a70..8c70795c21 100644 --- a/asyncgit/src/graph/chunk.rs +++ b/asyncgit/src/graph/chunk.rs @@ -4,7 +4,7 @@ pub enum Markers { Commit, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Chunk { pub alias: Option, pub parent_a: Option, diff --git a/asyncgit/src/graph/oids.rs b/asyncgit/src/graph/oids.rs index 792d99b09f..dca161de88 100644 --- a/asyncgit/src/graph/oids.rs +++ b/asyncgit/src/graph/oids.rs @@ -11,12 +11,12 @@ impl Default for GraphOids { } impl GraphOids { - /// + /// Create an empty alias map. pub fn new() -> Self { Self(HashMap::new()) } - /// + /// Get the alias for `id`, assigning a new one if it doesn't exist yet. pub fn get_or_insert(&mut self, id: &CommitId) -> usize { if let Some(&alias) = self.0.get(id) { return alias; @@ -27,7 +27,7 @@ impl GraphOids { alias } - /// + /// Look up the alias for `id`, returning `None` if not found. pub fn get(&self, id: &CommitId) -> Option { self.0.get(id).copied() } From 2385a66138df7fba7916266d6cde015c9a8eb5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:20 -0400 Subject: [PATCH 38/54] buffer refactor from im vector Swap im::Vector for plain Vec with checkpoint deltas. --- asyncgit/src/graph/buffer.rs | 263 +++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 104 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 4f861f366c..3f2e7a47c7 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -1,7 +1,8 @@ use super::chunk::{Chunk, Markers}; -use im::Vector; use std::collections::BTreeMap; +/// A single mutation of the lane state, recorded while processing one +/// commit. #[derive(Clone, Debug)] pub enum DeltaOp { Insert { index: usize, item: Option }, @@ -9,16 +10,38 @@ pub enum DeltaOp { Replace { index: usize, new: Option }, } +/// All lane-state mutations caused by processing a single commit. +/// Applying a `Delta` to the lane state of row `n` yields the lane +/// state of row `n + 1`. #[derive(Clone, Debug)] pub struct Delta(pub Vec); const CHECKPOINT_INTERVAL: usize = 100; +/// Delta-compressed history of the graph's lane state. +/// +/// While walking the log top-down, every commit mutates the set of +/// active lanes (a `Vec>`, one slot per lane). So, storign a +/// full copy of that state for each commit is a waste. This +/// buffer preserves ONLY the latest state PLUS the list of [`Delta`]s that +/// produced it. +/// Use [`Buffer::decompress`] to get the complete version. pub struct Buffer { - pub current: Vector>, + /// Lane state after the most recently processed commit. + pub current: Vec>, + + /// One [`Delta`] per processed commit, in the order of the walk. pub deltas: Vec, - pub checkpoints: BTreeMap>>, - mergers: Vec, + + /// Full lane-state snapshots taken every `CHECKPOINT_INTERVAL` + /// commits, keyed by delta index, for reducing decompression cost. + pub checkpoints: BTreeMap>>, + + /// Aliases of merge commits whose second parent still needs a new + /// lane. + merge_commits: Vec, + + /// Scratch list of the [`DeltaOp`]s recorded for processing commit pending_delta: Vec, } @@ -29,130 +52,162 @@ impl Default for Buffer { } impl Buffer { - pub fn new() -> Self { + pub const fn new() -> Self { Self { - current: Vector::new(), + current: Vec::new(), deltas: Vec::new(), checkpoints: BTreeMap::new(), - mergers: Vec::new(), + merge_commits: Vec::new(), pending_delta: Vec::new(), } } - pub fn merger(&mut self, alias: usize) { - self.mergers.push(alias); + /// Remember `alias` as a merge commit whose second parent must be + /// given its own lane. + pub fn track_merge_commit(&mut self, alias: usize) { + self.merge_commits.push(alias); } pub fn update(&mut self, new_chunk: &Chunk) { - self.pending_delta.clear(); + // Phase 1: place the new chunk into the lane array. + let placement_index = self.place_chunk(new_chunk); + + // Phase 2: consume the alias in all other live chunks. + if let Some(alias) = new_chunk.alias { + self.consume_alias_in_other_chunks( + alias, + placement_index, + ); + } - let mut empty_lanes: Vec = self - .current - .iter() - .enumerate() - .filter_map(|(i, c)| c.is_none().then_some(i)) - .collect(); + // Phase 3: flush any pending merge commits into new lanes. + self.flush_merge_commits(); - // sort descending so we can pop the lowest index first - empty_lanes.sort_unstable_by(|a, b| b.cmp(a)); + // Phase 4: commit the delta and maybe checkpoint. + self.commit_delta(); + } - let found_idx = if new_chunk.alias.is_some() { - self.current.iter().enumerate().find_map(|(i, c)| { - c.as_ref().and_then(|c| { - (c.parent_a == new_chunk.alias).then_some(i) - }) - }) - } else { - None - }; + fn place_chunk(&mut self, new_chunk: &Chunk) -> usize { + // Prefer a lane whose current occupant is waiting for this chunk as parent_a. + let target = self + .find_lane_awaiting_parent(new_chunk.alias) + .or_else(|| self.first_empty_lane()) + .unwrap_or(self.current.len()); - if let Some(idx) = found_idx { - self.record_replace(idx, Some(new_chunk.clone())); - } else if let Some(empty_idx) = empty_lanes.pop() { - self.record_replace(empty_idx, Some(new_chunk.clone())); + if target < self.current.len() { + self.record_replace(target, Some(new_chunk.clone())); } else { - self.record_insert( - self.current.len(), - Some(new_chunk.clone()), - ); + self.record_insert(target, Some(new_chunk.clone())); } + target + } - let current_length = self.current.len(); - for index in 0..current_length { - if Some(index) == found_idx { - continue; - } - if found_idx.is_none() && index == current_length - 1 { - continue; - } + fn find_lane_awaiting_parent( + &self, + alias: Option, + ) -> Option { + let alias = alias?; + self.current.iter().position(|slot| { + slot.as_ref() + .is_some_and(|chunk| chunk.parent_a == Some(alias)) + }) + } + + fn first_empty_lane(&self) -> Option { + self.current.iter().position(Option::is_none) + } - if let Some(mut c) = self.current[index].clone() { - let changed = new_chunk.alias.is_some_and(|alias| { - let a = c.parent_a == Some(alias); - let b = c.parent_b == Some(alias); - if a { - c.parent_a = None; - } - if b { - c.parent_b = None; - } - a || b - }); - - if changed { - if c.parent_a.is_none() && c.parent_b.is_none() { - self.record_replace(index, None); - } else { - self.record_replace(index, Some(c)); - } + fn consume_alias_in_other_chunks( + &mut self, + alias: usize, + skip_index: usize, + ) { + for index in 0..self.current.len() { + let mut chunk = match self.current[index].clone() { + Some(chunk) if index != skip_index => chunk, + _ => continue, + }; + + let changed_a = chunk.parent_a == Some(alias); + let changed_b = chunk.parent_b == Some(alias); + + if changed_a || changed_b { + if changed_a { + chunk.parent_a = None; + } + if changed_b { + chunk.parent_b = None; } + + self.record_replace( + index, + chunk.parent_a.is_some().then_some(chunk), + ); } } + } - while let Some(alias) = self.mergers.pop() { - if let Some(index) = self.current.iter().position(|c| { - c.as_ref() - .is_some_and(|chunk| chunk.alias == Some(alias)) - }) { - if let Some(mut c) = self.current[index].clone() { - let parent_b = c.parent_b; - c.parent_b = None; - self.record_replace(index, Some(c)); - - let new_lane = Chunk { - alias: None, - parent_a: parent_b, - parent_b: None, - marker: Markers::Commit, - }; - - if let Some(empty_idx) = empty_lanes.pop() { - self.record_replace( - empty_idx, - Some(new_lane), - ); - } else { - self.record_insert( - self.current.len(), - Some(new_lane), - ); - } + fn flush_merge_commits(&mut self) { + // Collect available empty lanes once, before we start maybe filling them. + let mut empty_lanes: Vec = self + .current + .iter() + .enumerate() + .filter_map(|(index, slot)| { + slot.is_none().then_some(index) + }) + .collect(); + + while let Some(alias) = self.merge_commits.pop() { + // Search for an occupied slot that matches the target alias. + // If found, extract its index and a mutable clone of the chunk. + let Some((index, mut chunk)) = + self.current.iter().enumerate().find_map( + |(index, slot)| { + let chunk = slot.as_ref()?; + (chunk.alias == Some(alias)) + .then(|| (index, chunk.clone())) + }, + ) + else { + continue; + }; + + let detached_parent = chunk.parent_b.take(); + self.record_replace(index, Some(chunk)); + + if let Some(parent) = detached_parent { + let new_lane = Chunk { + alias: None, + parent_a: Some(parent), + parent_b: None, + marker: Markers::Commit, + }; + + if let Some(empty_index) = empty_lanes.pop() { + self.record_replace(empty_index, Some(new_lane)); + } else { + self.record_insert( + self.current.len(), + Some(new_lane), + ); } } } + } - while self.current.last().is_some_and(Option::is_none) { - self.record_remove(self.current.len() - 1); + fn commit_delta(&mut self) { + while matches!(self.current.last(), Some(None)) { + let last = self.current.len() - 1; + self.record_remove(last); } - let delta = Delta(self.pending_delta.clone()); - self.deltas.push(delta); + self.deltas + .push(Delta(std::mem::take(&mut self.pending_delta))); - let current_step = self.deltas.len(); - if current_step > 0 && current_step % CHECKPOINT_INTERVAL == 0 - { - self.checkpoints - .insert(current_step - 1, self.current.clone()); + let step = self.deltas.len(); + if step % CHECKPOINT_INTERVAL == 0 { + self.checkpoints.insert(step - 1, self.current.clone()); } } @@ -161,7 +216,7 @@ impl Buffer { index, new: new.clone(), }); - self.current.set(index, new); + self.current[index] = new; } fn record_insert(&mut self, index: usize, item: Option) { @@ -181,10 +236,10 @@ impl Buffer { &self, start: usize, end: usize, - ) -> Vec>> { + ) -> Vec>> { let (current_index, mut state) = self.checkpoints.range(..=start).next_back().map_or_else( - || (None, Vector::new()), + || (None, Vec::new()), |(&i, s)| (Some(i), s.clone()), ); @@ -215,7 +270,7 @@ impl Buffer { } fn apply_delta_to_state( - state: &mut Vector>, + state: &mut Vec>, delta: &Delta, ) { for op in &delta.0 { @@ -227,7 +282,7 @@ impl Buffer { state.remove(*index); } DeltaOp::Replace { index, new } => { - state.set(*index, new.clone()); + state[*index].clone_from(new); } } } From fb3550e2a8fc9740035a768271eb2b310ba60cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:26 -0400 Subject: [PATCH 39/54] walk entry, loop, and overlay graph rendering Very clever use of WalkEntry to avoid a second commit-info pass. overlay_cell will no merge glyphs via bitflags to preserve verticals across bridges. --- Cargo.lock | 866 +++------------------------------ asyncgit/Cargo.toml | 1 - asyncgit/src/graph/walker.rs | 679 ++++++++++++++++++++------ asyncgit/src/revlog.rs | 170 ++++--- asyncgit/src/sync/logwalker.rs | 56 ++- asyncgit/src/sync/mod.rs | 8 +- 6 files changed, 731 insertions(+), 1049 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a65834b4f..85678f3f24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" @@ -159,7 +159,6 @@ dependencies = [ "git2", "git2-hooks", "gix", - "im", "invalidstring", "log", "openssl-sys", @@ -177,15 +176,6 @@ dependencies = [ "url", ] -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -245,15 +235,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -263,12 +244,6 @@ dependencies = [ "bit-vec 0.8.0", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.8.0" @@ -290,15 +265,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -364,12 +330,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - [[package]] name = "byteorder" version = "1.5.0" @@ -436,9 +396,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", @@ -496,9 +456,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -511,14 +471,13 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -527,15 +486,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -611,25 +561,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.10.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.3", - "serde", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -661,16 +592,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - [[package]] name = "ctr" version = "0.9.2" @@ -755,12 +676,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - [[package]] name = "der" version = "0.7.9" @@ -780,28 +695,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - [[package]] name = "diff" version = "0.1.13" @@ -950,9 +843,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -966,9 +859,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -993,25 +886,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set 0.5.3", - "regex", -] - [[package]] name = "fancy-regex" version = "0.16.2" @@ -1055,17 +929,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "filetime" version = "0.2.25" @@ -1086,18 +949,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.1" @@ -1233,7 +1084,6 @@ dependencies = [ "libc", "r-efi 6.0.0", "wasip2", - "wasip3", ] [[package]] @@ -1354,7 +1204,6 @@ dependencies = [ "parking_lot_core", "pretty_assertions", "ratatui", - "ratatui-textarea", "rayon-core", "ron", "scopeguard", @@ -2193,12 +2042,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" @@ -2349,12 +2192,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2382,20 +2219,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "im" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" -dependencies = [ - "bitmaps", - "rand_core", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "imara-diff" version = "0.1.8" @@ -2413,8 +2236,6 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", - "serde", - "serde_core", ] [[package]] @@ -2455,14 +2276,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.44.3" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", "regex", "similar", + "strip-ansi-escapes", + "tempfile", ] [[package]] @@ -2555,24 +2378,14 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "kasuari" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481b4381c813cebeca86bd55c781d21f902f34cf927ec08d6df3dfebcfd2002" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - [[package]] name = "kqueue" version = "1.1.1" @@ -2602,12 +2415,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - [[package]] name = "lazy_static" version = "1.5.0" @@ -2617,12 +2424,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.180" @@ -2686,15 +2487,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line-clipping" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" -dependencies = [ - "bitflags 2.10.0", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2713,12 +2505,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -2736,11 +2522,11 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -2779,27 +2565,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.7" @@ -2821,29 +2586,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "notify" version = "8.2.0" @@ -2908,9 +2650,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -3139,90 +2881,13 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", ] [[package]] @@ -3349,16 +3014,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -3410,12 +3065,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.5" @@ -3445,20 +3094,11 @@ dependencies = [ "getrandom 0.2.15", ] -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core", -] - [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" dependencies = [ "instability", "ratatui-core", @@ -3471,87 +3111,20 @@ dependencies = [ [[package]] name = "ratatui-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "compact_str", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indoc", "itertools", "kasuari", "lru", + "palette", "serde", "strum", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm 0.28.1", - "crossterm 0.29.0", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-termion" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cade85a8591fbc911e147951422f0d6fd40f4948b271b6216c7dc01838996f8" -dependencies = [ - "instability", - "ratatui-core", - "termion", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-textarea" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de236b7cc74b3f7dea227b3fbad97bf459cddf552b6503d888fb9a106eda59ab" -dependencies = [ - "ratatui-core", - "ratatui-crossterm", - "ratatui-widgets", - "unicode-width 0.2.0", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.10.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools", - "line-clipping", - "ratatui-core", - "serde", - "strum", - "time", "unicode-segmentation", "unicode-width 0.2.0", ] @@ -3969,16 +3542,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - [[package]] name = "slab" version = "0.4.9" @@ -4073,6 +3636,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4101,18 +3673,18 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", @@ -4207,90 +3779,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom", - "phf", - "phf_codegen", -] - -[[package]] -name = "termion" -version = "4.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" -dependencies = [ - "libc", - "numtoa", - "serde", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.10.0", - "fancy-regex 0.11.0", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "serde", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -4340,9 +3828,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde_core", "time-core", @@ -4392,9 +3878,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "two-face" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" +checksum = "3d112cfd41c1387546416bcf49c4ae2a1fcacda0d42c9e97120e9798c90c0923" dependencies = [ "serde", "serde_derive", @@ -4413,12 +3899,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uluru" version = "3.1.0" @@ -4478,12 +3958,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" version = "0.5.1" @@ -4523,19 +3997,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "atomic", - "getrandom 0.4.2", - "js-sys", - "serde_core", - "wasm-bindgen", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -4549,12 +4010,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "vtparse" -version = "0.6.2" +name = "vte" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ - "utf8parse", + "memchr", ] [[package]] @@ -4593,34 +4054,23 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", - "rustversion", "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" +name = "wasm-bindgen-backend" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", + "log", "proc-macro2", "quote", "syn 2.0.117", @@ -4628,121 +4078,33 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.10.0", - "hashbrown 0.15.2", - "indexmap", - "semver", -] - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "serde", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "serde", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" +name = "wasm-bindgen-macro" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" +name = "wasm-bindgen-macro-support" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] -name = "wezterm-input-types" -version = "0.1.0" +name = "wasm-bindgen-shared" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "which" @@ -5040,94 +4402,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "write16" version = "1.0.0" diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 4e0a3d110b..861e676152 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -25,7 +25,6 @@ gix = { version = "0.78.0", default-features = false, features = [ "revision", "status", ] } -im = "15" log = "0.4" # git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]} # git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]} diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index 0c0131f66b..d5f7681aad 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -1,16 +1,141 @@ use super::buffer::Buffer; use super::chunk::{Chunk, Markers}; use super::oids::GraphOids; -use super::{ConnectionType, GraphRow, MAX_LANE_COLORS}; -use crate::sync::{CommitId, CommitInfo}; -use im::Vector; +use super::{ + to_lane_idx, ConnectionType, GraphRow, LaneIdx, MAX_LANE_COLORS, +}; +use crate::sync::CommitId; use std::collections::{HashMap, HashSet}; +/// Get the lanes color index, which cycles through the ste palette. +fn lane_color(lane: usize) -> LaneIdx { + to_lane_idx(lane % MAX_LANE_COLORS) +} + +use bitflags::bitflags; + +bitflags! { + /// The neighboring cells a lane joins to. Overlapping lines + /// are merged through this representation rather than + /// a naive overwrite for readability. + #[derive(Clone, Copy, Default)] + struct Dirs: u8 { + const UP = 0b0001; + const DOWN = 0b0010; + const LEFT = 0b0100; + const RIGHT = 0b1000; + } +} + +impl Dirs { + #[allow(clippy::missing_const_for_fn)] + fn merge(self, other: Self) -> Self { + Self::from_bits_retain(self.bits() | other.bits()) + } + + #[allow(clippy::missing_const_for_fn)] + fn vertical(self) -> bool { + self.intersects(Self::UP | Self::DOWN) + } +} + +/// The sub network of an existing connection glyph +/// `None` represents commit markers, which are never drawn over. +fn conn_dirs(conn: ConnectionType) -> Option { + Some(match conn { + ConnectionType::Vertical | ConnectionType::VerticalDotted => { + Dirs::UP | Dirs::DOWN + } + ConnectionType::MergeBridgeMid => Dirs::LEFT | Dirs::RIGHT, + ConnectionType::MergeBridgeStart => Dirs::DOWN | Dirs::LEFT, + ConnectionType::MergeBridgeEnd => Dirs::DOWN | Dirs::RIGHT, + ConnectionType::BranchUp => Dirs::UP | Dirs::LEFT, + ConnectionType::BranchUpRight => Dirs::UP | Dirs::RIGHT, + ConnectionType::TeeLeft => Dirs::UP | Dirs::DOWN | Dirs::LEFT, + ConnectionType::TeeRight => { + Dirs::UP | Dirs::DOWN | Dirs::RIGHT + } + ConnectionType::TeeUp => Dirs::UP | Dirs::LEFT | Dirs::RIGHT, + ConnectionType::TeeDown => { + Dirs::DOWN | Dirs::LEFT | Dirs::RIGHT + } + ConnectionType::CommitNormal + | ConnectionType::CommitBranch + | ConnectionType::CommitMerge + | ConnectionType::CommitStash + | ConnectionType::CommitUncommitted => return None, + }) +} + +/// Determine the glyph for a cell's connectivity. +/// Vertical lines take precedence in crossed cells. +/// Yet the horizontal bridge continues in +/// the spacer columns either side, so we retain wholeness. +const fn dirs_conn(dirs: Dirs, dotted: bool) -> ConnectionType { + let up = dirs.contains(Dirs::UP); + let down = dirs.contains(Dirs::DOWN); + let left = dirs.contains(Dirs::LEFT); + let right = dirs.contains(Dirs::RIGHT); + match (up, down, left, right) { + (true, true, true, false) => ConnectionType::TeeLeft, + (true, true, false, true) => ConnectionType::TeeRight, + (true, false, true, true) => ConnectionType::TeeUp, + (false, true, true, true) => ConnectionType::TeeDown, + (true, false, true, false) => ConnectionType::BranchUp, + (true, false, false, true) => ConnectionType::BranchUpRight, + (false, true, true, false) => { + ConnectionType::MergeBridgeStart + } + (false, true, false, true) => ConnectionType::MergeBridgeEnd, + (false, false, _, _) => ConnectionType::MergeBridgeMid, + (true, true, _, _) + | (true | false, false | true, false, false) => { + if dotted { + ConnectionType::VerticalDotted + } else { + ConnectionType::Vertical + } + } + } +} + +/// Draw `add` into a cell, merging with whatever is already there. +/// The line with a vertical component is the chosen way with color +/// ensuring lanes stay visually continuous +fn overlay_cell( + cell: &mut Option<(ConnectionType, LaneIdx)>, + add: Dirs, + color: LaneIdx, +) { + if let Some((conn, existing_color)) = cell { + if let Some(existing) = conn_dirs(*conn) { + let is_dotted = + matches!(conn, ConnectionType::VerticalDotted); + + let resolved_color = + if existing.vertical() || !add.vertical() { + *existing_color + } else { + color + }; + + *cell = Some(( + dirs_conn(existing.merge(add), is_dotted), + resolved_color, + )); + } + } else { + *cell = Some((dirs_conn(add, false), color)); + } +} + pub struct GraphWalker { pub buffer: Buffer, pub oids: GraphOids, pub branch_lane_map: HashMap, - pub mergers_map: HashMap, + + /// Maps a merge commit's alias to the alias of its second parent. + pub merge_parents: HashMap, } impl Default for GraphWalker { @@ -25,54 +150,45 @@ impl GraphWalker { buffer: Buffer::new(), oids: GraphOids::new(), branch_lane_map: HashMap::new(), - mergers_map: HashMap::new(), + merge_parents: HashMap::new(), } } - pub fn process(&mut self, commit: &CommitInfo) { - let alias = self.oids.get_or_insert(&commit.id); - let parent_a = commit - .parents - .first() - .map(|p| self.oids.get_or_insert(p)); - let parent_b = - commit.parents.get(1).map(|p| self.oids.get_or_insert(p)); + pub fn process(&mut self, id: CommitId, parents: &[CommitId]) { + let alias = self.oids.get_or_insert(&id); + + let mut mapped_parents = + parents.iter().map(|p| self.oids.get_or_insert(p)); + + // We make the executive and saddening decision to not support octo/mega merges + // TUIs are simply a backwards medium for representing this complexity + let first_parent = mapped_parents.next(); + let second_parent = mapped_parents.next(); let chunk = Chunk { alias: Some(alias), - parent_a, - parent_b, + parent_a: first_parent, + parent_b: second_parent, marker: Markers::Commit, }; - if let Some(b) = parent_b { - self.mergers_map.insert(alias, b); - } + second_parent.map(|b| self.merge_parents.insert(alias, b)); - if parent_a.is_some() && parent_b.is_some() { - let already_tracked = - self.buffer.current.iter().any(|c| { - c.as_ref().is_some_and(|c| { - c.parent_a == parent_b && c.parent_b.is_none() - }) - }); - if !already_tracked { - self.buffer.merger(alias); - } + if first_parent.is_some() + && second_parent.is_some() + && !self.buffer.current.iter().flatten().any(|commit| { + commit.parent_a == second_parent + && commit.parent_b.is_none() + }) { + self.buffer.track_merge_commit(alias); } self.buffer.update(&chunk); } - pub fn snapshot_at( - &self, - global_idx: usize, - ) -> Vector> { - self.buffer - .decompress(global_idx, global_idx) - .into_iter() - .next() - .unwrap_or_default() + /// Number of commits already folded into the graph buffer. + pub fn processed_commits(&self) -> usize { + self.buffer.deltas.len() } pub fn compute_rows( @@ -83,27 +199,34 @@ impl GraphWalker { stashes: &HashSet, head_id: Option<&CommitId>, ) -> Vec { - let end = global_start + commit_range.len().saturating_sub(1); - let snapshots = self.buffer.decompress(global_start, end); + if commit_range.is_empty() { + return Vec::new(); + } + + // decompress one row before the range (when there is one) so + // every row's predecessor state comes from the same replay + let snap_start = global_start.saturating_sub(1); + let end = global_start + commit_range.len() - 1; + let snapshots = self.buffer.decompress(snap_start, end); + let offset = global_start - snap_start; commit_range .iter() .enumerate() .map(|(index, commit_id)| { - let curr = - snapshots.get(index).cloned().unwrap_or_default(); - let prev = if index > 0 { - snapshots.get(index - 1).cloned() - } else if global_start > 0 { - Some(self.snapshot_at(global_start - 1)) - } else { - None - }; + let place = index + offset; + + let current: &[Option] = + snapshots.get(place).map_or(&[], Vec::as_slice); + let previous = place + .checked_sub(1) + .and_then(|i| snapshots.get(i)) + .map(Vec::as_slice); self.render_row( commit_id, - &curr, - prev.as_ref(), + current, + previous, branch_tips, stashes, head_id, @@ -112,17 +235,108 @@ impl GraphWalker { .collect() } + fn draw_merge_bridge( + lanes: &mut [Option<(ConnectionType, LaneIdx)>], + merge_bridge: Option<(usize, usize)>, + commit_lane: usize, + current: &[Option], + previous: Option<&[Option]>, + ) { + let Some((from, to)) = merge_bridge.filter(|(f, t)| f != t) + else { + return; + }; + let target_lane = if from == commit_lane { to } else { from }; + + // only draw the corner continuing upward when the target + // lane already existed on the previous row; a brand-new + // lane starts at this corner + let continues_up = current[target_lane].is_some() + && previous.is_some_and(|prev| { + prev.get(target_lane) == Some(¤t[target_lane]) + }); + + // replace the plain vertical fill_lanes drew for the + // target lane with the precise corner/junction + lanes[target_lane] = None; + let target_dirs = { + let mut d = Dirs::DOWN; + if continues_up { + d |= Dirs::UP; + } + if target_lane > commit_lane { + d |= Dirs::LEFT; + } + if target_lane < commit_lane { + d |= Dirs::RIGHT; + } + d + }; + overlay_cell( + &mut lanes[target_lane], + target_dirs, + lane_color(target_lane), + ); + + Self::draw_bridge_span( + lanes, + from, + to, + lane_color(target_lane), + ); + } + + fn draw_branching_lanes( + lanes: &mut Vec>, + branching_lanes: &[usize], + commit_lane: usize, + ) -> Vec<(LaneIdx, LaneIdx)> { + let mut branches = Vec::new(); + for &branch_lane in branching_lanes { + let from = std::cmp::min(branch_lane, commit_lane); + let to = std::cmp::max(branch_lane, commit_lane); + branches.push((to_lane_idx(from), to_lane_idx(to))); + + if lanes.len() <= to { + lanes.resize(to + 1, None); + } + + Self::draw_bridge_span( + lanes, + from, + to, + lane_color(branch_lane), + ); + let branch_dirs = { + let mut d = Dirs::UP; + if branch_lane == to { + d |= Dirs::LEFT; + } + if branch_lane == from { + d |= Dirs::RIGHT; + } + d + }; + overlay_cell( + &mut lanes[branch_lane], + branch_dirs, + lane_color(branch_lane), + ); + } + branches + } + fn render_row( &self, commit_id: &CommitId, - curr: &Vector>, - prev: Option<&Vector>>, + current: &[Option], + previous: Option<&[Option]>, branch_tips: &HashSet, stashes: &HashSet, head_id: Option<&CommitId>, ) -> GraphRow { let alias = self.oids.get(commit_id); - let commit_lane = curr + let commit_lane = current .iter() .position(|c| { c.as_ref().is_some_and(|chunk| { @@ -132,99 +346,71 @@ impl GraphWalker { .unwrap_or(0); let parent_b_alias = - alias.and_then(|a| self.mergers_map.get(&a).copied()); + alias.and_then(|a| self.merge_parents.get(&a).copied()); let is_merge = parent_b_alias.is_some(); let is_branch_tip = branch_tips.contains(commit_id); let is_stash = stashes.contains(commit_id); - let branching_lanes: Vec = prev + let branching_lanes: Vec = previous .into_iter() .flatten() // Unwrapping the optional, returning empty vec when None .enumerate() .filter(|(i, pc)| { pc.is_some() - && curr.get(*i).is_none_or(Option::is_none) + && current.get(*i).is_none_or(Option::is_none) }) .map(|(i, _)| i) .collect(); - let mut lanes = vec![None; curr.len()]; + let mut lanes = vec![None; current.len()]; - let merge_bridge = is_merge - .then(|| { - let target_lane = curr.iter().position(|c| { + let merge_bridge = if is_merge && parent_b_alias.is_some() { + current + .iter() + .position(|c| { c.as_ref().is_some_and(|chunk| { - parent_b_alias.is_some() - && chunk.parent_a == parent_b_alias + chunk.parent_a == parent_b_alias }) - }); - target_lane.map(|t| { - if t > commit_lane { - (commit_lane, t) - } else { - (t, commit_lane) - } }) - }) - .flatten(); + .map(|t| (commit_lane.min(t), commit_lane.max(t))) + } else { + None + }; self.fill_lanes( &mut lanes, - curr, + current, alias, head_id, is_stash, is_merge, is_branch_tip, - &branching_lanes, ); - if let Some((from, to)) = merge_bridge { - let target_lane = - if from == commit_lane { to } else { from }; - Self::draw_bridge( - &mut lanes, - from, - to, - commit_lane, - target_lane, - ConnectionType::MergeBridgeMid, - ConnectionType::MergeBridgeStart, - ConnectionType::MergeBridgeEnd, - ); - } - - let mut branches = Vec::new(); - for &branch_lane in &branching_lanes { - let from = std::cmp::min(branch_lane, commit_lane); - let to = std::cmp::max(branch_lane, commit_lane); - branches.push((from, to)); - - if lanes.len() <= to { - lanes.resize(to + 1, None); - } + Self::draw_merge_bridge( + &mut lanes, + merge_bridge, + commit_lane, + current, + previous, + ); - Self::draw_bridge( - &mut lanes, - from, - to, - branch_lane, - branch_lane, - ConnectionType::MergeBridgeMid, - ConnectionType::BranchUp, - ConnectionType::BranchUpRight, - ); - } + let branches = Self::draw_branching_lanes( + &mut lanes, + &branching_lanes, + commit_lane, + ); GraphRow { - lane_count: curr.iter().flatten().count(), - commit_lane, + lane_count: to_lane_idx(current.iter().flatten().count()), + commit_lane: to_lane_idx(commit_lane), is_merge, is_branch_tip, is_stash, lanes, - merge_bridge, + merge_bridge: merge_bridge + .map(|(f, t)| (to_lane_idx(f), to_lane_idx(t))), branches, } } @@ -232,21 +418,16 @@ impl GraphWalker { #[allow(clippy::too_many_arguments)] fn fill_lanes( &self, - lanes: &mut [Option<(ConnectionType, usize)>], - curr: &Vector>, + lanes: &mut [Option<(ConnectionType, LaneIdx)>], + curr: &[Option], alias: Option, head_id: Option<&CommitId>, is_stash: bool, is_merge: bool, is_branch_tip: bool, - branching_lanes: &[usize], ) { for (lane_idx, chunk_item) in curr.iter().enumerate() { let Some(chunk) = chunk_item.as_ref() else { - if branching_lanes.contains(&lane_idx) { - lanes[lane_idx] = - Some((ConnectionType::BranchUp, lane_idx % MAX_LANE_COLORS)); - } continue; }; @@ -259,14 +440,16 @@ impl GraphWalker { _ => ConnectionType::CommitNormal, }; - lanes[lane_idx] = Some((conn_type, lane_idx % MAX_LANE_COLORS)); + lanes[lane_idx] = + Some((conn_type, lane_color(lane_idx))); } else { - let is_dotted = head_id - .and_then(|h| self.oids.get(h)) - .is_some_and(|ha| { - chunk.parent_a == Some(ha) - || chunk.parent_b == Some(ha) - }) && lane_idx == 0; + let target_oid = + head_id.and_then(|h| self.oids.get(h)); + + let is_dotted = lane_idx == 0 + && target_oid.is_some() + && (chunk.parent_a == target_oid + || chunk.parent_b == target_oid); let is_orphan = chunk.parent_a.is_none() && chunk.parent_b.is_none(); @@ -281,47 +464,231 @@ impl GraphWalker { ConnectionType::Vertical }; - lanes[lane_idx] = Some((conn, lane_idx % MAX_LANE_COLORS)); + lanes[lane_idx] = Some((conn, lane_color(lane_idx))); } } } - fn draw_bridge( - lanes: &mut [Option<(ConnectionType, usize)>], + /// Lay the horizontal run of a bridge over the lanes strictly + /// between its two ends, merging with whatever each cell already + /// shows. + fn draw_bridge_span( + lanes: &mut [Option<(ConnectionType, LaneIdx)>], from: usize, to: usize, - color_lane: usize, - corner_lane: usize, - mid: ConnectionType, - corner_right: ConnectionType, - corner_left: ConnectionType, + color: LaneIdx, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { - match lane { - Some(( - ConnectionType::Vertical | ConnectionType::VerticalDotted, - _, - )) => { - *lane = Some((ConnectionType::Cross, color_lane % MAX_LANE_COLORS)); - } - _ => { - *lane = Some((mid, color_lane % MAX_LANE_COLORS)); - } - } + overlay_cell(lane, Dirs::LEFT | Dirs::RIGHT, color); } + } +} - if corner_lane == to { - let new_corner = match (lanes[to], corner_right) { - (Some((ConnectionType::MergeBridgeStart, _)), ConnectionType::BranchUp) => ConnectionType::BranchUpMergeStart, - _ => corner_right, - }; - lanes[to] = Some((new_corner, color_lane % MAX_LANE_COLORS)); - } else if corner_lane == from { - let new_corner = match (lanes[from], corner_left) { - (Some((ConnectionType::MergeBridgeEnd, _)), ConnectionType::BranchUpRight) => ConnectionType::BranchUpRightMergeEnd, - _ => corner_left, - }; - lanes[from] = Some((new_corner, color_lane % MAX_LANE_COLORS)); +#[cfg(test)] +mod tests { + use super::*; + + fn id(n: usize) -> CommitId { + CommitId::from_str_unchecked(&format!("{n:040x}")) + .expect("valid oid") + } + + fn sym(conn: ConnectionType) -> char { + match conn { + ConnectionType::Vertical => '┃', + ConnectionType::VerticalDotted => '╏', + ConnectionType::CommitNormal => 'o', + ConnectionType::CommitBranch => '*', + ConnectionType::CommitMerge => 'M', + ConnectionType::CommitStash => '*', + ConnectionType::CommitUncommitted => '+', + ConnectionType::MergeBridgeStart => '┓', + ConnectionType::MergeBridgeMid => '━', + ConnectionType::MergeBridgeEnd => '┏', + ConnectionType::BranchUp => '┛', + ConnectionType::BranchUpRight => '┗', + ConnectionType::TeeLeft => '┫', + ConnectionType::TeeRight => '┣', + ConnectionType::TeeUp => '┻', + ConnectionType::TeeDown => '┳', + } + } + + /// Render a row the way the UI does: one glyph per lane plus a + /// spacer that carries a bridge's horizontal run. + fn row_to_string(row: &GraphRow) -> String { + let mut out = String::new(); + for (lane_index, conn) in row.lanes.iter().enumerate() { + out.push(conn.map_or(' ', |(c, _)| sym(c))); + + let in_bridge = row + .merge_bridge + .into_iter() + .chain(row.branches.iter().copied()) + .any(|(from, to)| { + lane_index >= usize::from(from) + && lane_index < usize::from(to) + }); + out.push(if in_bridge { '━' } else { ' ' }); + } + out.trim_end().to_string() + } + + /// Walk `history` (newest first, `(commit, parents)`) and render + /// every row. + fn render(history: &[(usize, &[usize])]) -> Vec { + let mut walker = GraphWalker::new(); + let ids: Vec = + history.iter().map(|(c, _)| id(*c)).collect(); + + for (commit, parents) in history { + let parents: Vec = + parents.iter().map(|p| id(*p)).collect(); + walker.process(id(*commit), &parents); + } + + walker + .compute_rows( + &ids, + 0, + &HashSet::new(), + &HashSet::new(), + None, + ) + .iter() + .map(row_to_string) + .collect() + } + + #[test] + fn linear_history() { + let rows = render(&[(1, &[2]), (2, &[3]), (3, &[])]); + assert_eq!(rows, vec!["o", "o", "o"]); + } + + #[test] + fn simple_merge() { + // 1 merges 3 into the line 1 → 2 → 4, 3 → 4 + let rows = + render(&[(1, &[2, 3]), (2, &[4]), (3, &[4]), (4, &[])]); + assert_eq!(rows, vec!["M━┓", "o ┃", "┃ o", "o━┛"]); + } + + #[test] + fn merge_into_tracked_lane_continues_through_corner() { + // 3's merge line joins lane 1 which keeps flowing to 5, + // so the corner must be a junction (┫), not a dead end (┓) + let rows = render(&[ + (1, &[3]), + (2, &[5]), + (3, &[4, 5]), + (4, &[6]), + (5, &[6]), + (6, &[]), + ]); + assert_eq!( + rows, + vec!["o", "┃ o", "M━┫", "o ┃", "┃ o", "o━┛"] + ); + } + + #[test] + fn merge_bridge_crosses_unrelated_lane() { + // 3 (lane 2) merges into 1's line (lane 0) while 2's line + // (lane 1) passes through: the crossed lane keeps its + // vertical instead of being cut by the bridge + let rows = render(&[ + (1, &[4]), + (2, &[5]), + (3, &[6, 4]), + (4, &[7]), + (5, &[7]), + (6, &[7]), + (7, &[]), + ]); + assert_eq!( + rows, + vec![ + "o", + "┃ o", + "┣━┃━M", + "o ┃ ┃", + "┃ o ┃", + "┃ ┃ o", + "o━┻━┛", + ] + ); + } + + #[test] + fn overlapping_branch_bridges_keep_inner_corner() { + // lanes 1 and 2 both close into the commit on lane 0; the + // outer bridge passes through the inner corner (┻) instead + // of erasing it + let rows = + render(&[(1, &[4]), (2, &[4]), (3, &[4]), (4, &[])]); + assert_eq!(rows, vec!["o", "┃ o", "┃ ┃ o", "o━┻━┛"]); + } + + #[test] + fn merge_and_branch_bridges_overlap() { + // commit 3 closes a branch from lane 2 while opening a merge + // to lane 3, crossing lane 1: every line stays continuous + let rows = render(&[ + (1, &[3, 4]), + (2, &[3]), + (3, &[5, 6]), + (4, &[5]), + (5, &[7]), + (6, &[7]), + (7, &[]), + ]); + assert_eq!( + rows, + vec![ + "M━┓", + "┃ ┃ o", + "M━┃━┻━┓", + "┃ o ┃", + "o━┛ ┃", + "┃ o", + "o━━━━━┛", + ] + ); + } + + #[test] + fn crossed_lane_keeps_own_color() { + let rows = &[ + (1usize, &[4usize][..]), + (2, &[5]), + (3, &[6, 4]), + (4, &[7]), + (5, &[7]), + (6, &[7]), + (7, &[]), + ]; + let mut walker = GraphWalker::new(); + let ids: Vec = + rows.iter().map(|(c, _)| id(*c)).collect(); + for (commit, parents) in rows { + let parents: Vec = + parents.iter().map(|p| id(*p)).collect(); + walker.process(id(*commit), &parents); } + let computed = walker.compute_rows( + &ids, + 0, + &HashSet::new(), + &HashSet::new(), + None, + ); + + // row of commit 3: lane 1 is crossed by the merge bridge but + // keeps both its vertical glyph and its own lane color + let crossed = computed[2].lanes[1] + .expect("crossed lane should not be empty"); + assert_eq!(crossed.0, ConnectionType::Vertical); + assert_eq!(crossed.1, lane_color(1)); } } diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index 607a2babf1..a70c39fd45 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -2,8 +2,8 @@ use crate::{ error::Result, graph::{GraphRow, GraphWalker}, sync::{ - get_commits_info, gix_repo, repo, CommitId, LogWalker, - LogWalkerWithoutFilter, RepoPath, SharedCommitFilterFn, + gix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter, + RepoPath, SharedCommitFilterFn, WalkEntry, }, AsyncGitNotification, Error, }; @@ -37,7 +37,7 @@ pub struct AsyncLogResult { /// pub duration: Duration, } -/// +/// Drives the background commit-log walker and exposes graph rows. pub struct AsyncLog { current: Arc>, current_head: Arc>>, @@ -47,6 +47,9 @@ pub struct AsyncLog { filter: Option, partial_extract: AtomicBool, repo: RepoPath, + /// All walk entries collected by the background thread, in walk order. + /// The graph walker reads these lazily, only as far as the viewport requires. + walk_entries: Arc>>, graph_walker: Arc>, } @@ -73,10 +76,18 @@ impl AsyncLog { background: Arc::new(AtomicBool::new(false)), filter, partial_extract: AtomicBool::new(false), + walk_entries: Arc::new(Mutex::new(Vec::new())), graph_walker: Arc::new(Mutex::new(GraphWalker::new())), } } + /// Computes graph rows for `commit_slice` starting at `global_start`. + /// + /// Driven lazily. Processes only as many + /// [`WalkEntry`]s as the viewport requires. + /// + /// Returns `None` when the background walk hasn't reached + /// `global_start + commit_slice.len()` yet. pub fn get_graph_rows( &self, commit_slice: &[CommitId], @@ -85,13 +96,27 @@ impl AsyncLog { stashes: &HashSet, head_id: Option<&CommitId>, ) -> Option> { - let walker_guard = self.graph_walker.lock().ok()?; let needed_end = global_start + commit_slice.len(); - if walker_guard.buffer.deltas.len() < needed_end { - return None; + + let mut walker = self.graph_walker.lock().ok()?; + + { + let entries = self.walk_entries.lock().ok()?; + if entries.len() < needed_end { + return None; + } + + // the walker may already be ahead of the requested range + // (you know, scrolling up), so only feed it entries + // we know it is yet to have seen + let processed = + walker.processed_commits().min(needed_end); + for entry in &entries[processed..needed_end] { + walker.process(entry.id, &entry.parents); + } } - Some(walker_guard.compute_rows( + Some(walker.compute_rows( commit_slice, global_start, branch_tips, @@ -192,7 +217,7 @@ impl AsyncLog { let sender = self.sender.clone(); let arc_pending = Arc::clone(&self.pending); let arc_background = Arc::clone(&self.background); - let arc_graph_walker = Arc::clone(&self.graph_walker); + let arc_walk_entries = Arc::clone(&self.walk_entries); let filter = self.filter.clone(); let repo_path = self.repo.clone(); @@ -209,7 +234,7 @@ impl AsyncLog { &arc_current, &arc_background, &sender, - &arc_graph_walker, + &arc_walk_entries, filter, ) .expect("failed to fetch"); @@ -227,7 +252,7 @@ impl AsyncLog { arc_current: &Arc>, arc_background: &Arc, sender: &Sender, - arc_graph_walker: &Arc>, + arc_walk_entries: &Arc>>, filter: Option, ) -> Result<()> { filter.map_or_else( @@ -237,7 +262,7 @@ impl AsyncLog { arc_current, arc_background, sender, - arc_graph_walker, + arc_walk_entries, ) }, |filter| { @@ -246,107 +271,94 @@ impl AsyncLog { arc_current, arc_background, sender, - arc_graph_walker, filter, ) }, ) } + /// A filtered walk yields a disconnected subset of the history, + /// which the graph cannot represent, so no topology entries are + /// collected here. fn fetch_helper_with_filter( repo_path: &RepoPath, arc_current: &Arc>, arc_background: &Arc, sender: &Sender, - arc_graph_walker: &Arc>, filter: SharedCommitFilterFn, ) -> Result<()> { - let start_time = Instant::now(); - - let mut entries = vec![CommitId::default(); LIMIT_COUNT]; - entries.resize(0, CommitId::default()); - let r = repo(repo_path)?; let mut walker = LogWalker::new(&r, LIMIT_COUNT)?.filter(Some(filter)); - loop { - entries.clear(); - let read = walker.read(&mut entries)?; + Self::walk_loop( + |out| walker.read(out), + arc_current, + arc_background, + sender, + None, + )?; - { - let infos = get_commits_info( - repo_path, - &entries[0..read], - 1000, - )?; - let mut gw = arc_graph_walker.lock()?; - for info in &infos { - gw.process(info); - } - } - - let mut current = arc_current.lock()?; - current.commits.extend(entries.iter()); - current.duration = start_time.elapsed(); + log::trace!("revlog visited: {}", walker.visited()); - if read == 0 { - break; - } - Self::notify(sender); + Ok(()) + } - let sleep_duration = - if arc_background.load(Ordering::Relaxed) { - SLEEP_BACKGROUND - } else { - SLEEP_FOREGROUND - }; + fn fetch_helper_without_filter( + repo_path: &RepoPath, + arc_current: &Arc>, + arc_background: &Arc, + sender: &Sender, + arc_walk_entries: &Arc>>, + ) -> Result<()> { + let mut repo: gix::Repository = gix_repo(repo_path)?; + let mut walker = + LogWalkerWithoutFilter::new(&mut repo, LIMIT_COUNT)?; - thread::sleep(sleep_duration); - } + Self::walk_loop( + |out| walker.read(out), + arc_current, + arc_background, + sender, + Some(arc_walk_entries), + )?; log::trace!("revlog visited: {}", walker.visited()); Ok(()) } - fn fetch_helper_without_filter( - repo_path: &RepoPath, + /// Drives `read` in batches, publishing every batch's commit ids + /// to `arc_current` and (when given) moving the full entries into + /// `walk_entries` for the graph. + fn walk_loop( + mut read: impl FnMut(&mut Vec) -> Result, arc_current: &Arc>, arc_background: &Arc, sender: &Sender, - arc_graph_walker: &Arc>, + walk_entries: Option<&Mutex>>, ) -> Result<()> { let start_time = Instant::now(); - let mut entries = vec![CommitId::default(); LIMIT_COUNT]; - entries.resize(0, CommitId::default()); - - let mut repo: gix::Repository = gix_repo(repo_path)?; - let mut walker = - LogWalkerWithoutFilter::new(&mut repo, LIMIT_COUNT)?; + let mut entries: Vec = + Vec::with_capacity(LIMIT_COUNT); loop { - entries.clear(); - let read = walker.read(&mut entries)?; + let read_count = read(&mut entries)?; { - let infos = get_commits_info( - repo_path, - &entries[0..read], - 1000, - )?; - let mut gw = arc_graph_walker.lock()?; - for info in &infos { - gw.process(info); - } + let mut current = arc_current.lock()?; + current.commits.extend(entries.iter().map(|e| e.id)); + current.duration = start_time.elapsed(); } - let mut current = arc_current.lock()?; - current.commits.extend(entries.iter()); - current.duration = start_time.elapsed(); + if let Some(walk_entries) = walk_entries { + walk_entries.lock()?.append(&mut entries); + } else { + entries.clear(); + } - if read == 0 { + if read_count == 0 { break; } Self::notify(sender); @@ -361,8 +373,6 @@ impl AsyncLog { thread::sleep(sleep_duration); } - log::trace!("revlog visited: {}", walker.visited()); - Ok(()) } @@ -370,6 +380,7 @@ impl AsyncLog { self.current.lock()?.commits.clear(); *self.current_head.lock()? = None; self.partial_extract.store(false, Ordering::Relaxed); + self.walk_entries.lock()?.clear(); *self.graph_walker.lock()? = GraphWalker::new(); Ok(()) } @@ -391,7 +402,6 @@ mod tests { use serial_test::serial; use tempfile::TempDir; - use crate::graph::GraphWalker; use crate::sync::tests::{debug_cmd_print, repo_init}; use crate::sync::RepoPath; use crate::AsyncLog; @@ -419,15 +429,14 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); - let arc_graph_walker = - Arc::new(Mutex::new(GraphWalker::new())); + let arc_walk_entries = Arc::new(Mutex::new(Vec::new())); let result = AsyncLog::fetch_helper_without_filter( &subdir_path, &arc_current, &arc_background, &tx_git, - &arc_graph_walker, + &arc_walk_entries, ); assert_eq!(result.unwrap(), ()); @@ -450,8 +459,7 @@ mod tests { duration: Duration::default(), })); let arc_background = Arc::new(AtomicBool::new(false)); - let arc_graph_walker = - Arc::new(Mutex::new(GraphWalker::new())); + let arc_walk_entries = Arc::new(Mutex::new(Vec::new())); std::env::set_var("GIT_DIR", git_dir); @@ -461,7 +469,7 @@ mod tests { &arc_current, &arc_background, &tx_git, - &arc_graph_walker, + &arc_walk_entries, ); std::env::remove_var("GIT_DIR"); diff --git a/asyncgit/src/sync/logwalker.rs b/asyncgit/src/sync/logwalker.rs index 81a6fd321e..29843271f6 100644 --- a/asyncgit/src/sync/logwalker.rs +++ b/asyncgit/src/sync/logwalker.rs @@ -2,11 +2,25 @@ use super::{CommitId, SharedCommitFilterFn}; use crate::error::Result; use git2::{Commit, Oid, Repository}; use gix::revision::Walk; +use smallvec::SmallVec; use std::{ cmp::Ordering, collections::{BinaryHeap, HashSet}, }; +/// A commit id together with the ids of its TWO parents. +/// +/// The parents come for free during a walk. +/// Collecting here avoid a second, convulted pass. +#[derive(Debug, Clone)] +pub struct WalkEntry { + /// The commit's own unique identifier. + pub id: CommitId, + + /// The commit's parent identifiers. + pub parents: SmallVec<[CommitId; 2]>, +} + struct TimeOrderedCommit<'a>(Commit<'a>); impl Eq for TimeOrderedCommit<'_> {} @@ -70,11 +84,18 @@ impl<'a> LogWalker<'a> { } /// - pub fn read(&mut self, out: &mut Vec) -> Result { + pub fn read( + &mut self, + out: &mut Vec, + ) -> Result { let mut count = 0_usize; while let Some(c) = self.commits.pop() { + let mut parents = SmallVec::new(); for p in c.0.parents() { + if parents.len() < 2 { + parents.push(p.id().into()); + } self.visit(p); } @@ -87,7 +108,7 @@ impl<'a> LogWalker<'a> { }; if commit_should_be_included { - out.push(id); + out.push(WalkEntry { id, parents }); } count += 1; @@ -157,11 +178,22 @@ impl<'a> LogWalkerWithoutFilter<'a> { } /// - pub fn read(&mut self, out: &mut Vec) -> Result { + pub fn read( + &mut self, + out: &mut Vec, + ) -> Result { let mut count = 0_usize; while let Some(Ok(info)) = self.walk.next() { - out.push(info.id.into()); + out.push(WalkEntry { + id: info.id.into(), + parents: info + .parent_ids + .iter() + .take(2) + .map(|id| CommitId::from(*id)) + .collect(), + }); count += 1; @@ -214,7 +246,7 @@ mod tests { walk.read(&mut items).unwrap(); assert_eq!(items.len(), 1); - assert_eq!(items[0], oid2); + assert_eq!(items[0].id, oid2); Ok(()) } @@ -238,11 +270,12 @@ mod tests { let mut walk = LogWalker::new(&repo, 100)?; walk.read(&mut items).unwrap(); - let info = get_commits_info(repo_path, &items, 50).unwrap(); + let ids: Vec = items.iter().map(|e| e.id).collect(); + let info = get_commits_info(repo_path, &ids, 50).unwrap(); dbg!(&info); assert_eq!(items.len(), 2); - assert_eq!(items[0], oid2); + assert_eq!(items[0].id, oid2); let mut items = Vec::new(); walk.read(&mut items).unwrap(); @@ -272,11 +305,12 @@ mod tests { let mut items = Vec::new(); assert!(matches!(walk.read(&mut items), Ok(2))); - let info = get_commits_info(repo_path, &items, 50).unwrap(); + let ids: Vec = items.iter().map(|e| e.id).collect(); + let info = get_commits_info(repo_path, &ids, 50).unwrap(); dbg!(&info); assert_eq!(items.len(), 2); - assert_eq!(items[0], oid2); + assert_eq!(items[0].id, oid2); let mut items = Vec::new(); assert!(matches!(walk.read(&mut items), Ok(0))); @@ -318,7 +352,7 @@ mod tests { walker.read(&mut items).unwrap(); assert_eq!(items.len(), 1); - assert_eq!(items[0], second_commit_id); + assert_eq!(items[0].id, second_commit_id); let mut items = Vec::new(); walker.read(&mut items).unwrap(); @@ -365,7 +399,7 @@ mod tests { walker.read(&mut items).unwrap(); assert_eq!(items.len(), 1); - assert_eq!(items[0], second_commit_id); + assert_eq!(items[0].id, second_commit_id); let log_filter = filter_commit_by_search( LogFilterSearch::new(LogFilterSearchOptions { diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 2a5f413e8f..38818b5fc7 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -72,7 +72,7 @@ pub use hooks::{ }; pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; -pub use logwalker::{LogWalker, LogWalkerWithoutFilter}; +pub use logwalker::{LogWalker, LogWalkerWithoutFilter, WalkEntry}; pub use merge::{ abort_pending_rebase, abort_pending_state, continue_pending_rebase, merge_branch, merge_commit, merge_msg, @@ -261,13 +261,13 @@ pub mod tests { r: &Repository, max_count: usize, ) -> Vec { - let mut commit_ids = Vec::::new(); + let mut entries = Vec::new(); LogWalker::new(r, max_count) .unwrap() - .read(&mut commit_ids) + .read(&mut entries) .unwrap(); - commit_ids + entries.iter().map(|e| e.id).collect() } /// Same as `repo_init`, but the repo is a bare repo (--bare) From fb507246f339161a2a41c9c59b1af629e94e9901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:30 -0400 Subject: [PATCH 40/54] lane idx and tee connection types cutting down our memory usage from 32+ to EIGHT! also tee connection types --- asyncgit/src/graph/mod.rs | 41 +++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index e1c8bee521..d5687ea829 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -8,34 +8,55 @@ pub use walker::GraphWalker; /// The maximum number of colors to use for graph lanes pub const MAX_LANE_COLORS: usize = 16; -/// The type of connection between nodes in the graph +// Yes, there are repositories where this is exceeded +// Are they very rare? Yes. +// On most terminals can more than 256 lanes even be represneted usefully? Not really. +pub type LaneIdx = u8; + +/// Convert a lane position into the compact [`LaneIdx`] +/// representation. This way we can keep full granularity when computing, +/// but not when storing. +pub(crate) fn to_lane_idx(lane: usize) -> LaneIdx { + LaneIdx::try_from(lane).unwrap_or(LaneIdx::MAX) +} + +/// The type of connection between nodes in the graph. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ConnectionType { Vertical, VerticalDotted, - Cross, CommitNormal, CommitBranch, CommitMerge, CommitStash, CommitUncommitted, + /// a bridge turning down into a lane that starts here MergeBridgeStart, + /// a bridge passing over an empty lane slot MergeBridgeMid, + /// a bridge turning down, lane starting to its right MergeBridgeEnd, - BranchDown, + /// a lane from above turning left into a commit BranchUp, + /// a lane from above turning right into a commit BranchUpRight, - BranchUpMergeStart, - BranchUpRightMergeEnd, + /// a continuing lane absorbing a bridge from its left + TeeLeft, + /// a continuing lane absorbing a bridge from its right + TeeRight, + /// a lane ending from above while a bridge passes through + TeeUp, + /// a lane starting downward while a bridge passes through + TeeDown, } #[derive(Clone, Debug, Default)] pub struct GraphRow { /// Number of active lanes at this commit row - pub lane_count: usize, + pub lane_count: LaneIdx, /// Which lane index this commit sits on - pub commit_lane: usize, + pub commit_lane: LaneIdx, /// Whether this is a merge commit (two parents) pub is_merge: bool, @@ -49,13 +70,13 @@ pub struct GraphRow { /// Connections emitted per lane: /// None = empty space /// Some((ConnectionType, `color_index`)) = draw this connector in this color - pub lanes: Vec>, + pub lanes: Vec>, /// Horizontal merge bridge: if this commit merges rightward, /// (`from_lane`, `to_lane`) — the span to draw ─ ╭ ╮ across - pub merge_bridge: Option<(usize, usize)>, + pub merge_bridge: Option<(LaneIdx, LaneIdx)>, /// Horizontal branch bridges: if this commit spawns branches, /// spans to draw ─ ╭ ╮ across - pub branches: Vec<(usize, usize)>, + pub branches: Vec<(LaneIdx, LaneIdx)>, } From 8f8217e7417d1e128f215c2e60c3e6b1bcb9151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:33 -0400 Subject: [PATCH 41/54] graph symbols and lane count with lane idx --- src/components/utils/graphrow.rs | 6 ++++-- src/components/utils/logitems.rs | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/utils/graphrow.rs b/src/components/utils/graphrow.rs index d626b6640f..e45bae89a7 100644 --- a/src/components/utils/graphrow.rs +++ b/src/components/utils/graphrow.rs @@ -3,7 +3,6 @@ pub const SYM_COMMIT_BRANCH: &str = "*"; pub const SYM_COMMIT_MERGE: &str = "M"; pub const SYM_COMMIT_STASH: &str = "*"; pub const SYM_COMMIT_UNCOMMITTED: &str = "+"; -pub const SYM_CROSS: &str = "╋"; pub const SYM_VERTICAL: &str = "┃"; pub const SYM_VERTICAL_DOTTED: &str = "╏"; pub const SYM_HORIZONTAL: &str = "━"; @@ -11,6 +10,9 @@ pub const SYM_MERGE_BRIDGE_START: &str = "┓"; pub const SYM_MERGE_BRIDGE_MID: &str = "━"; pub const SYM_MERGE_BRIDGE_END: &str = "┏"; pub const SYM_BRANCH_UP: &str = "┛"; -pub const SYM_BRANCH_DOWN: &str = "┓"; pub const SYM_BRANCH_UP_RIGHT: &str = "┗"; +pub const SYM_TEE_LEFT: &str = "┫"; +pub const SYM_TEE_RIGHT: &str = "┣"; +pub const SYM_TEE_UP: &str = "┻"; +pub const SYM_TEE_DOWN: &str = "┳"; pub const SYM_SPACE: &str = " "; diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 1ff46079a6..1025667d5c 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -83,7 +83,7 @@ impl LogEntry { } } -/// +/// A batch of parsed log entries with an index offset. #[derive(Default)] pub struct ItemBatch { index_offset: Option, @@ -154,9 +154,9 @@ impl ItemBatch { /// pub fn set_graph_rows(&mut self, rows: Vec) { - let mut max = 0; + let mut max: usize = 0; for (entry, row) in self.items.iter_mut().zip(rows) { - max = max.max(row.lane_count); + max = max.max(row.lane_count.into()); entry.graph = Some(row); } self.max_lane = max; From 253217f4072454ddbb8bdccf6182fb97da0880bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:36 -0400 Subject: [PATCH 42/54] commitlist graph rendering refactor big focuses on readability and reduced repition --- src/components/commitlist.rs | 356 +++++++++++++++++------------------ 1 file changed, 177 insertions(+), 179 deletions(-) diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 20aa791e16..69b82e4dd7 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -1,10 +1,10 @@ use super::utils::graphrow::{ - SYM_BRANCH_DOWN, SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, SYM_COMMIT, + SYM_BRANCH_UP, SYM_BRANCH_UP_RIGHT, SYM_COMMIT, SYM_COMMIT_BRANCH, SYM_COMMIT_MERGE, SYM_COMMIT_STASH, - SYM_COMMIT_UNCOMMITTED, SYM_CROSS, SYM_HORIZONTAL, - SYM_MERGE_BRIDGE_END, SYM_MERGE_BRIDGE_MID, - SYM_MERGE_BRIDGE_START, SYM_SPACE, SYM_VERTICAL, - SYM_VERTICAL_DOTTED, + SYM_COMMIT_UNCOMMITTED, SYM_HORIZONTAL, SYM_MERGE_BRIDGE_END, + SYM_MERGE_BRIDGE_MID, SYM_MERGE_BRIDGE_START, SYM_SPACE, + SYM_TEE_DOWN, SYM_TEE_LEFT, SYM_TEE_RIGHT, SYM_TEE_UP, + SYM_VERTICAL, SYM_VERTICAL_DOTTED, }; use super::utils::logitems::{ItemBatch, LogEntry}; use crate::{ @@ -40,6 +40,7 @@ use ratatui::{ Frame, }; use std::borrow::Cow; +use std::collections::HashSet; use std::{ cell::Cell, cmp, collections::BTreeMap, rc::Rc, time::Instant, }; @@ -56,7 +57,7 @@ const GRAPH_COLORS: &[Color] = &[ Color::Red, ]; -/// +/// Renders the commit log with a side-bar graph. pub struct CommitList { repo: RepoPathRef, title: Box, @@ -108,6 +109,16 @@ impl CommitList { } } + /// Whether the commit graph column is currently visible. + pub const fn is_graph_visible(&self) -> bool { + self.show_graph + } + + /// Whether the loaded window already has up-to-date graph rows. + pub const fn is_graph_ready(&self) -> bool { + self.items.graph_ready + } + /// pub fn get_loaded_slice(&self) -> (Vec, usize) { let offset = self.items.index_offset(); @@ -238,6 +249,9 @@ impl CommitList { .or_default() .push(local_branch); } + + // branch tips and head are baked into graph rows + self.items.graph_ready = false; } /// @@ -253,6 +267,9 @@ impl CommitList { .or_default() .push(remote_branch); } + + // branch tips are baked into graph rows + self.items.graph_ready = false; } /// @@ -490,7 +507,6 @@ impl CommitList { } fn build_graph_spans<'a>( - &self, row: &'a GraphRow, graph_col_width: usize, empty_lanes: &std::collections::HashSet, @@ -503,73 +519,47 @@ impl CommitList { } let (sym, graph_color) = match conn { None => (SYM_SPACE, Color::Reset), - Some((ConnectionType::Vertical, color_idx)) => ( - SYM_VERTICAL, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::VerticalDotted, color_idx)) => ( - SYM_VERTICAL_DOTTED, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::Cross, color_idx)) => ( - SYM_CROSS, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::BranchUpMergeStart, color_idx)) => ( - SYM_BRANCH_UP, // Reusing '┛' - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some(( - ConnectionType::BranchUpRightMergeEnd, - color_idx, - )) => ( - SYM_BRANCH_UP_RIGHT, // Reusing '┗' - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::CommitNormal, color_idx)) => ( - SYM_COMMIT, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::CommitBranch, color_idx)) => ( - SYM_COMMIT_BRANCH, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::CommitMerge, color_idx)) => ( - SYM_COMMIT_MERGE, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::CommitStash, color_idx)) => ( - SYM_COMMIT_STASH, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::CommitUncommitted, color_idx)) => ( - SYM_COMMIT_UNCOMMITTED, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::MergeBridgeStart, color_idx)) => ( - SYM_MERGE_BRIDGE_START, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::MergeBridgeMid, color_idx)) => ( - SYM_MERGE_BRIDGE_MID, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::MergeBridgeEnd, color_idx)) => ( - SYM_MERGE_BRIDGE_END, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::BranchDown, color_idx)) => ( - SYM_BRANCH_DOWN, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::BranchUp, color_idx)) => ( - SYM_BRANCH_UP, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), - Some((ConnectionType::BranchUpRight, color_idx)) => ( - SYM_BRANCH_UP_RIGHT, - GRAPH_COLORS[color_idx % GRAPH_COLORS.len()], - ), + Some((conn_type, color_idx)) => { + let color = GRAPH_COLORS[usize::from(*color_idx) + % GRAPH_COLORS.len()]; + let sym = match conn_type { + ConnectionType::Vertical => SYM_VERTICAL, + ConnectionType::VerticalDotted => { + SYM_VERTICAL_DOTTED + } + ConnectionType::TeeLeft => SYM_TEE_LEFT, + ConnectionType::TeeRight => SYM_TEE_RIGHT, + ConnectionType::TeeUp => SYM_TEE_UP, + ConnectionType::TeeDown => SYM_TEE_DOWN, + ConnectionType::BranchUpRight => { + SYM_BRANCH_UP_RIGHT + } + ConnectionType::CommitNormal => SYM_COMMIT, + ConnectionType::CommitBranch => { + SYM_COMMIT_BRANCH + } + ConnectionType::CommitMerge => { + SYM_COMMIT_MERGE + } + ConnectionType::CommitStash => { + SYM_COMMIT_STASH + } + ConnectionType::CommitUncommitted => { + SYM_COMMIT_UNCOMMITTED + } + ConnectionType::MergeBridgeStart => { + SYM_MERGE_BRIDGE_START + } + ConnectionType::MergeBridgeMid => { + SYM_MERGE_BRIDGE_MID + } + ConnectionType::MergeBridgeEnd => { + SYM_MERGE_BRIDGE_END + } + ConnectionType::BranchUp => SYM_BRANCH_UP, + }; + (sym, color) + } }; spans.push(Span::styled( sym, @@ -580,24 +570,26 @@ impl CommitList { let mut is_bridge_lane = false; let mut spacer_color = graph_color; - if let Some((from, to)) = row.merge_bridge { - if lane_index >= from && lane_index < to { - is_bridge_lane = true; - spacer_color = GRAPH_COLORS - [row.commit_lane % GRAPH_COLORS.len()]; - } - } + let commit_lane = usize::from(row.commit_lane); + + let edges = row + .merge_bridge + .into_iter() + .chain(row.branches.iter().copied()); + + for (st, fin) in edges { + let (start, finish) = + (usize::from(st), usize::from(fin)); - for &(from, to) in &row.branches { - if lane_index >= from && lane_index < to { + if (start..finish).contains(&lane_index) { is_bridge_lane = true; - let branch_lane = if row.commit_lane == from { - to + let target_lane = if commit_lane == start { + finish } else { - from + start }; spacer_color = GRAPH_COLORS - [branch_lane % GRAPH_COLORS.len()]; + [target_lane % GRAPH_COLORS.len()]; } } @@ -690,7 +682,7 @@ impl CommitList { empty_lanes: &std::collections::HashSet, ) { if let Some(ref row) = e.graph { - txt.extend(self.build_graph_spans( + txt.extend(Self::build_graph_spans( row, self.graph_col_width.get(), empty_lanes, @@ -787,111 +779,114 @@ impl CommitList { fn get_text(&self, height: usize, width: usize) -> Vec> { let selection = self.relative_selection(); - - let mut txt: Vec = Vec::with_capacity(height); - - let mut empty_lanes = std::collections::HashSet::new(); + let now = Local::now(); + let any_marked = !self.marked.is_empty(); if self.show_graph { - let mut max_lane_in_view = 0; - for e in self + let view = self .items .iter() .skip(self.scroll_top.get()) - .take(height) - { - if let Some(row) = &e.graph { - max_lane_in_view = - max_lane_in_view.max(row.lanes.len()); - } - } - - // Assume all lanes up to max_lane_in_view are empty - empty_lanes.extend(0..max_lane_in_view); - - // Remove lanes that have content - for e in self - .items - .iter() - .skip(self.scroll_top.get()) - .take(height) - { - if let Some(row) = &e.graph { - for (lane_idx, conn) in - row.lanes.iter().enumerate() - { - if !matches!( - conn, - None | Some(( - ConnectionType::MergeBridgeMid, - _ - )) - ) { - empty_lanes.remove(&lane_idx); - } - } - } - } + .take(height); + + let max_lane = view + .clone() + .filter_map(|e| e.graph.as_ref()) + .map(|row| row.lanes.len()) + .max() + .unwrap_or(0); + + let empty_lanes: std::collections::HashSet = (0..max_lane) + .filter(|&index| { + view.clone().filter_map(|entry| entry.graph.as_ref()).all(|row| { + row.lanes.get(index).is_none_or(|conn| { + matches!(conn, None | Some((ConnectionType::MergeBridgeMid, _))) + }) + }) + }) + .collect(); + + self.graph_col_width.set( + ((max_lane.saturating_sub(empty_lanes.len())) * 2) + .max(2), + ); - let width = ((max_lane_in_view - .saturating_sub(empty_lanes.len())) - * 2) - .max(2); - self.graph_col_width.set(width); + self.collect_entries( + height, + width, + selection, + now, + any_marked, + &empty_lanes, + ) } else { self.graph_col_width.set(0); + self.collect_entries( + height, + width, + selection, + now, + any_marked, + &HashSet::new(), + ) } + } - let any_marked = !self.marked.is_empty(); - let now = Local::now(); - - for (idx, e) in self - .items + fn collect_entries( + &self, + height: usize, + width: usize, + selection: usize, + now: DateTime, + any_marked: bool, + empty_lanes: &HashSet, + ) -> Vec> { + self.items .iter() .skip(self.scroll_top.get()) .take(height) .enumerate() - { - let tags = - self.tags.as_ref().and_then(|t| t.get(&e.id)).map( - |tags| { + .map(|(index, entry)| { + let tags = self + .tags + .as_ref() + .and_then(|t| t.get(&entry.id)) + .map(|tags| { tags.iter() - .map(|t| format!("<{}>", t.name)) + .map(|tag| format!("<{}>", tag.name)) .join(" ") - }, - ); - - let local_branches = - self.local_branches.get(&e.id).map(|local_branch| { - local_branch - .iter() - .map(|local_branch| { - format!("{{{0}}}", local_branch.name) - }) - .join(" ") - }); - - let marked = if any_marked { - self.is_marked(&e.id) - } else { - None - }; - - txt.push(self.get_entry_to_add( - e, - idx + self.scroll_top.get() == selection, - tags, - local_branches, - self.remote_branches_string(e), - &self.theme, - width.into(), - now, - marked, - &empty_lanes, - )); - } - - txt + }); + + let local_branches = self + .local_branches + .get(&entry.id) + .map(|local_branch| { + local_branch + .iter() + .map(|branch| { + format!("{{{}}}", branch.name) + }) + .join(" ") + }); + + let marked = any_marked + .then(|| self.is_marked(&entry.id)) + .flatten(); + + self.get_entry_to_add( + entry, + index + self.scroll_top.get() == selection, + tags, + local_branches, + self.remote_branches_string(entry), + &self.theme, + width, + now, + marked, + empty_lanes, + ) + }) + .collect() } fn remote_branches_string(&self, e: &LogEntry) -> Option { @@ -1358,7 +1353,6 @@ mod tests { #[test] fn test_build_graph_spans() { - let cl = CommitList::default(); let row = GraphRow { lane_count: 1, commit_lane: 0, @@ -1369,7 +1363,11 @@ mod tests { merge_bridge: None, branches: vec![], }; - let spans = cl.build_graph_spans(&row, 1); + let spans = CommitList::build_graph_spans( + &row, + 1, + &std::collections::HashSet::new(), + ); assert_eq!(spans.len(), 2); assert_eq!(spans[0].content, Cow::from(SYM_COMMIT)); From 3ede683df961fde1e7a0547657e17056a806c212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:39 -0400 Subject: [PATCH 43/54] revlog tab graph method extraction Extract update_graph_rows and gate behind is_graph_visible/ready. --- src/tabs/revlog.rs | 76 ++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 23687c5493..ef2d6dc648 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -60,7 +60,7 @@ enum LogSearch { Results(LogSearchResult), } -/// +/// Top-level revlog tab that ties together the commit list, details, and search. pub struct Revlog { repo: RepoPathRef, commit_details: CommitDetailsComponent, @@ -135,35 +135,10 @@ impl Revlog { self.list .refresh_extend_data(self.git_log.extract_items()?); - let (slice, global_start) = self.list.get_loaded_slice(); - if !slice.is_empty() { - let mut branch_tips = HashSet::new(); - let mut head_id = None; - - for (id, branches) in self.list.local_branches() { - branch_tips.insert(*id); - if head_id.is_none() - && branches.iter().any(|b| { - b.local_details() - .is_some_and(|d| d.is_head) - }) { - head_id = Some(*id); - } - } - branch_tips - .extend(self.list.remote_branches().keys()); - - let stashes = HashSet::new(); - - if let Some(rows) = self.git_log.get_graph_rows( - &slice, - global_start, - &branch_tips, - &stashes, - head_id.as_ref(), - ) { - self.list.set_graph_rows(rows); - } + if self.list.is_graph_visible() + && !self.list.is_graph_ready() + { + self.update_graph_rows(); } self.git_tags.request(Duration::from_secs(3), false)?; @@ -182,6 +157,47 @@ impl Revlog { Ok(()) } + /// Computes graph rows for the current commit window. Rows + /// don't depend on the selection + /// so its is skipped while + /// [`CommitList::is_graph_ready`] holds the bag. + fn update_graph_rows(&mut self) { + let (slice, global_start) = self.list.get_loaded_slice(); + if slice.is_empty() { + return; + } + + let head_id = self.list.local_branches().iter().find_map( + |(id, branches)| { + let has_head = branches.iter().any(|branch| { + branch.local_details().is_some_and(|b| b.is_head) + }); + has_head.then_some(*id) + }, + ); + + let branch_tips: HashSet<_> = self + .list + .local_branches() + .keys() + .chain(self.list.remote_branches().keys()) + .copied() + .collect(); + + // TODO: include stashes (heh) + let stashes = HashSet::new(); + + if let Some(rows) = self.git_log.get_graph_rows( + &slice, + global_start, + &branch_tips, + &stashes, + head_id.as_ref(), + ) { + self.list.set_graph_rows(rows); + } + } + /// pub fn update_git( &mut self, From 88277ca5912fa65bc5076e7b09fd4683da7e4d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:14:44 -0400 Subject: [PATCH 44/54] snapshot test infrastructure sets up insta for reliable graph snapshots --- Cargo.lock | 860 +++++++++++++++++- src/main.rs | 2 +- ...gitui__gitui__tests__log_graph_hidden.snap | 17 + ...gitui__gitui__tests__log_graph_linear.snap | 17 + ...__gitui__tests__log_graph_with_branch.snap | 19 + 5 files changed, 868 insertions(+), 47 deletions(-) create mode 100644 src/snapshots/gitui__gitui__tests__log_graph_hidden.snap create mode 100644 src/snapshots/gitui__gitui__tests__log_graph_linear.snap create mode 100644 src/snapshots/gitui__gitui__tests__log_graph_with_branch.snap diff --git a/Cargo.lock b/Cargo.lock index 85678f3f24..54b06564dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arc-swap" version = "1.8.0" @@ -150,7 +159,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" name = "asyncgit" version = "0.28.1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "crossbeam-channel", "dirs", "easy-cast", @@ -176,6 +185,15 @@ dependencies = [ "url", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -235,6 +253,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -244,6 +271,12 @@ dependencies = [ "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -258,9 +291,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -330,6 +363,18 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -486,6 +531,15 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -550,7 +604,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "crossterm_winapi", "mio", "parking_lot", @@ -561,6 +615,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.3", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -592,6 +665,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "ctr" version = "0.9.2" @@ -676,6 +759,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "der" version = "0.7.9" @@ -695,6 +784,28 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "diff" version = "0.1.13" @@ -886,6 +997,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fancy-regex" version = "0.16.2" @@ -897,6 +1027,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "faster-hex" version = "0.10.0" @@ -929,6 +1065,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "filetime" version = "0.2.25" @@ -949,6 +1096,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.1" @@ -1084,6 +1243,7 @@ dependencies = [ "libc", "r-efi 6.0.0", "wasip2", + "wasip3", ] [[package]] @@ -1138,7 +1298,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", "libgit2-sys", "log", @@ -1179,7 +1339,7 @@ dependencies = [ "asyncgit", "backtrace", "base64", - "bitflags 2.10.0", + "bitflags 2.13.0", "bugreport", "bwrap", "bytesize", @@ -1204,6 +1364,7 @@ dependencies = [ "parking_lot_core", "pretty_assertions", "ratatui", + "ratatui-textarea", "rayon-core", "ron", "scopeguard", @@ -1373,7 +1534,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "563361198101cedc975fe5760c91ac2e4126eec22216e81b659b45289feaf1ea" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bstr", "gix-path", "libc", @@ -1522,7 +1683,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bstr", "gix-features", "gix-path", @@ -1570,7 +1731,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31c6b3664efe5916c539c50e610f9958f2993faf8e29fa5a40fb80b6ac8486a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bstr", "filetime", "fnv", @@ -1706,7 +1867,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3df6fd8e514d8b99ec5042ee17909a17750ccf54d0b8b30c850954209c800322" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bstr", "gix-attributes", "gix-config-value", @@ -1788,7 +1949,7 @@ version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719cf7d669439e1fca735bd1c4de54d43c5d30e8883fd6063c4924b213d70c9" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bstr", "gix-commitgraph", "gix-date", @@ -1822,7 +1983,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beeb3bc63696cf7acb5747a361693ebdbcaf25b5d27d2308f38e9782983e7bce" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "gix-path", "libc", "windows-sys 0.61.2", @@ -1919,7 +2080,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37f8b53b4c56b01c43a4491c4edfe2ce66c654eb86232205172ceb1650d21c55" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -2026,6 +2187,17 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "heapless" version = "0.8.0" @@ -2042,6 +2214,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -2192,6 +2370,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2236,6 +2420,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -2250,7 +2436,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "inotify-sys", "libc", ] @@ -2386,6 +2572,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481b4381c813cebeca86bd55c781d21f902f34cf927ec08d6df3dfebcfd2002" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -2415,6 +2611,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2424,6 +2626,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.180" @@ -2456,7 +2664,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", "redox_syscall", ] @@ -2487,6 +2695,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2505,6 +2722,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2565,6 +2788,27 @@ dependencies = [ "libc", ] +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -2586,13 +2830,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "fsevent-sys", "inotify", "kqueue", @@ -2731,7 +2998,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", "once_cell", "onig_sys", @@ -2834,6 +3101,30 @@ dependencies = [ "sha2", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2882,54 +3173,131 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "phf" -version = "0.11.3" +name = "pest" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ - "phf_shared", + "memchr", + "ucd-trie", ] [[package]] -name = "phf_shared" -version = "0.11.3" +name = "pest_derive" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ - "siphasher", + "pest", + "pest_generator", ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "pest_generator" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pest_meta" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] [[package]] -name = "pkcs1" -version = "0.7.5" +name = "phf" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "der", - "pkcs8", - "spki", + "phf_macros", + "phf_shared", ] [[package]] -name = "pkcs8" -version = "0.10.2" +name = "phf_codegen" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "der", - "spki", + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", ] [[package]] @@ -3014,6 +3382,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3065,6 +3443,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -3125,6 +3509,74 @@ dependencies = [ "palette", "serde", "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" +dependencies = [ + "cfg-if", + "crossterm 0.28.1", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-termion" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c16cc35a9d9114e0b2bb4b22018b96ae7f5fe60e2595dc73e622b4e78624835" +dependencies = [ + "instability", + "ratatui-core", + "termion", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-textarea" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de236b7cc74b3f7dea227b3fbad97bf459cddf552b6503d888fb9a106eda59ab" +dependencies = [ + "ratatui-core", + "ratatui-crossterm", + "ratatui-widgets", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", "unicode-segmentation", "unicode-width 0.2.0", ] @@ -3155,7 +3607,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -3214,7 +3666,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "once_cell", "serde", "serde_derive", @@ -3264,7 +3716,7 @@ version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3277,7 +3729,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -3779,6 +4231,90 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termion" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" +dependencies = [ + "libc", + "numtoa", + "serde", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.0", + "fancy-regex 0.11.0", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "serde", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -3828,7 +4364,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -3899,6 +4437,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uluru" version = "3.1.0" @@ -3958,6 +4502,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3997,6 +4547,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -4018,6 +4581,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4106,6 +4678,114 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.2", + "indexmap", + "semver", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "serde", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "which" version = "8.0.0" @@ -4402,6 +5082,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/src/main.rs b/src/main.rs index fd662950a2..cb5fc69be6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,7 +114,7 @@ type Terminal = ratatui::Terminal>; static TICK_INTERVAL: Duration = Duration::from_secs(5); static SPINNER_INTERVAL: Duration = Duration::from_millis(80); -/// +/// Events sent to the event-loop to wake up the UI. #[derive(Clone)] pub enum QueueEvent { Tick, diff --git a/src/snapshots/gitui__gitui__tests__log_graph_hidden.snap b/src/snapshots/gitui__gitui__tests__log_graph_hidden.snap new file mode 100644 index 0000000000..bab83a0898 --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__log_graph_hidden.snap @@ -0,0 +1,17 @@ +--- +source: src/gitui.rs +assertion_line: 359 +expression: terminal.backend() +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Commit 3/3──────────────────────────────────────────────────────────────────────────────┐" +"│[COMMIT] <1m ago name commit B █" +"│[COMMIT] <1m ago name commit A ║" +"│[COMMIT] <1m ago name initial ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"└────────────────────────────────────────────────────────────────────────────────────────┘" +"Scroll [↑↓] Mark [˽] Toggle Graph [g] Details [⏎] Branches [b] Compare [⇧C] more [.]" diff --git a/src/snapshots/gitui__gitui__tests__log_graph_linear.snap b/src/snapshots/gitui__gitui__tests__log_graph_linear.snap new file mode 100644 index 0000000000..a03daefa14 --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__log_graph_linear.snap @@ -0,0 +1,17 @@ +--- +source: src/gitui.rs +assertion_line: 341 +expression: terminal.backend() +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Commit 3/3──────────────────────────────────────────────────────────────────────────────┐" +"│o [COMMIT] <1m ago name commit B █" +"│o [COMMIT] <1m ago name commit A ║" +"│o [COMMIT] <1m ago name initial ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"└────────────────────────────────────────────────────────────────────────────────────────┘" +"Scroll [↑↓] Mark [˽] Toggle Graph [g] Details [⏎] Branches [b] Compare [⇧C] more [.]" diff --git a/src/snapshots/gitui__gitui__tests__log_graph_with_branch.snap b/src/snapshots/gitui__gitui__tests__log_graph_with_branch.snap new file mode 100644 index 0000000000..b4033ba5ba --- /dev/null +++ b/src/snapshots/gitui__gitui__tests__log_graph_with_branch.snap @@ -0,0 +1,19 @@ +--- +source: src/gitui.rs +assertion_line: 375 +expression: terminal.backend() +--- +" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " +" ──────────────────────────────────────────────────────────────────────────────────────── " +"┌Commit 4/4──────────────────────────────────────────────────────────────────────────────┐" +"│M━┓ [COMMIT] <1m ago name merge C into main █" +"│o ┃ [COMMIT] <1m ago name commit A ║" +"│┃ o [COMMIT] <1m ago name commit C ║" +"│o━┛ [COMMIT] <1m ago name initial ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"│ ║" +"└────────────────────────────────────────────────────────────────────────────────────────┘" +"Scroll [↑↓] Mark [˽] Toggle Graph [g] Details [⏎] Branches [b] Compare [⇧C] more [.]" From 79d40c9b704e556d3ce0101ecd47fe221ab3ff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:20:32 -0400 Subject: [PATCH 45/54] trailing comma and minor cleanup --- filetreelist/src/lib.rs | 1 - src/app.rs | 6 +++--- src/components/status_tree.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/filetreelist/src/lib.rs b/filetreelist/src/lib.rs index 3e80065aba..74ccf6aed4 100644 --- a/filetreelist/src/lib.rs +++ b/filetreelist/src/lib.rs @@ -1,4 +1,3 @@ -// #![forbid(missing_docs)] #![forbid(unsafe_code)] #![deny( mismatched_lifetime_syntaxes, diff --git a/src/app.rs b/src/app.rs index 8626aa3b8e..5f3f427a98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -138,9 +138,9 @@ impl Environment { use crossbeam_channel::unbounded; Self { queue: Queue::new(), - theme: Default::default(), - key_config: Default::default(), - repo: RefCell::new(RepoPath::Path(Default::default())), + theme: Rc::default(), + key_config: Rc::default(), + repo: RefCell::new(RepoPath::Path(PathBuf::default())), options: Rc::new(RefCell::new(Options::test_env())), sender_git: unbounded().0, sender_app: unbounded().0, diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index ac4fc9f6a8..48935ca777 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -22,7 +22,7 @@ use std::{borrow::Cow, cell::Cell, path::Path}; //TODO: use new `filetreelist` crate -/// +/// Renders the working-tree status as a tree. #[allow(clippy::struct_excessive_bools)] pub struct StatusTreeComponent { title: String, From e06df12625f4866c9d73bfea21e3d063060058ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:44:07 -0400 Subject: [PATCH 46/54] fixed clippy lints --- asyncgit/src/graph/buffer.rs | 2 +- asyncgit/src/graph/walker.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 3f2e7a47c7..c02354901e 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -206,7 +206,7 @@ impl Buffer { .push(Delta(std::mem::take(&mut self.pending_delta))); let step = self.deltas.len(); - if step % CHECKPOINT_INTERVAL == 0 { + if step.is_multiple_of(CHECKPOINT_INTERVAL) { self.checkpoints.insert(step - 1, self.current.clone()); } } diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index d5f7681aad..33c5e2eebb 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -187,7 +187,7 @@ impl GraphWalker { } /// Number of commits already folded into the graph buffer. - pub fn processed_commits(&self) -> usize { + pub const fn processed_commits(&self) -> usize { self.buffer.deltas.len() } From 6964b940b53f168f5f666ae3d9cf71a9fa326f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 12:57:18 -0400 Subject: [PATCH 47/54] restore [0.28.1] release notes lost in rebase conflict resolution --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 859f74894c..90fd08d4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) * when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748)) +## [0.28.1] - 2026-03-21 + +### Changed +* support proper pre-push hook ([#2809](https://github.com/gitui-org/gitui/issues/2809)) +* improve `gitui --version` message [[@hlsxx](https://github.com/hlsxx)] ([#2838](https://github.com/gitui-org/gitui/issues/2838)) +* rust msrv bumped to `1.88` + +### Fixed +* fix extremely slow status loading in large repositories by replacing time-based cache invalidation with generation counter [[@DannyStoll1](https://github.com/DannyStoll1)] ([#2823](https://github.com/gitui-org/gitui/issues/2823)) +* fix panic when renaming or updating remote URL with no remotes configured [[@xvchris](https://github.com/xvchris)] ([#2868](https://github.com/gitui-org/gitui/issues/2868)) + ## [0.28.0] - 2025-12-14 **discard changes on checkout** From e611ed03f4ab2728d2fea5301686e0d90551d4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 13:10:12 -0400 Subject: [PATCH 48/54] fixed snapshot regression --- src/gitui.rs | 2 +- .../gitui__gitui__tests__app_log_tab_showing_one_commit.snap | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gitui.rs b/src/gitui.rs index 03d73b11c1..8052218ac2 100644 --- a/src/gitui.rs +++ b/src/gitui.rs @@ -226,7 +226,7 @@ mod tests { // Linux Temp Folder settings.add_filter(r" */tmp/\.tmp\S+-insta/", "[TEMP_FILE]"); // Commit ids that follow a vertical bar - settings.add_filter(r"│[a-z0-9]{7} ", "│[AAAAA] "); + settings.add_filter(r"│[^a-f0-9]*[a-f0-9]{7} ", "│[AAAAA] "); let _bound = settings.bind_to_scope(); } } diff --git a/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap b/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap index bbdd5be8a4..ea57f3165b 100644 --- a/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap +++ b/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap @@ -6,7 +6,7 @@ snapshot_kind: text " Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " " ──────────────────────────────────────────────────────────────────────────────────────── " "┌Commit 1/1──────────────────────────────────────────────────────────────────────────────┐" -"│[AAAAA] <1m ago name initial █" +"│[AAAAA] <1m ago name initial █" "│ ║" "│ ║" "│ ║" @@ -14,4 +14,4 @@ snapshot_kind: text "│ ║" "│ ║" "└────────────────────────────────────────────────────────────────────────────────────────┘" -"Scroll [↑↓] Mark [˽] Details [⏎] Branches [b] Compare [⇧C] Copy Hash [y] Tag [t] more [.]" +"Scroll [↑↓] Mark [˽] Toggle Graph [g] Details [⏎] Branches [b] Compare [⇧C] more [.]" From 62b8b117f5791a2d8db4aef15ea997d8f568ce1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 14:10:53 -0400 Subject: [PATCH 49/54] fixes to spatial positioning overly pessimistic collapsing rmeoved --- asyncgit/src/graph/buffer.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index c02354901e..3550c834ce 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -148,16 +148,6 @@ impl Buffer { } fn flush_merge_commits(&mut self) { - // Collect available empty lanes once, before we start maybe filling them. - let mut empty_lanes: Vec = self - .current - .iter() - .enumerate() - .filter_map(|(index, slot)| { - slot.is_none().then_some(index) - }) - .collect(); - while let Some(alias) = self.merge_commits.pop() { // Search for an occupied slot that matches the target alias. // If found, extract its index and a mutable clone of the chunk. @@ -184,14 +174,14 @@ impl Buffer { marker: Markers::Commit, }; - if let Some(empty_index) = empty_lanes.pop() { - self.record_replace(empty_index, Some(new_lane)); - } else { - self.record_insert( - self.current.len(), - Some(new_lane), - ); - } + // Always append the merge's second-parent lane to + // the end instead of reusing an existing empty slot, + // so the new visual column does not collapse + // spatial ordering of lanes already in existence. + self.record_insert( + self.current.len(), + Some(new_lane), + ); } } } From 3073bf58a571119250745af7d9d8ea3a20a10a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Sat, 13 Jun 2026 16:16:30 -0400 Subject: [PATCH 50/54] clippy fixes i keep forgetting asyncigt is different --- asyncgit/src/sync/remotes/mod.rs | 7 +++---- asyncgit/src/sync/sign.rs | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/asyncgit/src/sync/remotes/mod.rs b/asyncgit/src/sync/remotes/mod.rs index b32810dd9e..ad884d19c8 100644 --- a/asyncgit/src/sync/remotes/mod.rs +++ b/asyncgit/src/sync/remotes/mod.rs @@ -161,7 +161,7 @@ pub(crate) fn get_default_remote_for_fetch_in_repo( if let Some(branch) = branch { let remote_name = bytes2string(branch.name_bytes()?)?; - let entry_name = format!("branch.{}.remote", &remote_name); + let entry_name = format!("branch.{remote_name}.remote"); if let Ok(entry) = config.get_entry(&entry_name) { return bytes2string(entry.value_bytes()); @@ -211,8 +211,7 @@ pub(crate) fn get_default_remote_for_push_in_repo( if let Some(branch) = branch { let remote_name = bytes2string(branch.name_bytes()?)?; - let entry_name = - format!("branch.{}.pushRemote", &remote_name); + let entry_name = format!("branch.{remote_name}.pushRemote"); if let Ok(entry) = config.get_entry(&entry_name) { return bytes2string(entry.value_bytes()); @@ -222,7 +221,7 @@ pub(crate) fn get_default_remote_for_push_in_repo( return bytes2string(entry.value_bytes()); } - let entry_name = format!("branch.{}.remote", &remote_name); + let entry_name = format!("branch.{remote_name}.remote"); if let Ok(entry) = config.get_entry(&entry_name) { return bytes2string(entry.value_bytes()); diff --git a/asyncgit/src/sync/sign.rs b/asyncgit/src/sync/sign.rs index da5079f72c..bde0fc53d2 100644 --- a/asyncgit/src/sync/sign.rs +++ b/asyncgit/src/sync/sign.rs @@ -239,7 +239,7 @@ impl Sign for GPGSign { if !output.status.success() { return Err(SignError::Shellout(format!( "failed to sign data, program '{}' exited non-zero: {}", - &self.program, + self.program, std::str::from_utf8(&output.stderr) .unwrap_or("[error could not be read from stderr]") ))); @@ -250,7 +250,7 @@ impl Sign for GPGSign { if !stderr.contains("\n[GNUPG:] SIG_CREATED ") { return Err(SignError::Shellout( - format!("failed to sign data, program '{}' failed, SIG_CREATED not seen in stderr", &self.program), + format!("failed to sign data, program '{}' failed, SIG_CREATED not seen in stderr", self.program), )); } From 904b4e298bfe3086cadff186bacc4bb1ca5aba9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Tue, 23 Jun 2026 16:43:39 -0400 Subject: [PATCH 51/54] newtype wrappers for lane index and alias id --- asyncgit/src/graph/buffer.rs | 9 ++++--- asyncgit/src/graph/chunk.rs | 8 +++--- asyncgit/src/graph/mod.rs | 48 +++++++++++++++++++++++++++--------- asyncgit/src/graph/oids.rs | 9 ++++--- asyncgit/src/graph/walker.rs | 36 +++++++++++++-------------- 5 files changed, 69 insertions(+), 41 deletions(-) diff --git a/asyncgit/src/graph/buffer.rs b/asyncgit/src/graph/buffer.rs index 3550c834ce..402ea24996 100644 --- a/asyncgit/src/graph/buffer.rs +++ b/asyncgit/src/graph/buffer.rs @@ -1,4 +1,5 @@ use super::chunk::{Chunk, Markers}; +use super::AliasId; use std::collections::BTreeMap; /// A single mutation of the lane state, recorded while processing one @@ -39,7 +40,7 @@ pub struct Buffer { /// Aliases of merge commits whose second parent still needs a new /// lane. - merge_commits: Vec, + merge_commits: Vec, /// Scratch list of the [`DeltaOp`]s recorded for processing commit pending_delta: Vec, @@ -64,7 +65,7 @@ impl Buffer { /// Remember `alias` as a merge commit whose second parent must be /// given its own lane. - pub fn track_merge_commit(&mut self, alias: usize) { + pub fn track_merge_commit(&mut self, alias: AliasId) { self.merge_commits.push(alias); } @@ -104,7 +105,7 @@ impl Buffer { fn find_lane_awaiting_parent( &self, - alias: Option, + alias: Option, ) -> Option { let alias = alias?; self.current.iter().position(|slot| { @@ -119,7 +120,7 @@ impl Buffer { fn consume_alias_in_other_chunks( &mut self, - alias: usize, + alias: AliasId, skip_index: usize, ) { for index in 0..self.current.len() { diff --git a/asyncgit/src/graph/chunk.rs b/asyncgit/src/graph/chunk.rs index 8c70795c21..20748330a9 100644 --- a/asyncgit/src/graph/chunk.rs +++ b/asyncgit/src/graph/chunk.rs @@ -1,3 +1,5 @@ +use super::AliasId; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum Markers { Uncommitted, @@ -6,8 +8,8 @@ pub enum Markers { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Chunk { - pub alias: Option, - pub parent_a: Option, - pub parent_b: Option, + pub alias: Option, + pub parent_a: Option, + pub parent_b: Option, pub marker: Markers, } diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index d5687ea829..5bc8f87cd2 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -11,13 +11,39 @@ pub const MAX_LANE_COLORS: usize = 16; // Yes, there are repositories where this is exceeded // Are they very rare? Yes. // On most terminals can more than 256 lanes even be represneted usefully? Not really. -pub type LaneIdx = u8; +#[derive(Clone, Copy, Debug, Default)] +pub struct LaneIndex(u8); -/// Convert a lane position into the compact [`LaneIdx`] -/// representation. This way we can keep full granularity when computing, -/// but not when storing. -pub(crate) fn to_lane_idx(lane: usize) -> LaneIdx { - LaneIdx::try_from(lane).unwrap_or(LaneIdx::MAX) +/// Numeric alias assigned to each commit in the graph. +/// +/// The alias is a dense integer index created by [`GraphOids`](super::oids::GraphOids) +/// that avoids storing full [`CommitId`](crate::sync::CommitId)s inside the lane state. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct AliasId(usize); + +impl std::ops::Deref for AliasId { + type Target = usize; + fn deref(&self) -> &usize { + &self.0 + } +} + +impl From for AliasId { + fn from(v: usize) -> Self { + Self(v) + } +} + +impl From for LaneIndex { + fn from(lane: usize) -> Self { + Self(u8::try_from(lane).unwrap_or(u8::MAX)) + } +} + +impl From for usize { + fn from(lane: LaneIndex) -> Self { + lane.0 as usize + } } /// The type of connection between nodes in the graph. @@ -53,10 +79,10 @@ pub enum ConnectionType { #[derive(Clone, Debug, Default)] pub struct GraphRow { /// Number of active lanes at this commit row - pub lane_count: LaneIdx, + pub lane_count: LaneIndex, /// Which lane index this commit sits on - pub commit_lane: LaneIdx, + pub commit_lane: LaneIndex, /// Whether this is a merge commit (two parents) pub is_merge: bool, @@ -70,13 +96,13 @@ pub struct GraphRow { /// Connections emitted per lane: /// None = empty space /// Some((ConnectionType, `color_index`)) = draw this connector in this color - pub lanes: Vec>, + pub lanes: Vec>, /// Horizontal merge bridge: if this commit merges rightward, /// (`from_lane`, `to_lane`) — the span to draw ─ ╭ ╮ across - pub merge_bridge: Option<(LaneIdx, LaneIdx)>, + pub merge_bridge: Option<(LaneIndex, LaneIndex)>, /// Horizontal branch bridges: if this commit spawns branches, /// spans to draw ─ ╭ ╮ across - pub branches: Vec<(LaneIdx, LaneIdx)>, + pub branches: Vec<(LaneIndex, LaneIndex)>, } diff --git a/asyncgit/src/graph/oids.rs b/asyncgit/src/graph/oids.rs index dca161de88..38827c24c9 100644 --- a/asyncgit/src/graph/oids.rs +++ b/asyncgit/src/graph/oids.rs @@ -1,8 +1,9 @@ +use super::AliasId; use crate::sync::CommitId; use std::collections::HashMap; /// mapping of `CommitId` to a numeric alias -pub struct GraphOids(HashMap); +pub struct GraphOids(HashMap); impl Default for GraphOids { fn default() -> Self { @@ -17,18 +18,18 @@ impl GraphOids { } /// Get the alias for `id`, assigning a new one if it doesn't exist yet. - pub fn get_or_insert(&mut self, id: &CommitId) -> usize { + pub fn get_or_insert(&mut self, id: &CommitId) -> AliasId { if let Some(&alias) = self.0.get(id) { return alias; } - let alias = self.0.len(); + let alias = AliasId::from(self.0.len()); self.0.insert(*id, alias); alias } /// Look up the alias for `id`, returning `None` if not found. - pub fn get(&self, id: &CommitId) -> Option { + pub fn get(&self, id: &CommitId) -> Option { self.0.get(id).copied() } } diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index 33c5e2eebb..2b46b41233 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -1,15 +1,13 @@ use super::buffer::Buffer; use super::chunk::{Chunk, Markers}; use super::oids::GraphOids; -use super::{ - to_lane_idx, ConnectionType, GraphRow, LaneIdx, MAX_LANE_COLORS, -}; +use super::{AliasId, ConnectionType, GraphRow, LaneIndex, MAX_LANE_COLORS}; use crate::sync::CommitId; use std::collections::{HashMap, HashSet}; /// Get the lanes color index, which cycles through the ste palette. -fn lane_color(lane: usize) -> LaneIdx { - to_lane_idx(lane % MAX_LANE_COLORS) +fn lane_color(lane: usize) -> LaneIndex { + LaneIndex::from(lane % MAX_LANE_COLORS) } use bitflags::bitflags; @@ -103,9 +101,9 @@ const fn dirs_conn(dirs: Dirs, dotted: bool) -> ConnectionType { /// The line with a vertical component is the chosen way with color /// ensuring lanes stay visually continuous fn overlay_cell( - cell: &mut Option<(ConnectionType, LaneIdx)>, + cell: &mut Option<(ConnectionType, LaneIndex)>, add: Dirs, - color: LaneIdx, + color: LaneIndex, ) { if let Some((conn, existing_color)) = cell { if let Some(existing) = conn_dirs(*conn) { @@ -135,7 +133,7 @@ pub struct GraphWalker { pub branch_lane_map: HashMap, /// Maps a merge commit's alias to the alias of its second parent. - pub merge_parents: HashMap, + pub merge_parents: HashMap, } impl Default for GraphWalker { @@ -236,7 +234,7 @@ impl GraphWalker { } fn draw_merge_bridge( - lanes: &mut [Option<(ConnectionType, LaneIdx)>], + lanes: &mut [Option<(ConnectionType, LaneIndex)>], merge_bridge: Option<(usize, usize)>, commit_lane: usize, current: &[Option], @@ -287,15 +285,15 @@ impl GraphWalker { } fn draw_branching_lanes( - lanes: &mut Vec>, + lanes: &mut Vec>, branching_lanes: &[usize], commit_lane: usize, - ) -> Vec<(LaneIdx, LaneIdx)> { + ) -> Vec<(LaneIndex, LaneIndex)> { let mut branches = Vec::new(); for &branch_lane in branching_lanes { let from = std::cmp::min(branch_lane, commit_lane); let to = std::cmp::max(branch_lane, commit_lane); - branches.push((to_lane_idx(from), to_lane_idx(to))); + branches.push((LaneIndex::from(from), LaneIndex::from(to))); if lanes.len() <= to { lanes.resize(to + 1, None); @@ -403,14 +401,14 @@ impl GraphWalker { ); GraphRow { - lane_count: to_lane_idx(current.iter().flatten().count()), - commit_lane: to_lane_idx(commit_lane), + lane_count: LaneIndex::from(current.iter().flatten().count()), + commit_lane: LaneIndex::from(commit_lane), is_merge, is_branch_tip, is_stash, lanes, merge_bridge: merge_bridge - .map(|(f, t)| (to_lane_idx(f), to_lane_idx(t))), + .map(|(f, t)| (LaneIndex::from(f), LaneIndex::from(t))), branches, } } @@ -418,9 +416,9 @@ impl GraphWalker { #[allow(clippy::too_many_arguments)] fn fill_lanes( &self, - lanes: &mut [Option<(ConnectionType, LaneIdx)>], + lanes: &mut [Option<(ConnectionType, LaneIndex)>], curr: &[Option], - alias: Option, + alias: Option, head_id: Option<&CommitId>, is_stash: bool, is_merge: bool, @@ -473,10 +471,10 @@ impl GraphWalker { /// between its two ends, merging with whatever each cell already /// shows. fn draw_bridge_span( - lanes: &mut [Option<(ConnectionType, LaneIdx)>], + lanes: &mut [Option<(ConnectionType, LaneIndex)>], from: usize, to: usize, - color: LaneIdx, + color: LaneIndex, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { overlay_cell(lane, Dirs::LEFT | Dirs::RIGHT, color); From aa54b586e1192b8fc6ad53a316c7e3bbea07f21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Tue, 23 Jun 2026 16:43:58 -0400 Subject: [PATCH 52/54] rename Dirs to Directions --- asyncgit/src/graph/walker.rs | 54 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index 2b46b41233..e78462062e 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -17,7 +17,7 @@ bitflags! { /// are merged through this representation rather than /// a naive overwrite for readability. #[derive(Clone, Copy, Default)] - struct Dirs: u8 { + struct Directions: u8 { const UP = 0b0001; const DOWN = 0b0010; const LEFT = 0b0100; @@ -25,7 +25,7 @@ bitflags! { } } -impl Dirs { +impl Directions { #[allow(clippy::missing_const_for_fn)] fn merge(self, other: Self) -> Self { Self::from_bits_retain(self.bits() | other.bits()) @@ -39,23 +39,23 @@ impl Dirs { /// The sub network of an existing connection glyph /// `None` represents commit markers, which are never drawn over. -fn conn_dirs(conn: ConnectionType) -> Option { +fn conn_dirs(conn: ConnectionType) -> Option { Some(match conn { ConnectionType::Vertical | ConnectionType::VerticalDotted => { - Dirs::UP | Dirs::DOWN + Directions::UP | Directions::DOWN } - ConnectionType::MergeBridgeMid => Dirs::LEFT | Dirs::RIGHT, - ConnectionType::MergeBridgeStart => Dirs::DOWN | Dirs::LEFT, - ConnectionType::MergeBridgeEnd => Dirs::DOWN | Dirs::RIGHT, - ConnectionType::BranchUp => Dirs::UP | Dirs::LEFT, - ConnectionType::BranchUpRight => Dirs::UP | Dirs::RIGHT, - ConnectionType::TeeLeft => Dirs::UP | Dirs::DOWN | Dirs::LEFT, + ConnectionType::MergeBridgeMid => Directions::LEFT | Directions::RIGHT, + ConnectionType::MergeBridgeStart => Directions::DOWN | Directions::LEFT, + ConnectionType::MergeBridgeEnd => Directions::DOWN | Directions::RIGHT, + ConnectionType::BranchUp => Directions::UP | Directions::LEFT, + ConnectionType::BranchUpRight => Directions::UP | Directions::RIGHT, + ConnectionType::TeeLeft => Directions::UP | Directions::DOWN | Directions::LEFT, ConnectionType::TeeRight => { - Dirs::UP | Dirs::DOWN | Dirs::RIGHT + Directions::UP | Directions::DOWN | Directions::RIGHT } - ConnectionType::TeeUp => Dirs::UP | Dirs::LEFT | Dirs::RIGHT, + ConnectionType::TeeUp => Directions::UP | Directions::LEFT | Directions::RIGHT, ConnectionType::TeeDown => { - Dirs::DOWN | Dirs::LEFT | Dirs::RIGHT + Directions::DOWN | Directions::LEFT | Directions::RIGHT } ConnectionType::CommitNormal | ConnectionType::CommitBranch @@ -69,11 +69,11 @@ fn conn_dirs(conn: ConnectionType) -> Option { /// Vertical lines take precedence in crossed cells. /// Yet the horizontal bridge continues in /// the spacer columns either side, so we retain wholeness. -const fn dirs_conn(dirs: Dirs, dotted: bool) -> ConnectionType { - let up = dirs.contains(Dirs::UP); - let down = dirs.contains(Dirs::DOWN); - let left = dirs.contains(Dirs::LEFT); - let right = dirs.contains(Dirs::RIGHT); +const fn dirs_conn(dirs: Directions, dotted: bool) -> ConnectionType { + let up = dirs.contains(Directions::UP); + let down = dirs.contains(Directions::DOWN); + let left = dirs.contains(Directions::LEFT); + let right = dirs.contains(Directions::RIGHT); match (up, down, left, right) { (true, true, true, false) => ConnectionType::TeeLeft, (true, true, false, true) => ConnectionType::TeeRight, @@ -102,7 +102,7 @@ const fn dirs_conn(dirs: Dirs, dotted: bool) -> ConnectionType { /// ensuring lanes stay visually continuous fn overlay_cell( cell: &mut Option<(ConnectionType, LaneIndex)>, - add: Dirs, + add: Directions, color: LaneIndex, ) { if let Some((conn, existing_color)) = cell { @@ -258,15 +258,15 @@ impl GraphWalker { // target lane with the precise corner/junction lanes[target_lane] = None; let target_dirs = { - let mut d = Dirs::DOWN; + let mut d = Directions::DOWN; if continues_up { - d |= Dirs::UP; + d |= Directions::UP; } if target_lane > commit_lane { - d |= Dirs::LEFT; + d |= Directions::LEFT; } if target_lane < commit_lane { - d |= Dirs::RIGHT; + d |= Directions::RIGHT; } d }; @@ -306,12 +306,12 @@ impl GraphWalker { lane_color(branch_lane), ); let branch_dirs = { - let mut d = Dirs::UP; + let mut d = Directions::UP; if branch_lane == to { - d |= Dirs::LEFT; + d |= Directions::LEFT; } if branch_lane == from { - d |= Dirs::RIGHT; + d |= Directions::RIGHT; } d }; @@ -477,7 +477,7 @@ impl GraphWalker { color: LaneIndex, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { - overlay_cell(lane, Dirs::LEFT | Dirs::RIGHT, color); + overlay_cell(lane, Directions::LEFT | Directions::RIGHT, color); } } } From a23a59e59fb128ceeb5265aa071931c03f8bb443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Tue, 23 Jun 2026 16:45:07 -0400 Subject: [PATCH 53/54] extract rendering helpers from walker --- asyncgit/src/graph/mod.rs | 2 +- asyncgit/src/graph/walker.rs | 552 ++++++++++++++++++++++------------- 2 files changed, 354 insertions(+), 200 deletions(-) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index 5bc8f87cd2..185962768e 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -11,7 +11,7 @@ pub const MAX_LANE_COLORS: usize = 16; // Yes, there are repositories where this is exceeded // Are they very rare? Yes. // On most terminals can more than 256 lanes even be represneted usefully? Not really. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct LaneIndex(u8); /// Numeric alias assigned to each commit in the graph. diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index e78462062e..01fca55b4c 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -3,6 +3,7 @@ use super::chunk::{Chunk, Markers}; use super::oids::GraphOids; use super::{AliasId, ConnectionType, GraphRow, LaneIndex, MAX_LANE_COLORS}; use crate::sync::CommitId; +use core::cmp::Ordering; use std::collections::{HashMap, HashSet}; /// Get the lanes color index, which cycles through the ste palette. @@ -44,16 +45,28 @@ fn conn_dirs(conn: ConnectionType) -> Option { ConnectionType::Vertical | ConnectionType::VerticalDotted => { Directions::UP | Directions::DOWN } - ConnectionType::MergeBridgeMid => Directions::LEFT | Directions::RIGHT, - ConnectionType::MergeBridgeStart => Directions::DOWN | Directions::LEFT, - ConnectionType::MergeBridgeEnd => Directions::DOWN | Directions::RIGHT, + ConnectionType::MergeBridgeMid => { + Directions::LEFT | Directions::RIGHT + } + ConnectionType::MergeBridgeStart => { + Directions::DOWN | Directions::LEFT + } + ConnectionType::MergeBridgeEnd => { + Directions::DOWN | Directions::RIGHT + } ConnectionType::BranchUp => Directions::UP | Directions::LEFT, - ConnectionType::BranchUpRight => Directions::UP | Directions::RIGHT, - ConnectionType::TeeLeft => Directions::UP | Directions::DOWN | Directions::LEFT, + ConnectionType::BranchUpRight => { + Directions::UP | Directions::RIGHT + } + ConnectionType::TeeLeft => { + Directions::UP | Directions::DOWN | Directions::LEFT + } ConnectionType::TeeRight => { Directions::UP | Directions::DOWN | Directions::RIGHT } - ConnectionType::TeeUp => Directions::UP | Directions::LEFT | Directions::RIGHT, + ConnectionType::TeeUp => { + Directions::UP | Directions::LEFT | Directions::RIGHT + } ConnectionType::TeeDown => { Directions::DOWN | Directions::LEFT | Directions::RIGHT } @@ -152,33 +165,36 @@ impl GraphWalker { } } - pub fn process(&mut self, id: CommitId, parents: &[CommitId]) { - let alias = self.oids.get_or_insert(&id); + pub fn process( + &mut self, + commit_id: CommitId, + parents: &[CommitId], + ) { + let commit_alias = self.oids.get_or_insert(&commit_id); - let mut mapped_parents = - parents.iter().map(|p| self.oids.get_or_insert(p)); + let mut mapped_parents = parents + .iter() + .map(|parent_id| self.oids.get_or_insert(parent_id)); - // We make the executive and saddening decision to not support octo/mega merges - // TUIs are simply a backwards medium for representing this complexity + // We explicitly cap support at 2 parents, ignoring octo/mega merges. let first_parent = mapped_parents.next(); let second_parent = mapped_parents.next(); let chunk = Chunk { - alias: Some(alias), + alias: Some(commit_alias), parent_a: first_parent, parent_b: second_parent, marker: Markers::Commit, }; - second_parent.map(|b| self.merge_parents.insert(alias, b)); + if let Some(second) = second_parent { + self.merge_parents.insert(commit_alias, second); - if first_parent.is_some() - && second_parent.is_some() - && !self.buffer.current.iter().flatten().any(|commit| { - commit.parent_a == second_parent - && commit.parent_b.is_none() - }) { - self.buffer.track_merge_commit(alias); + if first_parent.is_some() + && !self.is_redundant_merge_track(second) + { + self.buffer.track_merge_commit(commit_alias); + } } self.buffer.update(&chunk); @@ -192,7 +208,7 @@ impl GraphWalker { pub fn compute_rows( &self, commit_range: &[CommitId], - global_start: usize, + global_start_index: usize, branch_tips: &HashSet, stashes: &HashSet, head_id: Option<&CommitId>, @@ -201,30 +217,36 @@ impl GraphWalker { return Vec::new(); } - // decompress one row before the range (when there is one) so - // every row's predecessor state comes from the same replay - let snap_start = global_start.saturating_sub(1); - let end = global_start + commit_range.len() - 1; - let snapshots = self.buffer.decompress(snap_start, end); - let offset = global_start - snap_start; + // Decompress one row before the range to establish predecessor state + let snapshot_start_index = + global_start_index.saturating_sub(1); + let snapshot_end_index = + global_start_index + commit_range.len() - 1; + let snapshots = self + .buffer + .decompress(snapshot_start_index, snapshot_end_index); + let index_offset = global_start_index - snapshot_start_index; commit_range .iter() .enumerate() - .map(|(index, commit_id)| { - let place = index + offset; + .map(|(range_index, commit_id)| { + let snapshot_index = range_index + index_offset; + + let current_snapshot = snapshots + .get(snapshot_index) + .map(Vec::as_slice) + .unwrap_or_default(); - let current: &[Option] = - snapshots.get(place).map_or(&[], Vec::as_slice); - let previous = place + let previous_snapshot = snapshot_index .checked_sub(1) - .and_then(|i| snapshots.get(i)) + .and_then(|index| snapshots.get(index)) .map(Vec::as_slice); self.render_row( commit_id, - current, - previous, + current_snapshot, + previous_snapshot, branch_tips, stashes, head_id, @@ -237,50 +259,48 @@ impl GraphWalker { lanes: &mut [Option<(ConnectionType, LaneIndex)>], merge_bridge: Option<(usize, usize)>, commit_lane: usize, - current: &[Option], - previous: Option<&[Option]>, + current_snapshot: &[Option], + previous_snapshot: Option<&[Option]>, ) { - let Some((from, to)) = merge_bridge.filter(|(f, t)| f != t) - else { + let Some((source_lane, target_lane)) = merge_bridge else { return; }; - let target_lane = if from == commit_lane { to } else { from }; - - // only draw the corner continuing upward when the target - // lane already existed on the previous row; a brand-new - // lane starts at this corner - let continues_up = current[target_lane].is_some() - && previous.is_some_and(|prev| { - prev.get(target_lane) == Some(¤t[target_lane]) - }); + if source_lane == target_lane { + return; + } - // replace the plain vertical fill_lanes drew for the - // target lane with the precise corner/junction - lanes[target_lane] = None; - let target_dirs = { - let mut d = Directions::DOWN; - if continues_up { - d |= Directions::UP; - } - if target_lane > commit_lane { - d |= Directions::LEFT; - } - if target_lane < commit_lane { - d |= Directions::RIGHT; - } - d + let destination_lane = if source_lane == commit_lane { + target_lane + } else { + source_lane }; + let connection_color = lane_color(destination_lane); + + let continues_upwards = Self::lane_continues_upwards( + destination_lane, + current_snapshot, + previous_snapshot, + ); + + let target_directions = Self::calculate_merge_directions( + commit_lane, + destination_lane, + continues_upwards, + ); + + // Replace the plain vertical fill with the precise corner/junction + lanes[destination_lane] = None; overlay_cell( - &mut lanes[target_lane], - target_dirs, - lane_color(target_lane), + &mut lanes[destination_lane], + target_directions, + connection_color, ); Self::draw_bridge_span( lanes, - from, - to, - lane_color(target_lane), + source_lane, + target_lane, + connection_color, ); } @@ -289,109 +309,188 @@ impl GraphWalker { branching_lanes: &[usize], commit_lane: usize, ) -> Vec<(LaneIndex, LaneIndex)> { - let mut branches = Vec::new(); - for &branch_lane in branching_lanes { - let from = std::cmp::min(branch_lane, commit_lane); - let to = std::cmp::max(branch_lane, commit_lane); - branches.push((LaneIndex::from(from), LaneIndex::from(to))); - - if lanes.len() <= to { - lanes.resize(to + 1, None); - } + branching_lanes + .iter() + .map(|&branch_lane| { + let start_lane = + std::cmp::min(branch_lane, commit_lane); + let end_lane = + std::cmp::max(branch_lane, commit_lane); + + Self::ensure_lane_capacity(lanes, end_lane); + + let connection_color = lane_color(branch_lane); + Self::draw_bridge_span( + lanes, + start_lane, + end_lane, + connection_color, + ); + + let branch_directions = + Self::calculate_branch_directions( + branch_lane, + start_lane, + end_lane, + ); + overlay_cell( + &mut lanes[branch_lane], + branch_directions, + connection_color, + ); + + (LaneIndex::from(start_lane), LaneIndex::from(end_lane)) + }) + .collect() + } - Self::draw_bridge_span( - lanes, - from, - to, - lane_color(branch_lane), - ); - let branch_dirs = { - let mut d = Directions::UP; - if branch_lane == to { - d |= Directions::LEFT; - } - if branch_lane == from { - d |= Directions::RIGHT; - } - d - }; - overlay_cell( - &mut lanes[branch_lane], - branch_dirs, - lane_color(branch_lane), - ); + /// Checks if tracking a merge commit would be redundant based on current buffer state. + fn is_redundant_merge_track( + &self, + target_parent: AliasId, + ) -> bool { + self.buffer.current.iter().flatten().any(|commit| { + commit.parent_a == Some(target_parent) + && commit.parent_b.is_none() + }) + } + + /// Determines if a lane should draw an upward-connecting corner. + fn lane_continues_upwards( + target_lane: usize, + current_snapshot: &[Option], + previous_snapshot: Option<&[Option]>, + ) -> bool { + let exists_in_current = + current_snapshot.get(target_lane).is_some(); + let matches_previous = + previous_snapshot.is_some_and(|previous| { + previous.get(target_lane) + == current_snapshot.get(target_lane) + }); + + exists_in_current && matches_previous + } + + /// Uses `Ordering` to elegantly map spatial relationships to visual bitmasks. + fn calculate_merge_directions( + commit_lane: usize, + target_lane: usize, + continues_upwards: bool, + ) -> Directions { + let mut directions = Directions::DOWN; + + if continues_upwards { + directions |= Directions::UP; + } + + match target_lane.cmp(&commit_lane) { + Ordering::Greater => directions |= Directions::LEFT, + Ordering::Less => directions |= Directions::RIGHT, + Ordering::Equal => {} + } + + directions + } + + fn calculate_branch_directions( + branch_lane: usize, + start_lane: usize, + end_lane: usize, + ) -> Directions { + let mut directions = Directions::UP; + + if branch_lane == end_lane { + directions |= Directions::LEFT; + } + if branch_lane == start_lane { + directions |= Directions::RIGHT; + } + + directions + } + + fn ensure_lane_capacity( + lanes: &mut Vec>, + required_index: usize, + ) { + if lanes.len() <= required_index { + lanes.resize(required_index + 1, None); } - branches } - fn render_row( + pub fn render_row( &self, commit_id: &CommitId, - current: &[Option], - previous: Option<&[Option]>, + current_snapshot: &[Option], + previous_snapshot: Option<&[Option]>, branch_tips: &HashSet, stashes: &HashSet, head_id: Option<&CommitId>, ) -> GraphRow { - let alias = self.oids.get(commit_id); - let commit_lane = current - .iter() - .position(|c| { - c.as_ref().is_some_and(|chunk| { - alias.is_some() && chunk.alias == alias - }) - }) - .unwrap_or(0); - - let parent_b_alias = - alias.and_then(|a| self.merge_parents.get(&a).copied()); - - let is_merge = parent_b_alias.is_some(); + let commit_alias = self.oids.get(commit_id); + let head_alias = head_id.and_then(|id| self.oids.get(id)); + let second_parent_alias = commit_alias.and_then(|alias| { + self.merge_parents.get(&alias).copied() + }); + + let commit_lane = + Self::find_commit_lane(current_snapshot, commit_alias); + let is_merge = second_parent_alias.is_some(); let is_branch_tip = branch_tips.contains(commit_id); let is_stash = stashes.contains(commit_id); - let branching_lanes: Vec = previous - .into_iter() - .flatten() // Unwrapping the optional, returning empty vec when None - .enumerate() - .filter(|(i, pc)| { - pc.is_some() - && current.get(*i).is_none_or(Option::is_none) - }) - .map(|(i, _)| i) - .collect(); + let branching_lanes = Self::find_branching_lanes( + current_snapshot, + previous_snapshot, + ); - let mut lanes = vec![None; current.len()]; + let merge_bridge = + second_parent_alias.and_then(|parent_alias| { + Self::calculate_merge_bridge( + current_snapshot, + commit_lane, + parent_alias, + ) + }); - let merge_bridge = if is_merge && parent_b_alias.is_some() { - current + let mut lanes: Vec> = + current_snapshot .iter() - .position(|c| { - c.as_ref().is_some_and(|chunk| { - chunk.parent_a == parent_b_alias + .enumerate() + .map(|(lane_index, chunk_option)| { + let chunk = chunk_option.as_ref()?; // Returns None early if the chunk is missing + + if commit_alias.is_some() + && chunk.alias == commit_alias + { + let connection = + Self::determine_commit_connection( + is_stash, + is_merge, + is_branch_tip, + ); + return Some(( + connection, + lane_color(lane_index), + )); + } + + Self::determine_passthrough_connection( + chunk, lane_index, head_alias, + ) + .map(|connection| { + (connection, lane_color(lane_index)) }) }) - .map(|t| (commit_lane.min(t), commit_lane.max(t))) - } else { - None - }; - - self.fill_lanes( - &mut lanes, - current, - alias, - head_id, - is_stash, - is_merge, - is_branch_tip, - ); + .collect(); Self::draw_merge_bridge( &mut lanes, merge_bridge, commit_lane, - current, - previous, + current_snapshot, + previous_snapshot, ); let branches = Self::draw_branching_lanes( @@ -400,70 +499,121 @@ impl GraphWalker { commit_lane, ); + let active_lane_count = + current_snapshot.iter().flatten().count(); + GraphRow { - lane_count: LaneIndex::from(current.iter().flatten().count()), + lane_count: LaneIndex::from(active_lane_count), commit_lane: LaneIndex::from(commit_lane), is_merge, is_branch_tip, is_stash, lanes, - merge_bridge: merge_bridge - .map(|(f, t)| (LaneIndex::from(f), LaneIndex::from(t))), + merge_bridge: merge_bridge.map(|(source, target)| { + (LaneIndex::from(source), LaneIndex::from(target)) + }), branches, } } - #[allow(clippy::too_many_arguments)] - fn fill_lanes( - &self, - lanes: &mut [Option<(ConnectionType, LaneIndex)>], - curr: &[Option], - alias: Option, - head_id: Option<&CommitId>, + /// Locates the primary lane for the current commit. + fn find_commit_lane( + current_snapshot: &[Option], + commit_alias: Option, + ) -> usize { + let Some(target_alias) = commit_alias else { + return 0; + }; + + current_snapshot + .iter() + .position(|chunk_option| { + chunk_option.as_ref().is_some_and(|chunk| { + chunk.alias == Some(target_alias) + }) + }) + .unwrap_or(0) + } + + /// Computes the span (min, max) between the commit's lane and its second parent's lane. + fn calculate_merge_bridge( + current_snapshot: &[Option], + commit_lane: usize, + second_parent_alias: AliasId, + ) -> Option<(usize, usize)> { + current_snapshot + .iter() + .position(|chunk_option| { + chunk_option.as_ref().is_some_and(|chunk| { + chunk.parent_a == Some(second_parent_alias) + }) + }) + .map(|target_lane| { + ( + commit_lane.min(target_lane), + commit_lane.max(target_lane), + ) + }) + } + + /// Identifies lanes that existed in the previous row but terminated before the current row. + fn find_branching_lanes( + current_snapshot: &[Option], + previous_snapshot: Option<&[Option]>, + ) -> Vec { + let Some(previous) = previous_snapshot else { + return Vec::new(); + }; + + previous + .iter() + .enumerate() + .filter(|(index, previous_chunk)| { + previous_chunk.is_some() + && current_snapshot + .get(*index) + .is_none_or(Option::is_none) + }) + .map(|(index, _)| index) + .collect() + } + + /// Determines the correct node type for the active commit lane. + fn determine_commit_connection( is_stash: bool, is_merge: bool, is_branch_tip: bool, - ) { - for (lane_idx, chunk_item) in curr.iter().enumerate() { - let Some(chunk) = chunk_item.as_ref() else { - continue; - }; - - if alias.is_some() && chunk.alias == alias { - let conn_type = - match (is_stash, is_merge, is_branch_tip) { - (true, _, _) => ConnectionType::CommitStash, - (_, true, _) => ConnectionType::CommitMerge, - (_, _, true) => ConnectionType::CommitBranch, - _ => ConnectionType::CommitNormal, - }; - - lanes[lane_idx] = - Some((conn_type, lane_color(lane_idx))); - } else { - let target_oid = - head_id.and_then(|h| self.oids.get(h)); - - let is_dotted = lane_idx == 0 - && target_oid.is_some() - && (chunk.parent_a == target_oid - || chunk.parent_b == target_oid); - - let is_orphan = chunk.parent_a.is_none() - && chunk.parent_b.is_none(); + ) -> ConnectionType { + match (is_stash, is_merge, is_branch_tip) { + (true, _, _) => ConnectionType::CommitStash, + (_, true, _) => ConnectionType::CommitMerge, + (_, _, true) => ConnectionType::CommitBranch, + _ => ConnectionType::CommitNormal, + } + } - if is_orphan { - continue; - } + /// Determines the correct vertical line style for non-commit passthrough lanes. + fn determine_passthrough_connection( + chunk: &Chunk, + lane_index: usize, + head_alias: Option, + ) -> Option { + let is_orphan = + chunk.parent_a.is_none() && chunk.parent_b.is_none(); + + if is_orphan { + return None; + } - let conn = if is_dotted { - ConnectionType::VerticalDotted - } else { - ConnectionType::Vertical - }; + let is_dotted = lane_index == 0 + && head_alias.is_some() + && (chunk.parent_a == head_alias + || chunk.parent_b == head_alias); - lanes[lane_idx] = Some((conn, lane_color(lane_idx))); - } + if is_dotted { + Some(ConnectionType::VerticalDotted) + } else { + Some(ConnectionType::Vertical) } } @@ -477,7 +627,11 @@ impl GraphWalker { color: LaneIndex, ) { for lane in lanes.iter_mut().take(to).skip(from + 1) { - overlay_cell(lane, Directions::LEFT | Directions::RIGHT, color); + overlay_cell( + lane, + Directions::LEFT | Directions::RIGHT, + color, + ); } } } From d5c44714b1260519937ecf73014b55d15a62a880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Tue, 23 Jun 2026 16:50:21 -0400 Subject: [PATCH 54/54] clippy fixes --- asyncgit/src/graph/mod.rs | 2 +- asyncgit/src/graph/walker.rs | 11 ++++++++--- src/app.rs | 2 +- src/components/commitlist.rs | 11 +++++++---- src/components/utils/statustree.rs | 2 +- src/main.rs | 1 - src/ui/reflow.rs | 4 ++-- 7 files changed, 20 insertions(+), 13 deletions(-) diff --git a/asyncgit/src/graph/mod.rs b/asyncgit/src/graph/mod.rs index 185962768e..a69e89c2a7 100644 --- a/asyncgit/src/graph/mod.rs +++ b/asyncgit/src/graph/mod.rs @@ -42,7 +42,7 @@ impl From for LaneIndex { impl From for usize { fn from(lane: LaneIndex) -> Self { - lane.0 as usize + lane.0 as Self } } diff --git a/asyncgit/src/graph/walker.rs b/asyncgit/src/graph/walker.rs index 01fca55b4c..d64774b1af 100644 --- a/asyncgit/src/graph/walker.rs +++ b/asyncgit/src/graph/walker.rs @@ -1,7 +1,9 @@ use super::buffer::Buffer; use super::chunk::{Chunk, Markers}; use super::oids::GraphOids; -use super::{AliasId, ConnectionType, GraphRow, LaneIndex, MAX_LANE_COLORS}; +use super::{ + AliasId, ConnectionType, GraphRow, LaneIndex, MAX_LANE_COLORS, +}; use crate::sync::CommitId; use core::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -339,7 +341,10 @@ impl GraphWalker { connection_color, ); - (LaneIndex::from(start_lane), LaneIndex::from(end_lane)) + ( + LaneIndex::from(start_lane), + LaneIndex::from(end_lane), + ) }) .collect() } @@ -579,7 +584,7 @@ impl GraphWalker { } /// Determines the correct node type for the active commit lane. - fn determine_commit_connection( + const fn determine_commit_connection( is_stash: bool, is_merge: bool, is_branch_tip: bool, diff --git a/src/app.rs b/src/app.rs index 5f3f427a98..435b5eedde 100644 --- a/src/app.rs +++ b/src/app.rs @@ -161,7 +161,7 @@ impl App { key_config: KeyConfig, ) -> Result { let repo = RefCell::new(cliargs.repo_path.clone()); - log::trace!("open repo at: {:?}", &repo); + log::trace!("open repo at: {repo:?}"); let repo_path_text = repo_work_dir(&repo.borrow()).unwrap_or_default(); diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 69b82e4dd7..2b524567cf 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -772,7 +772,7 @@ impl CommitList { // commit msg txt.push(Span::styled( - format!("{:message_width$}", &e.msg), + format!("{:message_width$}", e.msg), style_msg, )); } @@ -1354,12 +1354,15 @@ mod tests { #[test] fn test_build_graph_spans() { let row = GraphRow { - lane_count: 1, - commit_lane: 0, + lane_count: 1.into(), + commit_lane: 0.into(), is_merge: false, is_branch_tip: false, is_stash: false, - lanes: vec![Some((ConnectionType::CommitNormal, 0))], + lanes: vec![Some(( + ConnectionType::CommitNormal, + 0.into(), + ))], merge_bridge: None, branches: vec![], }; diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index 6147e57ead..8eb6cdd10b 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -437,7 +437,7 @@ impl StatusTree { if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed) { // we encountered an inner path that is still collapsed - inner_collapsed = Some(format!("{}/", &item_path)); + inner_collapsed = Some(format!("{item_path}/")); } if prefix diff --git a/src/main.rs b/src/main.rs index cb5fc69be6..db8b1dbf18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,6 @@ #![forbid(unsafe_code)] #![deny( - mismatched_lifetime_syntaxes, unused_imports, unused_must_use, dead_code, diff --git a/src/ui/reflow.rs b/src/ui/reflow.rs index 2238c44532..6947bed11a 100644 --- a/src/ui/reflow.rs +++ b/src/ui/reflow.rs @@ -44,7 +44,7 @@ impl<'a> LineComposer<'a> for WordWrapper<'a, '_> { return None; } std::mem::swap(&mut self.current_line, &mut self.next_line); - self.next_line.truncate(0); + self.next_line.clear(); let mut current_line_width = self .current_line @@ -175,7 +175,7 @@ impl<'a> LineComposer<'a> for LineTruncator<'a, '_> { return None; } - self.current_line.truncate(0); + self.current_line.clear(); let mut current_line_width = 0; let mut skip_rest = false;