1use rustc_hash::FxHashMap;
13
14use crate::void_backend::{CommitCid, VoidCommit};
15
16type CommitPosMap<'a> = FxHashMap<&'a CommitCid, (usize, usize)>;
18
19#[derive(Debug)]
21pub struct Graph<'a> {
22 pub commits: &'a [VoidCommit],
24 pub commit_pos_map: CommitPosMap<'a>,
26 pub edges: Vec<Vec<Edge>>,
28 pub max_pos_x: usize,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct Edge {
35 pub edge_type: EdgeType,
37 pub pos_x: usize,
39 pub associated_line_pos_x: usize,
41}
42
43impl Edge {
44 pub fn new(edge_type: EdgeType, pos_x: usize, line_pos_x: usize) -> Self {
46 Self {
47 edge_type,
48 pos_x,
49 associated_line_pos_x: line_pos_x,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
56pub enum EdgeType {
57 Vertical,
59 Horizontal,
61 Up,
63 Down,
65 Left,
67 Right,
69 RightTop,
71 RightBottom,
73 LeftTop,
75 LeftBottom,
77}
78
79impl EdgeType {
80 pub fn is_vertically_related(&self) -> bool {
82 matches!(self, EdgeType::Vertical | EdgeType::Up | EdgeType::Down)
83 }
84}
85
86pub trait GraphContext {
91 fn children(&self, cid: &CommitCid) -> Vec<&CommitCid>;
93
94 fn parents(&self, cid: &CommitCid) -> Vec<&CommitCid>;
96
97 fn commit(&self, cid: &CommitCid) -> Option<&VoidCommit>;
99}
100
101pub struct SimpleGraphContext<'a> {
103 commits: &'a [VoidCommit],
105 cid_to_index: FxHashMap<&'a CommitCid, usize>,
107 children_map: FxHashMap<&'a CommitCid, Vec<&'a CommitCid>>,
109}
110
111impl<'a> SimpleGraphContext<'a> {
112 pub fn new(commits: &'a [VoidCommit]) -> Self {
114 let mut cid_to_index = FxHashMap::default();
115 let mut children_map: FxHashMap<&CommitCid, Vec<&CommitCid>> = FxHashMap::default();
116
117 for (i, commit) in commits.iter().enumerate() {
119 cid_to_index.insert(&commit.cid, i);
120 }
121
122 for commit in commits {
124 for parent_cid in &commit.parents {
125 children_map
126 .entry(parent_cid)
127 .or_default()
128 .push(&commit.cid);
129 }
130 }
131
132 Self {
133 commits,
134 cid_to_index,
135 children_map,
136 }
137 }
138}
139
140impl<'a> GraphContext for SimpleGraphContext<'a> {
141 fn children(&self, cid: &CommitCid) -> Vec<&CommitCid> {
142 self.children_map.get(cid).cloned().unwrap_or_default()
143 }
144
145 fn parents(&self, cid: &CommitCid) -> Vec<&CommitCid> {
146 self.cid_to_index
147 .get(cid)
148 .and_then(|&i| self.commits.get(i))
149 .map(|c| c.parents.iter().collect())
150 .unwrap_or_default()
151 }
152
153 fn commit(&self, cid: &CommitCid) -> Option<&VoidCommit> {
154 self.cid_to_index
155 .get(cid)
156 .and_then(|&i| self.commits.get(i))
157 }
158}
159
160pub fn calc_graph<'a>(commits: &'a [VoidCommit], ctx: &'a impl GraphContext) -> Graph<'a> {
162 let commit_pos_map = calc_commit_positions(commits, ctx);
163 let (edges, max_pos_x) = calc_edges(&commit_pos_map, commits, ctx);
164
165 Graph {
166 commits,
167 commit_pos_map,
168 edges,
169 max_pos_x,
170 }
171}
172
173#[derive(Debug)]
177pub struct OwnedGraph {
178 pub commit_pos_map: FxHashMap<CommitCid, (usize, usize)>,
180 pub edges: Vec<Vec<Edge>>,
182 pub max_pos_x: usize,
184}
185
186pub fn calc_graph_owned(commits: &[VoidCommit]) -> OwnedGraph {
191 let ctx = SimpleGraphContext::new(commits);
192 let graph = calc_graph(commits, &ctx);
193
194 OwnedGraph {
195 commit_pos_map: graph
196 .commit_pos_map
197 .into_iter()
198 .map(|(k, v)| (k.clone(), v))
199 .collect(),
200 edges: graph.edges,
201 max_pos_x: graph.max_pos_x,
202 }
203}
204
205fn calc_commit_positions<'a>(
207 commits: &'a [VoidCommit],
208 ctx: &impl GraphContext,
209) -> CommitPosMap<'a> {
210 let mut commit_pos_map: CommitPosMap = FxHashMap::default();
211 let mut commit_line_state: Vec<Option<&CommitCid>> = Vec::new();
212
213 for (pos_y, commit) in commits.iter().enumerate() {
214 let filtered_children = filtered_children_cid(commit, ctx);
215 if filtered_children.is_empty() {
216 let pos_x = get_first_vacant_line(&commit_line_state);
218 add_commit_line(commit, &mut commit_line_state, pos_x);
219 commit_pos_map.insert(&commit.cid, (pos_x, pos_y));
220 } else {
221 let pos_x = update_commit_line(commit, &mut commit_line_state, &filtered_children);
223 commit_pos_map.insert(&commit.cid, (pos_x, pos_y));
224 }
225 }
226
227 commit_pos_map
228}
229
230fn filtered_children_cid<'a>(
232 commit: &'a VoidCommit,
233 ctx: &'a impl GraphContext,
234) -> Vec<&'a CommitCid> {
235 ctx.children(&commit.cid)
236 .into_iter()
237 .filter(|child_cid| {
238 let child_parents = ctx.parents(child_cid);
239 !child_parents.is_empty() && *child_parents[0] == commit.cid
240 })
241 .collect()
242}
243
244fn get_first_vacant_line(commit_line_state: &[Option<&CommitCid>]) -> usize {
246 commit_line_state
247 .iter()
248 .position(|c| c.is_none())
249 .unwrap_or(commit_line_state.len())
250}
251
252fn add_commit_line<'a>(
254 commit: &'a VoidCommit,
255 commit_line_state: &mut Vec<Option<&'a CommitCid>>,
256 pos_x: usize,
257) {
258 if commit_line_state.len() <= pos_x {
259 commit_line_state.push(Some(&commit.cid));
260 } else {
261 commit_line_state[pos_x] = Some(&commit.cid);
262 }
263}
264
265fn update_commit_line<'a>(
267 commit: &'a VoidCommit,
268 commit_line_state: &mut [Option<&'a CommitCid>],
269 target_commit_cids: &[&CommitCid],
270) -> usize {
271 if commit_line_state.is_empty() {
272 return 0;
273 }
274
275 let mut min_pos_x = commit_line_state.len().saturating_sub(1);
276
277 for target_cid in target_commit_cids {
278 for (pos_x, commit_cid) in commit_line_state.iter().enumerate() {
279 if let Some(cid) = commit_cid {
280 if *cid == *target_cid {
281 commit_line_state[pos_x] = None;
282 if min_pos_x > pos_x {
283 min_pos_x = pos_x;
284 }
285 break;
286 }
287 }
288 }
289 }
290
291 commit_line_state[min_pos_x] = Some(&commit.cid);
292 min_pos_x
293}
294
295#[derive(Debug, Clone)]
297struct WrappedEdge<'a> {
298 edge: Edge,
299 edge_parent_cid: &'a CommitCid,
300}
301
302impl<'a> WrappedEdge<'a> {
303 fn new(
304 edge_type: EdgeType,
305 pos_x: usize,
306 line_pos_x: usize,
307 edge_parent_cid: &'a CommitCid,
308 ) -> Self {
309 Self {
310 edge: Edge::new(edge_type, pos_x, line_pos_x),
311 edge_parent_cid,
312 }
313 }
314}
315
316fn calc_edges<'a>(
318 commit_pos_map: &CommitPosMap<'a>,
319 commits: &'a [VoidCommit],
320 ctx: &impl GraphContext,
321) -> (Vec<Vec<Edge>>, usize) {
322 let mut max_pos_x = 0;
323 let mut edges: Vec<Vec<WrappedEdge<'a>>> = vec![vec![]; commits.len()];
324
325 for commit in commits {
327 let (pos_x, pos_y) = commit_pos_map[&commit.cid];
328 let cid = &commit.cid;
329
330 for child_cid in ctx.children(cid) {
331 let (child_pos_x, child_pos_y) = commit_pos_map[child_cid];
332
333 if pos_x == child_pos_x {
334 draw_vertical_edge(&mut edges, pos_x, pos_y, child_pos_y, cid);
336 } else {
337 let child_commit = ctx.commit(child_cid);
338 let child_first_parent = child_commit.and_then(|c| c.parents.first());
339
340 if child_first_parent == Some(cid) {
341 draw_branch_edge(
343 &mut edges,
344 pos_x,
345 pos_y,
346 child_pos_x,
347 child_pos_y,
348 cid,
349 );
350 }
351 }
353 }
354
355 if max_pos_x < pos_x {
356 max_pos_x = pos_x;
357 }
358
359 if let Some(first_parent) = commit.parents.first() {
361 if ctx.commit(first_parent).is_none() {
362 edges[pos_y].push(WrappedEdge::new(EdgeType::Down, pos_x, pos_x, cid));
363 ((pos_y + 1)..commits.len()).for_each(|y| {
364 edges[y].push(WrappedEdge::new(EdgeType::Vertical, pos_x, pos_x, cid));
365 });
366 }
367 }
368 }
369
370 for commit in commits {
372 let (pos_x, pos_y) = commit_pos_map[&commit.cid];
373 let cid = &commit.cid;
374
375 for child_cid in ctx.children(cid) {
376 let (child_pos_x, child_pos_y) = commit_pos_map[child_cid];
377
378 if pos_x != child_pos_x {
379 let child_commit = ctx.commit(child_cid);
380 let child_first_parent = child_commit.and_then(|c| c.parents.first());
381
382 if child_first_parent != Some(cid) {
383 draw_merge_edge(
385 &mut edges,
386 commit_pos_map,
387 commits,
388 pos_x,
389 pos_y,
390 child_pos_x,
391 child_pos_y,
392 cid,
393 &mut max_pos_x,
394 );
395 }
396 }
397 }
398
399 if max_pos_x < pos_x {
400 max_pos_x = pos_x;
401 }
402 }
403
404 let edges: Vec<Vec<Edge>> = edges
406 .into_iter()
407 .map(|es| {
408 let mut es: Vec<Edge> = es.into_iter().map(|e| e.edge).collect();
409 es.sort_by_key(|e| (e.associated_line_pos_x, e.pos_x, e.edge_type));
410 es.dedup();
411 es
412 })
413 .collect();
414
415 (edges, max_pos_x)
416}
417
418fn draw_vertical_edge<'a>(
420 edges: &mut [Vec<WrappedEdge<'a>>],
421 pos_x: usize,
422 pos_y: usize,
423 child_pos_y: usize,
424 cid: &'a CommitCid,
425) {
426 edges[pos_y].push(WrappedEdge::new(EdgeType::Up, pos_x, pos_x, cid));
427 for y in ((child_pos_y + 1)..pos_y).rev() {
428 edges[y].push(WrappedEdge::new(EdgeType::Vertical, pos_x, pos_x, cid));
429 }
430 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Down, pos_x, pos_x, cid));
431}
432
433fn draw_branch_edge<'a>(
435 edges: &mut [Vec<WrappedEdge<'a>>],
436 pos_x: usize,
437 pos_y: usize,
438 child_pos_x: usize,
439 child_pos_y: usize,
440 cid: &'a CommitCid,
441) {
442 if pos_x < child_pos_x {
443 edges[pos_y].push(WrappedEdge::new(EdgeType::Right, pos_x, child_pos_x, cid));
445 for x in (pos_x + 1)..child_pos_x {
446 edges[pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, child_pos_x, cid));
447 }
448 edges[pos_y].push(WrappedEdge::new(
449 EdgeType::RightBottom,
450 child_pos_x,
451 child_pos_x,
452 cid,
453 ));
454 } else {
455 edges[pos_y].push(WrappedEdge::new(EdgeType::Left, pos_x, child_pos_x, cid));
457 for x in (child_pos_x + 1)..pos_x {
458 edges[pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, child_pos_x, cid));
459 }
460 edges[pos_y].push(WrappedEdge::new(
461 EdgeType::LeftBottom,
462 child_pos_x,
463 child_pos_x,
464 cid,
465 ));
466 }
467
468 for y in ((child_pos_y + 1)..pos_y).rev() {
470 edges[y].push(WrappedEdge::new(
471 EdgeType::Vertical,
472 child_pos_x,
473 child_pos_x,
474 cid,
475 ));
476 }
477 edges[child_pos_y].push(WrappedEdge::new(
478 EdgeType::Down,
479 child_pos_x,
480 child_pos_x,
481 cid,
482 ));
483}
484
485#[allow(clippy::too_many_arguments)]
487fn draw_merge_edge<'a>(
488 edges: &mut [Vec<WrappedEdge<'a>>],
489 commit_pos_map: &CommitPosMap,
490 commits: &'a [VoidCommit],
491 pos_x: usize,
492 pos_y: usize,
493 child_pos_x: usize,
494 child_pos_y: usize,
495 cid: &'a CommitCid,
496 max_pos_x: &mut usize,
497) {
498 let mut overlap = false;
499 let mut new_pos_x = pos_x;
500
501 let mut skip_judge_overlap = true;
503 for y in (child_pos_y + 1)..pos_y {
504 let processing_commit_pos_x = commit_pos_map.get(&commits[y].cid).unwrap().0;
505 if processing_commit_pos_x == new_pos_x {
506 skip_judge_overlap = false;
507 break;
508 }
509 if edges[y]
510 .iter()
511 .filter(|e| e.edge.pos_x == pos_x)
512 .filter(|e| matches!(e.edge.edge_type, EdgeType::Vertical))
513 .any(|e| e.edge_parent_cid != cid)
514 {
515 skip_judge_overlap = false;
516 break;
517 }
518 }
519
520 if !skip_judge_overlap {
521 for y in (child_pos_y + 1)..pos_y {
523 let processing_commit_pos_x = commit_pos_map.get(&commits[y].cid).unwrap().0;
524 if processing_commit_pos_x == new_pos_x {
525 overlap = true;
526 if new_pos_x < processing_commit_pos_x + 1 {
527 new_pos_x = processing_commit_pos_x + 1;
528 }
529 }
530 for edge in &edges[y] {
531 if edge.edge.pos_x >= new_pos_x
532 && edge.edge_parent_cid != cid
533 && matches!(edge.edge.edge_type, EdgeType::Vertical)
534 {
535 overlap = true;
536 if new_pos_x < edge.edge.pos_x + 1 {
537 new_pos_x = edge.edge.pos_x + 1;
538 }
539 }
540 }
541 }
542 }
543
544 if overlap {
545 edges[pos_y].push(WrappedEdge::new(EdgeType::Right, pos_x, pos_x, cid));
547 for x in (pos_x + 1)..new_pos_x {
548 edges[pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, pos_x, cid));
549 }
550 edges[pos_y].push(WrappedEdge::new(
551 EdgeType::RightBottom,
552 new_pos_x,
553 pos_x,
554 cid,
555 ));
556 for y in ((child_pos_y + 1)..pos_y).rev() {
557 edges[y].push(WrappedEdge::new(EdgeType::Vertical, new_pos_x, pos_x, cid));
558 }
559 edges[child_pos_y].push(WrappedEdge::new(EdgeType::RightTop, new_pos_x, pos_x, cid));
560 for x in (child_pos_x + 1)..new_pos_x {
561 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, pos_x, cid));
562 }
563 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Right, child_pos_x, pos_x, cid));
564
565 if *max_pos_x < new_pos_x {
566 *max_pos_x = new_pos_x;
567 }
568 } else {
569 edges[pos_y].push(WrappedEdge::new(EdgeType::Up, pos_x, pos_x, cid));
571 for y in ((child_pos_y + 1)..pos_y).rev() {
572 edges[y].push(WrappedEdge::new(EdgeType::Vertical, pos_x, pos_x, cid));
573 }
574
575 if pos_x < child_pos_x {
576 edges[child_pos_y].push(WrappedEdge::new(EdgeType::LeftTop, pos_x, pos_x, cid));
577 for x in (pos_x + 1)..child_pos_x {
578 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, pos_x, cid));
579 }
580 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Left, child_pos_x, pos_x, cid));
581 } else {
582 edges[child_pos_y].push(WrappedEdge::new(EdgeType::RightTop, pos_x, pos_x, cid));
583 for x in (child_pos_x + 1)..pos_x {
584 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Horizontal, x, pos_x, cid));
585 }
586 edges[child_pos_y].push(WrappedEdge::new(EdgeType::Right, child_pos_x, pos_x, cid));
587 }
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 fn make_commit(cid: &str, parents: Vec<&str>) -> VoidCommit {
596 VoidCommit {
597 cid: CommitCid(cid.to_string()),
598 parents: parents.into_iter().map(|p| CommitCid(p.to_string())).collect(),
599 message: String::new(),
600 timestamp_ms: 0,
601 is_signed: false,
602 signature_valid: None,
603 author: None,
604 }
605 }
606
607 #[test]
608 fn test_linear_graph() {
609 let commits = vec![
611 make_commit("c3", vec!["c2"]),
612 make_commit("c2", vec!["c1"]),
613 make_commit("c1", vec![]),
614 ];
615 let ctx = SimpleGraphContext::new(&commits);
616 let graph = calc_graph(&commits, &ctx);
617
618 for commit in &commits {
620 let (x, _) = graph.commit_pos_map[&commit.cid];
621 assert_eq!(x, 0);
622 }
623 }
624
625 #[test]
626 fn test_branch_graph() {
627 let commits = vec![
630 make_commit("c3", vec!["c1"]),
631 make_commit("c2", vec!["c1"]),
632 make_commit("c1", vec![]),
633 ];
634 let ctx = SimpleGraphContext::new(&commits);
635 let graph = calc_graph(&commits, &ctx);
636
637 let (x3, _) = graph.commit_pos_map[&commits[0].cid];
639 let (x2, _) = graph.commit_pos_map[&commits[1].cid];
640 assert_ne!(x3, x2);
641 }
642
643 #[test]
644 fn test_linear_edges() {
645 let commits = vec![
647 make_commit("c1", vec!["c2"]),
648 make_commit("c2", vec!["c3"]),
649 make_commit("c3", vec![]),
650 ];
651 let ctx = SimpleGraphContext::new(&commits);
652 let graph = calc_graph(&commits, &ctx);
653
654 println!("edges[0]: {:?}", graph.edges[0]);
656 println!("edges[1]: {:?}", graph.edges[1]);
657 println!("edges[2]: {:?}", graph.edges[2]);
658 println!("max_pos_x: {}", graph.max_pos_x);
659
660 assert!(!graph.edges[1].is_empty(), "middle row should have edges");
662 }
663}