const MAX_LANES: usize = 24;
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct GraphRow {
pub node_col: usize,
pub top: Vec<(usize, usize)>,
pub bottom: Vec<(usize, usize)>,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Graph {
pub rows: Vec<GraphRow>,
pub lane_count: usize,
}
pub fn compute_graph(commits: &[(String, Vec<String>)]) -> Graph {
let mut lanes: Vec<Option<String>> = Vec::new();
let mut rows = Vec::with_capacity(commits.len());
let mut lane_count = 1usize;
for (id, parents) in commits {
let incoming: Vec<usize> = lanes
.iter()
.enumerate()
.filter_map(|(i, l)| (l.as_deref() == Some(id.as_str())).then_some(i))
.collect();
let node_col = match incoming.first() {
Some(&first) => first,
None => free_slot(&mut lanes), };
let mut top = Vec::new();
for (i, lane) in lanes.iter().enumerate() {
if lane.is_none() {
continue;
}
if incoming.contains(&i) {
top.push((i, node_col));
} else {
top.push((i, i));
}
}
for &i in &incoming {
lanes[i] = None;
}
if node_col < lanes.len() {
lanes[node_col] = None;
}
let mut parent_cols = Vec::new();
for (k, parent) in parents.iter().enumerate() {
let col = if let Some(existing) = lanes
.iter()
.position(|l| l.as_deref() == Some(parent.as_str()))
{
existing
} else if k == 0 {
node_col
} else {
free_slot(&mut lanes)
};
ensure_len(&mut lanes, col);
lanes[col] = Some(parent.clone());
if !parent_cols.contains(&col) {
parent_cols.push(col);
}
}
let mut bottom = Vec::new();
for (j, lane) in lanes.iter().enumerate() {
if lane.is_none() {
continue;
}
if parent_cols.contains(&j) {
bottom.push((node_col, j));
} else {
bottom.push((j, j));
}
}
while matches!(lanes.last(), Some(None)) {
lanes.pop();
}
for &(a, b) in top.iter().chain(bottom.iter()) {
lane_count = lane_count.max(a + 1).max(b + 1);
}
lane_count = lane_count.max(node_col + 1).max(lanes.len());
rows.push(GraphRow {
node_col,
top,
bottom,
});
}
Graph {
rows,
lane_count: lane_count.clamp(1, MAX_LANES),
}
}
fn free_slot(lanes: &mut Vec<Option<String>>) -> usize {
match lanes.iter().position(|l| l.is_none()) {
Some(i) => i,
None => {
lanes.push(None);
lanes.len() - 1
}
}
}
fn ensure_len(lanes: &mut Vec<Option<String>>, idx: usize) {
if idx >= lanes.len() {
lanes.resize(idx + 1, None);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(id: &str, parents: &[&str]) -> (String, Vec<String>) {
(
id.to_string(),
parents.iter().map(|p| p.to_string()).collect(),
)
}
#[test]
fn linear_history_stays_in_one_lane() {
let commits = [c("A", &["B"]), c("B", &["C"]), c("C", &[])];
let g = compute_graph(&commits);
assert_eq!(g.lane_count, 1);
assert!(g.rows.iter().all(|r| r.node_col == 0));
assert!(g.rows[0].top.is_empty());
assert_eq!(g.rows[0].bottom, vec![(0, 0)]);
assert!(g.rows[2].bottom.is_empty());
}
#[test]
fn branch_then_merge_uses_two_lanes() {
let commits = [
c("A", &["B", "D"]),
c("B", &["C"]),
c("D", &["C"]),
c("C", &["E"]),
c("E", &[]),
];
let g = compute_graph(&commits);
assert_eq!(g.lane_count, 2, "branch should occupy a second lane");
assert_eq!(g.rows[0].node_col, 0);
assert_eq!(g.rows[0].bottom, vec![(0, 0), (0, 1)]);
assert_eq!(g.rows[2].node_col, 1);
assert_eq!(g.rows[2].bottom, vec![(1, 0)]);
assert_eq!(g.rows[4].node_col, 0);
assert!(g.rows[4].bottom.is_empty());
}
}