gitkraft_core/features/graph/
ops.rs1use std::collections::HashMap;
4
5use super::types::{GraphEdge, GraphRow};
6use crate::features::commits::CommitInfo;
7
8pub fn build_graph(commits: &[CommitInfo]) -> Vec<GraphRow> {
17 if commits.is_empty() {
18 return Vec::new();
19 }
20
21 let mut lanes: Vec<Option<String>> = Vec::new();
22 let mut color_map: HashMap<String, usize> = HashMap::new();
23 let mut next_color: usize = 0;
24 let mut rows: Vec<GraphRow> = Vec::with_capacity(commits.len());
25
26 for commit in commits {
27 let oid = &commit.oid;
28
29 let node_col = lanes
31 .iter()
32 .position(|l| l.as_deref() == Some(oid))
33 .unwrap_or_else(|| {
34 lanes.push(Some(oid.clone()));
35 lanes.len() - 1
36 });
37
38 let node_color = *color_map.entry(oid.clone()).or_insert_with(|| {
40 let c = next_color;
41 next_color += 1;
42 c
43 });
44
45 let mut edges: Vec<GraphEdge> = Vec::new();
47
48 for (col, lane) in lanes.iter().enumerate() {
50 if col == node_col {
51 continue;
52 }
53 if let Some(ref target_oid) = lane {
54 let lane_color = *color_map.entry(target_oid.clone()).or_insert_with(|| {
55 let c = next_color;
56 next_color += 1;
57 c
58 });
59 edges.push(GraphEdge {
60 from_column: col,
61 to_column: col,
62 color_index: lane_color,
63 });
64 }
65 }
66
67 let parents = &commit.parent_ids;
69
70 if parents.is_empty() {
71 lanes[node_col] = None;
73 } else {
74 let first_parent = &parents[0];
76 lanes[node_col] = Some(first_parent.clone());
77
78 let first_parent_color = *color_map.entry(first_parent.clone()).or_insert_with(|| {
79 let c = next_color;
80 next_color += 1;
81 c
82 });
83
84 edges.push(GraphEdge {
85 from_column: node_col,
86 to_column: node_col,
87 color_index: first_parent_color,
88 });
89
90 for parent_oid in &parents[1..] {
92 let existing = lanes
93 .iter()
94 .position(|l| l.as_deref() == Some(parent_oid.as_str()));
95
96 let target_col = if let Some(col) = existing {
97 col
98 } else if let Some(free) = lanes.iter().position(|l| l.is_none()) {
99 lanes[free] = Some(parent_oid.clone());
100 free
101 } else {
102 lanes.push(Some(parent_oid.clone()));
103 lanes.len() - 1
104 };
105
106 let parent_color = *color_map.entry(parent_oid.clone()).or_insert_with(|| {
107 let c = next_color;
108 next_color += 1;
109 c
110 });
111
112 edges.push(GraphEdge {
113 from_column: node_col,
114 to_column: target_col,
115 color_index: parent_color,
116 });
117 }
118 }
119
120 while lanes.last() == Some(&None) {
122 lanes.pop();
123 }
124
125 let width = lanes.len().max(1);
126
127 rows.push(GraphRow {
128 width,
129 node_column: node_col,
130 node_color,
131 edges,
132 });
133 }
134
135 rows
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use chrono::Utc;
142
143 fn make_commit(oid: &str, parents: &[&str]) -> CommitInfo {
144 CommitInfo {
145 oid: oid.to_string(),
146 short_oid: oid[..7.min(oid.len())].to_string(),
147 summary: format!("commit {oid}"),
148 message: format!("commit {oid}"),
149 author_name: "Test".to_string(),
150 author_email: "test@test.com".to_string(),
151 time: Utc::now(),
152 parent_ids: parents.iter().map(|s| s.to_string()).collect(),
153 refs: Vec::new(),
154 }
155 }
156
157 #[test]
158 fn empty_commits_returns_empty_graph() {
159 let rows = build_graph(&[]);
160 assert!(rows.is_empty());
161 }
162
163 #[test]
164 fn single_commit_no_parents() {
165 let commits = vec![make_commit("aaa0000", &[])];
166 let rows = build_graph(&commits);
167 assert_eq!(rows.len(), 1);
168 assert_eq!(rows[0].node_column, 0);
169 assert!(rows[0].edges.is_empty());
170 }
171
172 #[test]
173 fn linear_history() {
174 let commits = vec![
175 make_commit("ccc0000", &["bbb0000"]),
176 make_commit("bbb0000", &["aaa0000"]),
177 make_commit("aaa0000", &[]),
178 ];
179 let rows = build_graph(&commits);
180 assert_eq!(rows.len(), 3);
181 for row in &rows {
182 assert_eq!(row.node_column, 0);
183 }
184 }
185
186 #[test]
187 fn branch_uses_separate_lane() {
188 let commits = vec![
189 make_commit("ddd0000", &["bbb0000", "ccc0000"]),
190 make_commit("ccc0000", &["aaa0000"]),
191 make_commit("bbb0000", &["aaa0000"]),
192 make_commit("aaa0000", &[]),
193 ];
194 let rows = build_graph(&commits);
195 assert_eq!(rows.len(), 4);
196 let merge_row = &rows[0];
197 assert!(merge_row.edges.len() >= 2);
198 }
199
200 #[test]
201 fn graph_width_is_at_least_node_column_plus_one() {
202 let commits = vec![
203 make_commit("ccc0000", &["bbb0000"]),
204 make_commit("bbb0000", &["aaa0000"]),
205 make_commit("aaa0000", &[]),
206 ];
207 let rows = build_graph(&commits);
208 for row in &rows {
209 assert!(row.width > row.node_column);
210 }
211 }
212}