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