1use crate::Result;
2use crate::model::{
3 Bounds, GitGraphArrowLayout, GitGraphBranchLayout, GitGraphCommitLayout, GitGraphDiagramLayout,
4};
5use crate::text::{TextMeasurer, TextStyle};
6use serde::Deserialize;
7use std::collections::HashMap;
8
9const LAYOUT_OFFSET: f64 = 10.0;
10const COMMIT_STEP: f64 = 40.0;
11const DEFAULT_POS: f64 = 30.0;
12const THEME_COLOR_LIMIT: usize = 8;
13
14const COMMIT_TYPE_MERGE: i64 = 3;
15
16#[derive(Debug, Clone, Deserialize)]
17struct GitGraphBranch {
18 name: String,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22struct GitGraphCommit {
23 id: String,
24 #[serde(default)]
25 message: String,
26 #[serde(default)]
27 parents: Vec<String>,
28 seq: i64,
29 #[serde(default)]
30 tags: Vec<String>,
31 #[serde(rename = "type")]
32 commit_type: i64,
33 branch: String,
34 #[serde(default, rename = "customType")]
35 custom_type: Option<i64>,
36 #[serde(default, rename = "customId")]
37 custom_id: Option<bool>,
38}
39
40#[derive(Debug, Clone, Deserialize)]
41struct GitGraphModel {
42 #[serde(default)]
43 branches: Vec<GitGraphBranch>,
44 #[serde(default)]
45 commits: Vec<GitGraphCommit>,
46 #[serde(default)]
47 direction: String,
48 #[serde(rename = "type")]
49 diagram_type: String,
50}
51
52fn cfg_f64(cfg: &serde_json::Value, path: &[&str]) -> Option<f64> {
53 let mut cur = cfg;
54 for k in path {
55 cur = cur.get(*k)?;
56 }
57 cur.as_f64()
58}
59
60fn cfg_bool(cfg: &serde_json::Value, path: &[&str]) -> Option<bool> {
61 let mut cur = cfg;
62 for k in path {
63 cur = cur.get(*k)?;
64 }
65 cur.as_bool()
66}
67
68fn cfg_string(cfg: &serde_json::Value, path: &[&str]) -> Option<String> {
69 let mut cur = cfg;
70 for k in path {
71 cur = cur.get(*k)?;
72 }
73 cur.as_str().map(|s| s.to_string())
74}
75
76fn cfg_font_size(cfg: &serde_json::Value) -> f64 {
77 cfg.get("fontSize")
78 .and_then(|v| {
79 v.as_f64()
80 .or_else(|| v.as_i64().map(|n| n as f64))
81 .or_else(|| v.as_u64().map(|n| n as f64))
82 })
83 .unwrap_or(16.0)
84 .max(1.0)
85}
86
87fn normalize_css_font_family(font_family: &str) -> String {
88 let s = font_family.trim().trim_end_matches(';').trim();
89 if s.is_empty() {
90 return String::new();
91 }
92
93 let mut parts: Vec<String> = Vec::new();
94 let mut cur = String::new();
95 let mut in_single = false;
96 let mut in_double = false;
97
98 for ch in s.chars() {
99 match ch {
100 '\'' if !in_double => {
101 in_single = !in_single;
102 cur.push(ch);
103 }
104 '"' if !in_single => {
105 in_double = !in_double;
106 cur.push(ch);
107 }
108 ',' if !in_single && !in_double => {
109 let p = cur.trim();
110 if !p.is_empty() {
111 parts.push(p.to_string());
112 }
113 cur.clear();
114 }
115 _ => cur.push(ch),
116 }
117 }
118
119 let p = cur.trim();
120 if !p.is_empty() {
121 parts.push(p.to_string());
122 }
123
124 parts.join(",")
125}
126
127fn commit_symbol_type(commit: &GitGraphCommit) -> i64 {
128 commit.custom_type.unwrap_or(commit.commit_type)
129}
130
131#[derive(Debug, Clone, Copy)]
132struct CommitPosition {
133 x: f64,
134 y: f64,
135}
136
137fn find_closest_parent(
138 parents: &[String],
139 dir: &str,
140 commit_pos: &HashMap<String, CommitPosition>,
141) -> Option<String> {
142 let mut target: f64 = if dir == "BT" { f64::INFINITY } else { 0.0 };
143 let mut closest: Option<String> = None;
144 for parent in parents {
145 let Some(pos) = commit_pos.get(parent) else {
146 continue;
147 };
148 let parent_position = if dir == "TB" || dir == "BT" {
149 pos.y
150 } else {
151 pos.x
152 };
153 if dir == "BT" {
154 if parent_position <= target {
155 closest = Some(parent.clone());
156 target = parent_position;
157 }
158 } else if parent_position >= target {
159 closest = Some(parent.clone());
160 target = parent_position;
161 }
162 }
163 closest
164}
165
166fn should_reroute_arrow(
167 commit_a: &GitGraphCommit,
168 commit_b: &GitGraphCommit,
169 p1: CommitPosition,
170 p2: CommitPosition,
171 all_commits: &HashMap<String, GitGraphCommit>,
172 dir: &str,
173) -> bool {
174 let commit_b_is_furthest = if dir == "TB" || dir == "BT" {
175 p1.x < p2.x
176 } else {
177 p1.y < p2.y
178 };
179 let branch_to_get_curve = if commit_b_is_furthest {
180 commit_b.branch.as_str()
181 } else {
182 commit_a.branch.as_str()
183 };
184
185 all_commits.values().any(|commit_x| {
186 commit_x.branch == branch_to_get_curve
187 && commit_x.seq > commit_a.seq
188 && commit_x.seq < commit_b.seq
189 })
190}
191
192fn find_lane(y1: f64, y2: f64, lanes: &mut Vec<f64>, depth: usize) -> f64 {
193 let candidate = y1 + (y1 - y2).abs() / 2.0;
194 if depth > 5 {
195 return candidate;
196 }
197
198 let ok = lanes.iter().all(|lane| (lane - candidate).abs() >= 10.0);
199 if ok {
200 lanes.push(candidate);
201 return candidate;
202 }
203
204 let diff = (y1 - y2).abs();
205 find_lane(y1, y2 - diff / 5.0, lanes, depth + 1)
206}
207
208fn draw_arrow(
209 commit_a: &GitGraphCommit,
210 commit_b: &GitGraphCommit,
211 all_commits: &HashMap<String, GitGraphCommit>,
212 commit_pos: &HashMap<String, CommitPosition>,
213 branch_index: &HashMap<String, usize>,
214 lanes: &mut Vec<f64>,
215 dir: &str,
216) -> Option<GitGraphArrowLayout> {
217 let p1 = *commit_pos.get(&commit_a.id)?;
218 let p2 = *commit_pos.get(&commit_b.id)?;
219 let arrow_needs_rerouting = should_reroute_arrow(commit_a, commit_b, p1, p2, all_commits, dir);
220
221 let mut color_class_num = branch_index.get(&commit_b.branch).copied().unwrap_or(0);
222 if commit_b.commit_type == COMMIT_TYPE_MERGE
223 && commit_a
224 .id
225 .as_str()
226 .ne(commit_b.parents.first().map(|s| s.as_str()).unwrap_or(""))
227 {
228 color_class_num = branch_index
229 .get(&commit_a.branch)
230 .copied()
231 .unwrap_or(color_class_num);
232 }
233
234 let mut line_def: Option<String> = None;
235 if arrow_needs_rerouting {
236 let arc = "A 10 10, 0, 0, 0,";
237 let arc2 = "A 10 10, 0, 0, 1,";
238 let radius = 10.0;
239 let offset = 10.0;
240
241 let line_y = if p1.y < p2.y {
242 find_lane(p1.y, p2.y, lanes, 0)
243 } else {
244 find_lane(p2.y, p1.y, lanes, 0)
245 };
246 let line_x = if p1.x < p2.x {
247 find_lane(p1.x, p2.x, lanes, 0)
248 } else {
249 find_lane(p2.x, p1.x, lanes, 0)
250 };
251
252 if dir == "TB" {
253 if p1.x < p2.x {
254 line_def = Some(format!(
255 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
256 p1.x,
257 p1.y,
258 line_x - radius,
259 p1.y,
260 arc2,
261 line_x,
262 p1.y + offset,
263 line_x,
264 p2.y - radius,
265 arc,
266 line_x + offset,
267 p2.y,
268 p2.x,
269 p2.y
270 ));
271 } else {
272 color_class_num = branch_index.get(&commit_a.branch).copied().unwrap_or(0);
273 line_def = Some(format!(
274 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
275 p1.x,
276 p1.y,
277 line_x + radius,
278 p1.y,
279 arc,
280 line_x,
281 p1.y + offset,
282 line_x,
283 p2.y - radius,
284 arc2,
285 line_x - offset,
286 p2.y,
287 p2.x,
288 p2.y
289 ));
290 }
291 } else if dir == "BT" {
292 if p1.x < p2.x {
293 line_def = Some(format!(
294 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
295 p1.x,
296 p1.y,
297 line_x - radius,
298 p1.y,
299 arc,
300 line_x,
301 p1.y - offset,
302 line_x,
303 p2.y + radius,
304 arc2,
305 line_x + offset,
306 p2.y,
307 p2.x,
308 p2.y
309 ));
310 } else {
311 color_class_num = branch_index.get(&commit_a.branch).copied().unwrap_or(0);
312 line_def = Some(format!(
313 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
314 p1.x,
315 p1.y,
316 line_x + radius,
317 p1.y,
318 arc2,
319 line_x,
320 p1.y - offset,
321 line_x,
322 p2.y + radius,
323 arc,
324 line_x - offset,
325 p2.y,
326 p2.x,
327 p2.y
328 ));
329 }
330 } else if p1.y < p2.y {
331 line_def = Some(format!(
332 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
333 p1.x,
334 p1.y,
335 p1.x,
336 line_y - radius,
337 arc,
338 p1.x + offset,
339 line_y,
340 p2.x - radius,
341 line_y,
342 arc2,
343 p2.x,
344 line_y + offset,
345 p2.x,
346 p2.y
347 ));
348 } else {
349 color_class_num = branch_index.get(&commit_a.branch).copied().unwrap_or(0);
350 line_def = Some(format!(
351 "M {} {} L {} {} {} {} {} L {} {} {} {} {} L {} {}",
352 p1.x,
353 p1.y,
354 p1.x,
355 line_y + radius,
356 arc2,
357 p1.x + offset,
358 line_y,
359 p2.x - radius,
360 line_y,
361 arc,
362 p2.x,
363 line_y - offset,
364 p2.x,
365 p2.y
366 ));
367 }
368 } else {
369 let arc = "A 20 20, 0, 0, 0,";
370 let arc2 = "A 20 20, 0, 0, 1,";
371 let radius = 20.0;
372 let offset = 20.0;
373
374 if dir == "TB" {
375 if p1.x < p2.x {
376 if commit_b.commit_type == COMMIT_TYPE_MERGE
377 && commit_a.id.as_str().ne(commit_b
378 .parents
379 .first()
380 .map(|s| s.as_str())
381 .unwrap_or(""))
382 {
383 line_def = Some(format!(
384 "M {} {} L {} {} {} {} {} L {} {}",
385 p1.x,
386 p1.y,
387 p1.x,
388 p2.y - radius,
389 arc,
390 p1.x + offset,
391 p2.y,
392 p2.x,
393 p2.y
394 ));
395 } else {
396 line_def = Some(format!(
397 "M {} {} L {} {} {} {} {} L {} {}",
398 p1.x,
399 p1.y,
400 p2.x - radius,
401 p1.y,
402 arc2,
403 p2.x,
404 p1.y + offset,
405 p2.x,
406 p2.y
407 ));
408 }
409 }
410
411 if p1.x > p2.x {
412 if commit_b.commit_type == COMMIT_TYPE_MERGE
413 && commit_a.id.as_str().ne(commit_b
414 .parents
415 .first()
416 .map(|s| s.as_str())
417 .unwrap_or(""))
418 {
419 line_def = Some(format!(
420 "M {} {} L {} {} {} {} {} L {} {}",
421 p1.x,
422 p1.y,
423 p1.x,
424 p2.y - radius,
425 arc2,
426 p1.x - offset,
427 p2.y,
428 p2.x,
429 p2.y
430 ));
431 } else {
432 line_def = Some(format!(
433 "M {} {} L {} {} {} {} {} L {} {}",
434 p1.x,
435 p1.y,
436 p2.x + radius,
437 p1.y,
438 arc,
439 p2.x,
440 p1.y + offset,
441 p2.x,
442 p2.y
443 ));
444 }
445 }
446
447 if p1.x == p2.x {
448 line_def = Some(format!("M {} {} L {} {}", p1.x, p1.y, p2.x, p2.y));
449 }
450 } else if dir == "BT" {
451 if p1.x < p2.x {
452 if commit_b.commit_type == COMMIT_TYPE_MERGE
453 && commit_a.id.as_str().ne(commit_b
454 .parents
455 .first()
456 .map(|s| s.as_str())
457 .unwrap_or(""))
458 {
459 line_def = Some(format!(
460 "M {} {} L {} {} {} {} {} L {} {}",
461 p1.x,
462 p1.y,
463 p1.x,
464 p2.y + radius,
465 arc2,
466 p1.x + offset,
467 p2.y,
468 p2.x,
469 p2.y
470 ));
471 } else {
472 line_def = Some(format!(
473 "M {} {} L {} {} {} {} {} L {} {}",
474 p1.x,
475 p1.y,
476 p2.x - radius,
477 p1.y,
478 arc,
479 p2.x,
480 p1.y - offset,
481 p2.x,
482 p2.y
483 ));
484 }
485 }
486
487 if p1.x > p2.x {
488 if commit_b.commit_type == COMMIT_TYPE_MERGE
489 && commit_a.id.as_str().ne(commit_b
490 .parents
491 .first()
492 .map(|s| s.as_str())
493 .unwrap_or(""))
494 {
495 line_def = Some(format!(
496 "M {} {} L {} {} {} {} {} L {} {}",
497 p1.x,
498 p1.y,
499 p1.x,
500 p2.y + radius,
501 arc,
502 p1.x - offset,
503 p2.y,
504 p2.x,
505 p2.y
506 ));
507 } else {
508 line_def = Some(format!(
509 "M {} {} L {} {} {} {} {} L {} {}",
510 p1.x,
511 p1.y,
512 p2.x - radius,
513 p1.y,
514 arc,
515 p2.x,
516 p1.y - offset,
517 p2.x,
518 p2.y
519 ));
520 }
521 }
522
523 if p1.x == p2.x {
524 line_def = Some(format!("M {} {} L {} {}", p1.x, p1.y, p2.x, p2.y));
525 }
526 } else {
527 if p1.y < p2.y {
528 if commit_b.commit_type == COMMIT_TYPE_MERGE
529 && commit_a.id.as_str().ne(commit_b
530 .parents
531 .first()
532 .map(|s| s.as_str())
533 .unwrap_or(""))
534 {
535 line_def = Some(format!(
536 "M {} {} L {} {} {} {} {} L {} {}",
537 p1.x,
538 p1.y,
539 p2.x - radius,
540 p1.y,
541 arc2,
542 p2.x,
543 p1.y + offset,
544 p2.x,
545 p2.y
546 ));
547 } else {
548 line_def = Some(format!(
549 "M {} {} L {} {} {} {} {} L {} {}",
550 p1.x,
551 p1.y,
552 p1.x,
553 p2.y - radius,
554 arc,
555 p1.x + offset,
556 p2.y,
557 p2.x,
558 p2.y
559 ));
560 }
561 }
562
563 if p1.y > p2.y {
564 if commit_b.commit_type == COMMIT_TYPE_MERGE
565 && commit_a.id.as_str().ne(commit_b
566 .parents
567 .first()
568 .map(|s| s.as_str())
569 .unwrap_or(""))
570 {
571 line_def = Some(format!(
572 "M {} {} L {} {} {} {} {} L {} {}",
573 p1.x,
574 p1.y,
575 p2.x - radius,
576 p1.y,
577 arc,
578 p2.x,
579 p1.y - offset,
580 p2.x,
581 p2.y
582 ));
583 } else {
584 line_def = Some(format!(
585 "M {} {} L {} {} {} {} {} L {} {}",
586 p1.x,
587 p1.y,
588 p1.x,
589 p2.y + radius,
590 arc2,
591 p1.x + offset,
592 p2.y,
593 p2.x,
594 p2.y
595 ));
596 }
597 }
598
599 if p1.y == p2.y {
600 line_def = Some(format!("M {} {} L {} {}", p1.x, p1.y, p2.x, p2.y));
601 }
602 }
603 }
604
605 let d = line_def?;
606 Some(GitGraphArrowLayout {
607 from: commit_a.id.clone(),
608 to: commit_b.id.clone(),
609 class_index: (color_class_num % THEME_COLOR_LIMIT) as i64,
610 d,
611 })
612}
613
614pub fn layout_gitgraph_diagram(
615 semantic: &serde_json::Value,
616 effective_config: &serde_json::Value,
617 measurer: &dyn TextMeasurer,
618) -> Result<GitGraphDiagramLayout> {
619 let model: GitGraphModel = crate::json::from_value_ref(semantic)?;
620 let _ = model.diagram_type.as_str();
621
622 let direction = if model.direction.trim().is_empty() {
623 "LR".to_string()
624 } else {
625 model.direction.trim().to_string()
626 };
627
628 let rotate_commit_label =
629 cfg_bool(effective_config, &["gitGraph", "rotateCommitLabel"]).unwrap_or(true);
630 let show_commit_label =
631 cfg_bool(effective_config, &["gitGraph", "showCommitLabel"]).unwrap_or(true);
632 let show_branches = cfg_bool(effective_config, &["gitGraph", "showBranches"]).unwrap_or(true);
633 let diagram_padding = cfg_f64(effective_config, &["gitGraph", "diagramPadding"])
634 .unwrap_or(8.0)
635 .max(0.0);
636 let parallel_commits =
637 cfg_bool(effective_config, &["gitGraph", "parallelCommits"]).unwrap_or(false);
638
639 let font_family = cfg_string(effective_config, &["fontFamily"])
642 .or_else(|| cfg_string(effective_config, &["themeVariables", "fontFamily"]))
643 .map(|s| s.trim().trim_end_matches(';').trim().to_string())
644 .filter(|s| !s.is_empty())
645 .unwrap_or_else(|| "\"trebuchet ms\", verdana, arial, sans-serif".to_string());
646 let font_size = cfg_font_size(effective_config);
647 let apply_bbox_corrections = normalize_css_font_family(&font_family)
648 == r#""trebuchet ms",verdana,arial,sans-serif"#
649 && (font_size - 16.0).abs() <= 1e-9;
650
651 let label_style = TextStyle {
652 font_family: Some(font_family),
653 font_size,
654 font_weight: None,
655 };
656
657 fn corr_px(num_over_2048: i32) -> f64 {
658 num_over_2048 as f64 / 2048.0
661 }
662
663 fn gitgraph_branch_label_bbox_width_correction_px(text: &str) -> f64 {
664 match text {
670 "develop" => corr_px(16), "feature" => corr_px(-48), "newbranch" => corr_px(-32), "testBranch" => corr_px(-32), "testBranch2" => corr_px(-32), "__proto__" => corr_px(-16), "branch/example-branch" => corr_px(-64), _ => 0.0,
685 }
686 }
687
688 fn gitgraph_branch_label_bbox_width_px(
689 measurer: &dyn TextMeasurer,
690 text: &str,
691 style: &TextStyle,
692 apply_corrections: bool,
693 ) -> f64 {
694 let base = crate::text::round_to_1_64_px(
697 measurer
698 .measure_svg_simple_text_bbox_width_px(text, style)
699 .max(0.0),
700 );
701 let extra = if apply_corrections {
702 gitgraph_branch_label_bbox_width_correction_px(text)
703 } else {
704 0.0
705 };
706 (base + extra).max(0.0)
707 }
708
709 let mut branches: Vec<GitGraphBranchLayout> = Vec::new();
710 let mut branch_pos: HashMap<String, f64> = HashMap::new();
711 let mut branch_index: HashMap<String, usize> = HashMap::new();
712 let mut pos = 0.0;
713 for (i, b) in model.branches.iter().enumerate() {
714 let metrics = measurer.measure(&b.name, &label_style);
716 let bbox_w = gitgraph_branch_label_bbox_width_px(
717 measurer,
718 &b.name,
719 &label_style,
720 apply_bbox_corrections,
721 );
722 branch_pos.insert(b.name.clone(), pos);
723 branch_index.insert(b.name.clone(), i);
724
725 branches.push(GitGraphBranchLayout {
726 name: b.name.clone(),
727 index: i as i64,
728 pos,
729 bbox_width: bbox_w.max(0.0),
730 bbox_height: metrics.height.max(0.0),
731 });
732
733 pos += 50.0
734 + if rotate_commit_label { 40.0 } else { 0.0 }
735 + if direction == "TB" || direction == "BT" {
736 bbox_w.max(0.0) / 2.0
737 } else {
738 0.0
739 };
740 }
741
742 let mut commits_by_id: HashMap<String, GitGraphCommit> = HashMap::new();
743 for c in &model.commits {
744 commits_by_id.insert(c.id.clone(), c.clone());
745 }
746
747 let mut commit_order: Vec<GitGraphCommit> = model.commits.clone();
748 commit_order.sort_by_key(|c| c.seq);
749
750 let mut sorted_keys: Vec<String> = commit_order.iter().map(|c| c.id.clone()).collect();
751 if direction == "BT" {
752 sorted_keys.reverse();
753 }
754
755 let mut commit_pos: HashMap<String, CommitPosition> = HashMap::new();
756 let mut commits: Vec<GitGraphCommitLayout> = Vec::new();
757 let mut max_pos: f64 = 0.0;
758 let mut cur_pos = if direction == "TB" || direction == "BT" {
759 DEFAULT_POS
760 } else {
761 0.0
762 };
763
764 for id in &sorted_keys {
765 let Some(commit) = commits_by_id.get(id) else {
766 continue;
767 };
768
769 if parallel_commits {
770 if !commit.parents.is_empty() {
771 if let Some(closest_parent) =
772 find_closest_parent(&commit.parents, &direction, &commit_pos)
773 {
774 if let Some(parent_position) = commit_pos.get(&closest_parent) {
775 if direction == "TB" {
776 cur_pos = parent_position.y + COMMIT_STEP;
777 } else if direction == "BT" {
778 let current_position = commit_pos
779 .get(&commit.id)
780 .copied()
781 .unwrap_or(CommitPosition { x: 0.0, y: 0.0 });
782 cur_pos = current_position.y - COMMIT_STEP;
783 } else {
784 cur_pos = parent_position.x + COMMIT_STEP;
785 }
786 }
787 }
788 } else if direction == "TB" {
789 cur_pos = DEFAULT_POS;
790 }
791 }
792
793 let pos_with_offset = if direction == "BT" && parallel_commits {
794 cur_pos
795 } else {
796 cur_pos + LAYOUT_OFFSET
797 };
798 let Some(branch_lane) = branch_pos.get(&commit.branch).copied() else {
799 return Err(crate::Error::InvalidModel {
800 message: format!("unknown branch for commit {}: {}", commit.id, commit.branch),
801 });
802 };
803
804 let (x, y) = if direction == "TB" || direction == "BT" {
805 (branch_lane, pos_with_offset)
806 } else {
807 (pos_with_offset, branch_lane)
808 };
809 commit_pos.insert(commit.id.clone(), CommitPosition { x, y });
810
811 commits.push(GitGraphCommitLayout {
812 id: commit.id.clone(),
813 message: commit.message.clone(),
814 seq: commit.seq,
815 commit_type: commit.commit_type,
816 custom_type: commit.custom_type,
817 custom_id: commit.custom_id,
818 tags: commit.tags.clone(),
819 parents: commit.parents.clone(),
820 branch: commit.branch.clone(),
821 pos: cur_pos,
822 pos_with_offset,
823 x,
824 y,
825 });
826
827 cur_pos = if direction == "BT" && parallel_commits {
828 cur_pos + COMMIT_STEP
829 } else {
830 cur_pos + COMMIT_STEP + LAYOUT_OFFSET
831 };
832 max_pos = max_pos.max(cur_pos);
833 }
834
835 let mut lanes: Vec<f64> = if show_branches {
836 branches.iter().map(|b| b.pos).collect()
837 } else {
838 Vec::new()
839 };
840
841 let mut arrows: Vec<GitGraphArrowLayout> = Vec::new();
842 let mut commits_for_arrows = model.commits.clone();
845 commits_for_arrows.sort_by_key(|c| c.seq);
846 for commit_b in &commits_for_arrows {
847 for parent in &commit_b.parents {
848 let Some(commit_a) = commits_by_id.get(parent) else {
849 continue;
850 };
851 if let Some(a) = draw_arrow(
852 commit_a,
853 commit_b,
854 &commits_by_id,
855 &commit_pos,
856 &branch_index,
857 &mut lanes,
858 &direction,
859 ) {
860 arrows.push(a);
861 }
862 }
863 }
864
865 let mut min_x = f64::INFINITY;
866 let mut min_y = f64::INFINITY;
867 let mut max_x = f64::NEG_INFINITY;
868 let mut max_y = f64::NEG_INFINITY;
869
870 for b in &branches {
871 if direction == "TB" || direction == "BT" {
872 min_x = min_x.min(b.pos);
873 max_x = max_x.max(b.pos);
874 min_y = min_y.min(DEFAULT_POS.min(max_pos));
875 max_y = max_y.max(DEFAULT_POS.max(max_pos));
876 } else {
877 min_y = min_y.min(b.pos);
878 max_y = max_y.max(b.pos);
879 min_x = min_x.min(0.0);
880 max_x = max_x.max(max_pos);
881 let label_left =
882 -b.bbox_width - 4.0 - if rotate_commit_label { 30.0 } else { 0.0 } - 19.0;
883 min_x = min_x.min(label_left);
884 }
885 }
886
887 for c in &commits {
888 let r = if commit_symbol_type(&commits_by_id[&c.id]) == COMMIT_TYPE_MERGE {
889 9.0
890 } else {
891 10.0
892 };
893 min_x = min_x.min(c.x - r);
894 min_y = min_y.min(c.y - r);
895 max_x = max_x.max(c.x + r);
896 max_y = max_y.max(c.y + r);
897 }
898
899 let bounds = if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite()
900 {
901 Some(Bounds {
902 min_x: min_x - diagram_padding,
903 min_y: min_y - diagram_padding,
904 max_x: max_x + diagram_padding,
905 max_y: max_y + diagram_padding,
906 })
907 } else {
908 None
909 };
910
911 Ok(GitGraphDiagramLayout {
912 bounds,
913 direction,
914 rotate_commit_label,
915 show_branches,
916 show_commit_label,
917 parallel_commits,
918 diagram_padding,
919 max_pos,
920 branches,
921 commits,
922 arrows,
923 })
924}