use super::theme::Theme;
use crate::worktree::CommitRow;
use git2::Oid;
use ratatui::{
style::{Modifier, Style},
text::Span,
};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PipeKind {
Terminates,
Starts,
Continues,
}
#[derive(Debug, Clone)]
pub struct Pipe {
pub from_pos: i16,
pub to_pos: i16,
pub from_hash: Oid,
pub to_hash: Oid,
pub kind: PipeKind,
}
impl Pipe {
#[inline]
fn left(&self) -> i16 {
self.from_pos.min(self.to_pos)
}
#[inline]
fn right(&self) -> i16 {
self.from_pos.max(self.to_pos)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CellType {
Connection,
Commit,
Merge,
}
#[derive(Debug, Clone)]
struct Cell {
up: bool,
down: bool,
left: bool,
right: bool,
cell_type: CellType,
}
impl Cell {
fn new_connection() -> Self {
Self {
up: false,
down: false,
left: false,
right: false,
cell_type: CellType::Connection,
}
}
fn set_up(&mut self) {
self.up = true;
}
fn set_down(&mut self) {
self.down = true;
}
fn set_left(&mut self) {
self.left = true;
}
fn set_right(&mut self) {
self.right = true;
}
fn set_type(&mut self, t: CellType) {
self.cell_type = t;
}
}
pub fn box_drawing_chars(up: bool, down: bool, left: bool, right: bool) -> (char, char) {
match (up, down, left, right) {
(true, true, true, true) => ('│', '─'),
(true, true, true, false) => ('│', ' '),
(true, true, false, true) => ('│', '─'),
(true, true, false, false) => ('│', ' '),
(true, false, true, true) => ('┴', '─'),
(true, false, true, false) => ('╯', ' '),
(true, false, false, true) => ('╰', '─'),
(true, false, false, false) => ('╵', ' '),
(false, true, true, true) => ('┬', '─'),
(false, true, true, false) => ('╮', ' '),
(false, true, false, true) => ('╭', '─'),
(false, true, false, false) => ('╷', ' '),
(false, false, true, true) => ('─', '─'),
(false, false, true, false) => ('─', ' '),
(false, false, false, true) => ('╶', '─'),
(false, false, false, false) => (' ', ' '),
}
}
fn start_hash() -> Oid {
Oid::ZERO_SHA1
}
pub fn build_pipe_sets(commits: &[CommitRow]) -> Vec<Vec<Pipe>> {
if commits.is_empty() {
return Vec::new();
}
let mut pipes = vec![Pipe {
from_pos: 0,
to_pos: 0,
from_hash: start_hash(),
to_hash: commits[0].hash,
kind: PipeKind::Starts,
}];
let mut out = Vec::with_capacity(commits.len());
for commit in commits {
pipes = get_next_pipes(&pipes, commit);
out.push(pipes.clone());
}
out
}
fn get_next_pipes(prev_pipes: &[Pipe], commit: &CommitRow) -> Vec<Pipe> {
let max_pos: i16 = prev_pipes.iter().map(|p| p.to_pos).max().unwrap_or(0);
let current_pipes: Vec<&Pipe> = prev_pipes.iter().filter(|p| p.kind != PipeKind::Terminates).collect();
let mut pos: i16 = max_pos + 1;
for pipe in ¤t_pipes {
if pipe.to_hash == commit.hash {
pos = pipe.to_pos;
break;
}
}
let mut new_pipes: Vec<Pipe> = Vec::with_capacity(current_pipes.len() + commit.parents.len());
let first_parent = commit.parents.first().copied().unwrap_or_else(start_hash);
new_pipes.push(Pipe {
from_pos: pos,
to_pos: pos,
from_hash: commit.hash,
to_hash: first_parent,
kind: PipeKind::Starts,
});
let mut taken_spots: HashSet<i16> = HashSet::new();
let mut traversed_spots: HashSet<i16> = HashSet::new();
let mut traversed_spots_for_continuing: HashSet<i16> = HashSet::new();
for pipe in ¤t_pipes {
if pipe.to_hash != commit.hash {
traversed_spots_for_continuing.insert(pipe.to_pos);
}
}
fn next_free(spots: &HashSet<i16>) -> i16 {
let mut i: i16 = 0;
while spots.contains(&i) {
i += 1;
}
i
}
fn next_free_for_new(taken: &HashSet<i16>, traversed_for_continuing: &HashSet<i16>) -> i16 {
let mut i: i16 = 0;
while taken.contains(&i) || traversed_for_continuing.contains(&i) {
i += 1;
}
i
}
fn traverse(taken: &mut HashSet<i16>, traversed: &mut HashSet<i16>, from: i16, to: i16) {
let (l, r) = if from <= to { (from, to) } else { (to, from) };
for i in l..=r {
traversed.insert(i);
}
taken.insert(to);
}
for pipe in ¤t_pipes {
if pipe.to_hash == commit.hash {
new_pipes.push(Pipe {
from_pos: pipe.to_pos,
to_pos: pos,
from_hash: pipe.from_hash,
to_hash: pipe.to_hash,
kind: PipeKind::Terminates,
});
traverse(&mut taken_spots, &mut traversed_spots, pipe.to_pos, pos);
} else if pipe.to_pos < pos {
let avail = next_free(&traversed_spots);
new_pipes.push(Pipe {
from_pos: pipe.to_pos,
to_pos: avail,
from_hash: pipe.from_hash,
to_hash: pipe.to_hash,
kind: PipeKind::Continues,
});
traverse(&mut taken_spots, &mut traversed_spots, pipe.to_pos, avail);
}
}
if commit.parents.len() >= 2 {
for parent in commit.parents.iter().skip(1) {
let avail = next_free_for_new(&taken_spots, &traversed_spots_for_continuing);
new_pipes.push(Pipe {
from_pos: pos,
to_pos: avail,
from_hash: commit.hash,
to_hash: *parent,
kind: PipeKind::Starts,
});
taken_spots.insert(avail);
}
}
for pipe in ¤t_pipes {
if pipe.to_hash != commit.hash && pipe.to_pos > pos {
let mut last = pipe.to_pos;
let mut i = pipe.to_pos;
while i > pos {
i -= 1;
if taken_spots.contains(&i) || traversed_spots.contains(&i) {
break;
}
last = i;
}
new_pipes.push(Pipe {
from_pos: pipe.to_pos,
to_pos: last,
from_hash: pipe.from_hash,
to_hash: pipe.to_hash,
kind: PipeKind::Continues,
});
traverse(&mut taken_spots, &mut traversed_spots, pipe.to_pos, last);
}
}
new_pipes.sort_by(|a, b| {
if a.to_pos == b.to_pos {
a.kind.cmp(&b.kind)
} else {
a.to_pos.cmp(&b.to_pos)
}
});
new_pipes
}
pub fn render_pipe_set(pipes: &[Pipe], theme: &Theme) -> Vec<Span<'static>> {
let mut max_pos: i16 = 0;
let mut commit_pos: i16 = 0;
let mut start_count: usize = 0;
for pipe in pipes {
if pipe.kind == PipeKind::Starts {
start_count += 1;
commit_pos = pipe.from_pos;
} else if pipe.kind == PipeKind::Terminates {
commit_pos = pipe.to_pos;
}
if pipe.right() > max_pos {
max_pos = pipe.right();
}
}
let is_merge = start_count > 1;
let mut cells: Vec<Cell> = (0..=max_pos).map(|_| Cell::new_connection()).collect();
for pipe in pipes {
if pipe.kind == PipeKind::Starts {
apply_pipe(&mut cells, pipe);
}
}
for pipe in pipes {
if pipe.kind != PipeKind::Starts
&& !(pipe.kind == PipeKind::Terminates && pipe.from_pos == commit_pos && pipe.to_pos == commit_pos)
{
apply_pipe(&mut cells, pipe);
}
}
if let Some(c) = cells.get_mut(commit_pos as usize) {
c.set_type(if is_merge { CellType::Merge } else { CellType::Commit });
}
let connector_style = Style::default().fg(theme.branch);
let node_style = Style::default().fg(theme.branch).add_modifier(Modifier::BOLD);
let mut out: Vec<Span<'static>> = Vec::with_capacity(cells.len() * 2);
for cell in &cells {
let (glyph, filler) = box_drawing_chars(cell.up, cell.down, cell.left, cell.right);
let render_glyph: String = match cell.cell_type {
CellType::Connection => glyph.to_string(),
CellType::Commit => '○'.to_string(),
CellType::Merge => '◎'.to_string(),
};
let style = if matches!(cell.cell_type, CellType::Commit | CellType::Merge) {
node_style
} else {
connector_style
};
out.push(Span::styled(render_glyph, style));
out.push(Span::styled(filler.to_string(), connector_style));
}
out
}
fn apply_pipe(cells: &mut [Cell], pipe: &Pipe) {
let left = pipe.left();
let right = pipe.right();
if left != right {
for i in (left + 1)..right {
if let Some(c) = cells.get_mut(i as usize) {
c.set_left();
c.set_right();
}
}
if let Some(c) = cells.get_mut(left as usize) {
c.set_right();
}
if let Some(c) = cells.get_mut(right as usize) {
c.set_left();
}
}
match pipe.kind {
PipeKind::Starts | PipeKind::Continues => {
if let Some(c) = cells.get_mut(pipe.to_pos as usize) {
c.set_down();
}
}
PipeKind::Terminates => {}
}
match pipe.kind {
PipeKind::Terminates | PipeKind::Continues => {
if let Some(c) = cells.get_mut(pipe.from_pos as usize) {
c.set_up();
}
}
PipeKind::Starts => {}
}
}
pub fn render_commits(commits: &[CommitRow], theme: &Theme) -> Vec<Vec<Span<'static>>> {
build_pipe_sets(commits)
.into_iter()
.map(|pipes| render_pipe_set(&pipes, theme))
.collect()
}
#[doc(hidden)]
pub fn test_row(hash: &str, parents: &[&str]) -> CommitRow {
fn oid(label: &str) -> Oid {
if label.len() == 40 {
return Oid::from_str(label).expect("test hash must be valid hex");
}
let padded = format!("{:0>40}", label);
Oid::from_str(&padded).expect("test hash label must be valid hex")
}
CommitRow {
hash: oid(hash),
author: String::new(),
parents: parents.iter().map(|s| oid(s)).collect(),
subject: String::new(),
}
}