1const MAX_LANES: usize = 24;
16
17#[derive(Clone, Debug, PartialEq, Eq, Default)]
21pub struct GraphRow {
22 pub node_col: usize,
24 pub top: Vec<(usize, usize)>,
26 pub bottom: Vec<(usize, usize)>,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Default)]
32pub struct Graph {
33 pub rows: Vec<GraphRow>,
34 pub lane_count: usize,
35}
36
37pub fn compute_graph(commits: &[(String, Vec<String>)]) -> Graph {
40 let mut lanes: Vec<Option<String>> = Vec::new();
42 let mut rows = Vec::with_capacity(commits.len());
43 let mut lane_count = 1usize;
44
45 for (id, parents) in commits {
46 let incoming: Vec<usize> = lanes
48 .iter()
49 .enumerate()
50 .filter_map(|(i, l)| (l.as_deref() == Some(id.as_str())).then_some(i))
51 .collect();
52
53 let node_col = match incoming.first() {
54 Some(&first) => first,
55 None => free_slot(&mut lanes), };
57
58 let mut top = Vec::new();
61 for (i, lane) in lanes.iter().enumerate() {
62 if lane.is_none() {
63 continue;
64 }
65 if incoming.contains(&i) {
66 top.push((i, node_col));
67 } else {
68 top.push((i, i));
69 }
70 }
71
72 for &i in &incoming {
74 lanes[i] = None;
75 }
76 if node_col < lanes.len() {
77 lanes[node_col] = None;
78 }
79
80 let mut parent_cols = Vec::new();
84 for (k, parent) in parents.iter().enumerate() {
85 let col = if let Some(existing) = lanes
86 .iter()
87 .position(|l| l.as_deref() == Some(parent.as_str()))
88 {
89 existing
90 } else if k == 0 {
91 node_col
92 } else {
93 free_slot(&mut lanes)
94 };
95 ensure_len(&mut lanes, col);
96 lanes[col] = Some(parent.clone());
97 if !parent_cols.contains(&col) {
98 parent_cols.push(col);
99 }
100 }
101
102 let mut bottom = Vec::new();
105 for (j, lane) in lanes.iter().enumerate() {
106 if lane.is_none() {
107 continue;
108 }
109 if parent_cols.contains(&j) {
110 bottom.push((node_col, j));
111 } else {
112 bottom.push((j, j));
113 }
114 }
115
116 while matches!(lanes.last(), Some(None)) {
117 lanes.pop();
118 }
119
120 for &(a, b) in top.iter().chain(bottom.iter()) {
121 lane_count = lane_count.max(a + 1).max(b + 1);
122 }
123 lane_count = lane_count.max(node_col + 1).max(lanes.len());
124
125 rows.push(GraphRow {
126 node_col,
127 top,
128 bottom,
129 });
130 }
131
132 Graph {
133 rows,
134 lane_count: lane_count.clamp(1, MAX_LANES),
135 }
136}
137
138fn free_slot(lanes: &mut Vec<Option<String>>) -> usize {
139 match lanes.iter().position(|l| l.is_none()) {
140 Some(i) => i,
141 None => {
142 lanes.push(None);
143 lanes.len() - 1
144 }
145 }
146}
147
148fn ensure_len(lanes: &mut Vec<Option<String>>, idx: usize) {
149 if idx >= lanes.len() {
150 lanes.resize(idx + 1, None);
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 fn c(id: &str, parents: &[&str]) -> (String, Vec<String>) {
159 (
160 id.to_string(),
161 parents.iter().map(|p| p.to_string()).collect(),
162 )
163 }
164
165 #[test]
166 fn linear_history_stays_in_one_lane() {
167 let commits = [c("A", &["B"]), c("B", &["C"]), c("C", &[])];
168 let g = compute_graph(&commits);
169 assert_eq!(g.lane_count, 1);
170 assert!(g.rows.iter().all(|r| r.node_col == 0));
171 assert!(g.rows[0].top.is_empty());
173 assert_eq!(g.rows[0].bottom, vec![(0, 0)]);
174 assert!(g.rows[2].bottom.is_empty());
175 }
176
177 #[test]
178 fn branch_then_merge_uses_two_lanes() {
179 let commits = [
181 c("A", &["B", "D"]),
182 c("B", &["C"]),
183 c("D", &["C"]),
184 c("C", &["E"]),
185 c("E", &[]),
186 ];
187 let g = compute_graph(&commits);
188 assert_eq!(g.lane_count, 2, "branch should occupy a second lane");
189 assert_eq!(g.rows[0].node_col, 0);
191 assert_eq!(g.rows[0].bottom, vec![(0, 0), (0, 1)]);
192 assert_eq!(g.rows[2].node_col, 1);
194 assert_eq!(g.rows[2].bottom, vec![(1, 0)]);
195 assert_eq!(g.rows[4].node_col, 0);
197 assert!(g.rows[4].bottom.is_empty());
198 }
199}