1use crate::graph::{CommitInfo, GitGraph, HeadInfo};
4use crate::print::format::CommitFormat;
5use crate::settings::{Characters, Settings};
6use itertools::Itertools;
7use std::cmp::max;
8use std::collections::hash_map::Entry::{Occupied, Vacant};
9use std::collections::HashMap;
10use std::fmt::Write;
11use textwrap::Options;
12use yansi::Paint;
13
14const SPACE: u8 = 0;
15const DOT: u8 = 1;
16const CIRCLE: u8 = 2;
17const VER: u8 = 3;
18const HOR: u8 = 4;
19const CROSS: u8 = 5;
20const R_U: u8 = 6;
21const R_D: u8 = 7;
22const L_D: u8 = 8;
23const L_U: u8 = 9;
24const VER_L: u8 = 10;
25const VER_R: u8 = 11;
26const HOR_U: u8 = 12;
27const HOR_D: u8 = 13;
28
29const ARR_L: u8 = 14;
30const ARR_R: u8 = 15;
31
32const WHITE: u8 = 7;
33const HEAD_COLOR: u8 = 14;
34const HASH_COLOR: u8 = 11;
35
36pub type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
56
57pub fn print_unicode(graph: &GitGraph, settings: &Settings) -> Result<UnicodeGraphInfo, String> {
59 if graph.all_branches.is_empty() {
60 return Ok((vec![], vec![], vec![]));
61 }
62 let num_cols = 2 * graph
63 .all_branches
64 .iter()
65 .map(|b| b.visual.column.unwrap_or(0))
66 .max()
67 .unwrap()
68 + 1;
69
70 let head_idx = graph.indices.get(&graph.head.oid);
71
72 let inserts = get_inserts(graph, settings.compact);
73
74 let (indent1, indent2) = if let Some((_, ind1, ind2)) = settings.wrapping {
75 (" ".repeat(ind1.unwrap_or(0)), " ".repeat(ind2.unwrap_or(0)))
76 } else {
77 ("".to_string(), "".to_string())
78 };
79
80 let wrap_options = if let Some((width, _, _)) = settings.wrapping {
81 create_wrapping_options(width, &indent1, &indent2, num_cols + 4)?
82 } else {
83 None
84 };
85
86 let mut index_map = vec![];
89 let mut text_lines = vec![];
90 let mut offset = 0;
91 for (idx, info) in graph.commits.iter().enumerate() {
92 index_map.push(idx + offset);
93 let cnt_inserts = if let Some(inserts) = inserts.get(&idx) {
94 inserts
95 .iter()
96 .filter(|vec| {
97 vec.iter().all(|occ| match occ {
98 Occ::Commit(_, _) => false,
99 Occ::Range(_, _, _, _) => true,
100 })
101 })
102 .count()
103 } else {
104 0
105 };
106
107 let head = if head_idx == Some(&idx) {
108 Some(&graph.head)
109 } else {
110 None
111 };
112
113 let lines = format(
114 &settings.format,
115 graph,
116 info,
117 head,
118 settings.colored,
119 &wrap_options,
120 )?;
121
122 let num_lines = if lines.is_empty() { 0 } else { lines.len() - 1 };
123 let max_inserts = max(cnt_inserts, num_lines);
124 let add_lines = max_inserts - num_lines;
125
126 text_lines.extend(lines.into_iter().map(Some));
127 text_lines.extend((0..add_lines).map(|_| None));
128
129 offset += max_inserts;
130 }
131
132 let mut grid = Grid::new(
133 num_cols,
134 graph.commits.len() + offset,
135 GridCell {
136 character: SPACE,
137 color: WHITE,
138 pers: settings.branches.persistence.len() as u8 + 2,
139 },
140 );
141
142 for (idx, info) in graph.commits.iter().enumerate() {
144 let Some(trace) = info.branch_trace else {
145 continue;
146 };
147 let branch = &graph.all_branches[trace];
148 let column = branch.visual.column.unwrap();
149 let idx_map = index_map[idx];
150
151 let branch_color = branch.visual.term_color;
152
153 grid.set(
154 column * 2,
155 idx_map,
156 if info.is_merge { CIRCLE } else { DOT },
157 branch_color,
158 branch.persistence,
159 );
160 for p in 0..2 {
161 let parent = info.parents[p];
162 let Some(par_oid) = parent else {
163 continue;
164 };
165 let Some(par_idx) = graph.indices.get(&par_oid) else {
166 let idx_bottom = grid.height;
169 vline(
170 &mut grid,
171 (idx_map, idx_bottom),
172 column,
173 branch_color,
174 branch.persistence,
175 );
176 continue;
177 };
178
179 let par_idx_map = index_map[*par_idx];
180 let par_info = &graph.commits[*par_idx];
181 let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
182 let par_column = par_branch.visual.column.unwrap();
183
184 let (color, pers) = if info.is_merge {
185 (par_branch.visual.term_color, par_branch.persistence)
186 } else {
187 (branch_color, branch.persistence)
188 };
189
190 if branch.visual.column == par_branch.visual.column {
191 if par_idx_map > idx_map + 1 {
192 vline(&mut grid, (idx_map, par_idx_map), column, color, pers);
193 }
194 } else {
195 let split_index = super::get_deviate_index(graph, idx, *par_idx);
196 let split_idx_map = index_map[split_index];
197 let insert_idx = find_insert_idx(&inserts[&split_index], idx, *par_idx).unwrap();
198 let idx_split = split_idx_map + insert_idx;
199
200 let is_secondary_merge = info.is_merge && p > 0;
201
202 let row123 = (idx_map, idx_split, par_idx_map);
203 let col12 = (column, par_column);
204 zig_zag_line(&mut grid, row123, col12, is_secondary_merge, color, pers);
205 }
206 }
207 }
208
209 if settings.reverse_commit_order {
210 text_lines.reverse();
211 grid.reverse();
212 }
213
214 let lines = print_graph(&settings.characters, &grid, text_lines, settings.colored);
215
216 Ok((lines.0, lines.1, index_map))
217}
218
219fn create_wrapping_options<'a>(
221 width: Option<usize>,
222 indent1: &'a str,
223 indent2: &'a str,
224 graph_width: usize,
225) -> Result<Option<Options<'a>>, String> {
226 let wrapping = if let Some(width) = width {
227 Some(
228 textwrap::Options::new(width)
229 .initial_indent(indent1)
230 .subsequent_indent(indent2),
231 )
232 } else if atty::is(atty::Stream::Stdout) {
233 let width = crossterm::terminal::size()
234 .map_err(|err| err.to_string())?
235 .0 as usize;
236 let text_width = width.saturating_sub(graph_width);
237 if text_width < 40 {
238 None
240 } else {
241 Some(
242 textwrap::Options::new(text_width)
243 .initial_indent(indent1)
244 .subsequent_indent(indent2),
245 )
246 }
247 } else {
248 None
249 };
250 Ok(wrapping)
251}
252
253fn find_insert_idx(inserts: &[Vec<Occ>], child_idx: usize, parent_idx: usize) -> Option<usize> {
255 for (insert_idx, sub_entry) in inserts.iter().enumerate() {
256 for occ in sub_entry {
257 if let Occ::Range(i1, i2, _, _) = occ {
258 if *i1 == child_idx && *i2 == parent_idx {
259 return Some(insert_idx);
260 }
261 }
262 }
263 }
264 None
265}
266
267fn zig_zag_line(
269 grid: &mut Grid,
270 row123: (usize, usize, usize),
271 col12: (usize, usize),
272 is_merge: bool,
273 color: u8,
274 pers: u8,
275) {
276 let (row1, row2, row3) = row123;
277 let (col1, col2) = col12;
278 vline(grid, (row1, row2), col1, color, pers);
279 hline(grid, row2, (col2, col1), is_merge, color, pers);
280 vline(grid, (row2, row3), col2, color, pers);
281}
282
283fn vline(grid: &mut Grid, (from, to): (usize, usize), column: usize, color: u8, pers: u8) {
285 for i in (from + 1)..to {
286 let (curr, _, old_pers) = grid.get_tuple(column * 2, i);
287 let (new_col, new_pers) = if pers < old_pers {
288 (Some(color), Some(pers))
289 } else {
290 (None, None)
291 };
292 match curr {
293 DOT | CIRCLE => {}
294 HOR => {
295 grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
296 }
297 HOR_U | HOR_D => {
298 grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
299 }
300 CROSS | VER | VER_L | VER_R => grid.set_opt(column * 2, i, None, new_col, new_pers),
301 L_D | L_U => {
302 grid.set_opt(column * 2, i, Some(VER_L), new_col, new_pers);
303 }
304 R_D | R_U => {
305 grid.set_opt(column * 2, i, Some(VER_R), new_col, new_pers);
306 }
307 _ => {
308 grid.set_opt(column * 2, i, Some(VER), new_col, new_pers);
309 }
310 }
311 }
312}
313
314fn hline(
316 grid: &mut Grid,
317 index: usize,
318 (from, to): (usize, usize),
319 merge: bool,
320 color: u8,
321 pers: u8,
322) {
323 if from == to {
324 return;
325 }
326 let from_2 = from * 2;
327 let to_2 = to * 2;
328 if from < to {
329 for column in (from_2 + 1)..to_2 {
330 if merge && column == to_2 - 1 {
331 grid.set(column, index, ARR_R, color, pers);
332 } else {
333 let (curr, _, old_pers) = grid.get_tuple(column, index);
334 let (new_col, new_pers) = if pers < old_pers {
335 (Some(color), Some(pers))
336 } else {
337 (None, None)
338 };
339 match curr {
340 DOT | CIRCLE => {}
341 VER => grid.set_opt(column, index, Some(CROSS), None, None),
342 HOR | CROSS | HOR_U | HOR_D => {
343 grid.set_opt(column, index, None, new_col, new_pers)
344 }
345 L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
346 L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
347 _ => {
348 grid.set_opt(column, index, Some(HOR), new_col, new_pers);
349 }
350 }
351 }
352 }
353
354 let (left, _, old_pers) = grid.get_tuple(from_2, index);
355 let (new_col, new_pers) = if pers < old_pers {
356 (Some(color), Some(pers))
357 } else {
358 (None, None)
359 };
360 match left {
361 DOT | CIRCLE => {}
362 VER => grid.set_opt(from_2, index, Some(VER_R), new_col, new_pers),
363 VER_L => grid.set_opt(from_2, index, Some(CROSS), None, None),
364 VER_R => {}
365 HOR | L_U => grid.set_opt(from_2, index, Some(HOR_U), new_col, new_pers),
366 _ => {
367 grid.set_opt(from_2, index, Some(R_D), new_col, new_pers);
368 }
369 }
370
371 let (right, _, old_pers) = grid.get_tuple(to_2, index);
372 let (new_col, new_pers) = if pers < old_pers {
373 (Some(color), Some(pers))
374 } else {
375 (None, None)
376 };
377 match right {
378 DOT | CIRCLE => {}
379 VER => grid.set_opt(to_2, index, Some(VER_L), None, None),
380 VER_L | HOR_U => grid.set_opt(to_2, index, None, new_col, new_pers),
381 HOR | R_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
382 _ => {
383 grid.set_opt(to_2, index, Some(L_U), new_col, new_pers);
384 }
385 }
386 } else {
387 for column in (to_2 + 1)..from_2 {
388 if merge && column == to_2 + 1 {
389 grid.set(column, index, ARR_L, color, pers);
390 } else {
391 let (curr, _, old_pers) = grid.get_tuple(column, index);
392 let (new_col, new_pers) = if pers < old_pers {
393 (Some(color), Some(pers))
394 } else {
395 (None, None)
396 };
397 match curr {
398 DOT | CIRCLE => {}
399 VER => grid.set_opt(column, index, Some(CROSS), None, None),
400 HOR | CROSS | HOR_U | HOR_D => {
401 grid.set_opt(column, index, None, new_col, new_pers)
402 }
403 L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
404 L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
405 _ => {
406 grid.set_opt(column, index, Some(HOR), new_col, new_pers);
407 }
408 }
409 }
410 }
411
412 let (left, _, old_pers) = grid.get_tuple(to_2, index);
413 let (new_col, new_pers) = if pers < old_pers {
414 (Some(color), Some(pers))
415 } else {
416 (None, None)
417 };
418 match left {
419 DOT | CIRCLE => {}
420 VER => grid.set_opt(to_2, index, Some(VER_R), None, None),
421 VER_R => grid.set_opt(to_2, index, None, new_col, new_pers),
422 HOR | L_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
423 _ => {
424 grid.set_opt(to_2, index, Some(R_U), new_col, new_pers);
425 }
426 }
427
428 let (right, _, old_pers) = grid.get_tuple(from_2, index);
429 let (new_col, new_pers) = if pers < old_pers {
430 (Some(color), Some(pers))
431 } else {
432 (None, None)
433 };
434 match right {
435 DOT | CIRCLE => {}
436 VER => grid.set_opt(from_2, index, Some(VER_L), new_col, new_pers),
437 VER_R => grid.set_opt(from_2, index, Some(CROSS), None, None),
438 VER_L => grid.set_opt(from_2, index, None, new_col, new_pers),
439 HOR | R_D => grid.set_opt(from_2, index, Some(HOR_D), new_col, new_pers),
440 _ => {
441 grid.set_opt(from_2, index, Some(L_D), new_col, new_pers);
442 }
443 }
444 }
445}
446
447fn get_inserts(graph: &GitGraph, compact: bool) -> HashMap<usize, Vec<Vec<Occ>>> {
468 let mut inserts: HashMap<usize, Vec<Vec<Occ>>> = HashMap::new();
471
472 for (idx, info) in graph.commits.iter().enumerate() {
476 let column = graph.all_branches[info.branch_trace.unwrap()]
480 .visual
481 .column
482 .unwrap();
483
484 inserts.insert(idx, vec![vec![Occ::Commit(idx, column)]]);
485 }
486
487 for (idx, info) in graph.commits.iter().enumerate() {
491 if let Some(trace) = info.branch_trace {
493 let branch = &graph.all_branches[trace];
495 let column = branch.visual.column.unwrap();
497
498 for p in 0..2 {
500 let parent = info.parents[p];
501 let Some(par_oid) = parent else {
502 continue;
503 };
504 if let Some(par_idx) = graph.indices.get(&par_oid) {
506 let par_info = &graph.commits[*par_idx];
507 let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
508 let par_column = par_branch.visual.column.unwrap();
509 let column_range = sorted(column, par_column);
511
512 if column != par_column {
515 let split_index = super::get_deviate_index(graph, idx, *par_idx);
519 match inserts.entry(split_index) {
521 Occupied(mut entry) => {
524 let mut insert_at = entry.get().len();
527 for (insert_idx, sub_entry) in entry.get().iter().enumerate() {
528 let mut occ = false;
529 for other_range in sub_entry {
531 if other_range.overlaps(&column_range) {
533 match other_range {
534 Occ::Commit(target_index, _) => {
536 if !compact
540 || !info.is_merge
541 || idx != *target_index
542 || p == 0
543 {
544 occ = true;
545 break;
546 }
547 }
548 Occ::Range(o_idx, o_par_idx, _, _) => {
550 if idx != *o_idx && par_idx != o_par_idx {
552 occ = true;
553 break;
554 }
555 }
556 }
557 }
558 }
559 if !occ {
561 insert_at = insert_idx;
562 break;
563 }
564 }
565 let vec = entry.get_mut();
567 if insert_at == vec.len() {
569 vec.push(vec![Occ::Range(
570 idx,
571 *par_idx,
572 column_range.0,
573 column_range.1,
574 )]);
575 } else {
576 vec[insert_at].push(Occ::Range(
578 idx,
579 *par_idx,
580 column_range.0,
581 column_range.1,
582 ));
583 }
584 }
585 Vacant(entry) => {
587 entry.insert(vec![vec![Occ::Range(
589 idx,
590 *par_idx,
591 column_range.0,
592 column_range.1,
593 )]]);
594 }
595 }
596 }
597 }
598 }
599 }
600 }
601
602 inserts
604}
605
606fn print_graph(
608 characters: &Characters,
609 grid: &Grid,
610 text_lines: Vec<Option<String>>,
611 color: bool,
612) -> (Vec<String>, Vec<String>) {
613 let mut g_lines = vec![];
614 let mut t_lines = vec![];
615
616 for (row, line) in grid.data.chunks(grid.width).zip(text_lines.into_iter()) {
617 let mut g_out = String::new();
618 let mut t_out = String::new();
619
620 if color {
621 for cell in row {
622 let chars = cell.char(characters);
623 if cell.character == SPACE {
624 write!(g_out, "{}", chars)
625 } else {
626 write!(g_out, "{}", chars.to_string().fixed(cell.color))
627 }
628 .unwrap();
629 }
630 } else {
631 let str = row
632 .iter()
633 .map(|cell| cell.char(characters))
634 .collect::<String>();
635 write!(g_out, "{}", str).unwrap();
636 }
637
638 if let Some(line) = line {
639 write!(t_out, "{}", line).unwrap();
640 }
641
642 g_lines.push(g_out);
643 t_lines.push(t_out);
644 }
645
646 (g_lines, t_lines)
647}
648
649fn format(
651 format: &CommitFormat,
652 graph: &GitGraph,
653 info: &CommitInfo,
654 head: Option<&HeadInfo>,
655 color: bool,
656 wrapping: &Option<Options>,
657) -> Result<Vec<String>, String> {
658 let commit = graph
659 .repository
660 .find_commit(info.oid)
661 .map_err(|err| err.message().to_string())?;
662
663 let branch_str = format_branches(graph, info, head, color);
664
665 let hash_color = if color { Some(HASH_COLOR) } else { None };
666
667 crate::print::format::format(&commit, branch_str, wrapping, hash_color, format)
668}
669
670pub fn format_branches(
672 graph: &GitGraph,
673 info: &CommitInfo,
674 head: Option<&HeadInfo>,
675 color: bool,
676) -> String {
677 let curr_color = info
678 .branch_trace
679 .map(|branch_idx| &graph.all_branches[branch_idx].visual.term_color);
680
681 let mut branch_str = String::new();
682
683 let head_str = "HEAD ->";
684 if let Some(head) = head {
685 if !head.is_branch {
686 if color {
687 write!(branch_str, " {}", head_str.fixed(HEAD_COLOR))
688 } else {
689 write!(branch_str, " {}", head_str)
690 }
691 .unwrap();
692 }
693 }
694
695 if !info.branches.is_empty() {
696 write!(branch_str, " (").unwrap();
697
698 let branches = info.branches.iter().sorted_by_key(|br| {
699 if let Some(head) = head {
700 head.name != graph.all_branches[**br].name
701 } else {
702 false
703 }
704 });
705
706 for (idx, branch_index) in branches.enumerate() {
707 let branch = &graph.all_branches[*branch_index];
708 let branch_color = branch.visual.term_color;
709
710 if let Some(head) = head {
711 if idx == 0 && head.is_branch {
712 if color {
713 write!(branch_str, "{} ", head_str.fixed(14))
714 } else {
715 write!(branch_str, "{} ", head_str)
716 }
717 .unwrap();
718 }
719 }
720
721 if color {
722 write!(branch_str, "{}", &branch.name.fixed(branch_color))
723 } else {
724 write!(branch_str, "{}", &branch.name)
725 }
726 .unwrap();
727
728 if idx < info.branches.len() - 1 {
729 write!(branch_str, ", ").unwrap();
730 }
731 }
732 write!(branch_str, ")").unwrap();
733 }
734
735 if !info.tags.is_empty() {
736 write!(branch_str, " [").unwrap();
737 for (idx, tag_index) in info.tags.iter().enumerate() {
738 let tag = &graph.all_branches[*tag_index];
739 let tag_color = curr_color.unwrap_or(&tag.visual.term_color);
740
741 let tag_name = &tag.name[5..];
742 if color {
743 write!(branch_str, "{}", tag_name.fixed(*tag_color))
744 } else {
745 write!(branch_str, "{}", tag_name)
746 }
747 .unwrap();
748
749 if idx < info.tags.len() - 1 {
750 write!(branch_str, ", ").unwrap();
751 }
752 }
753 write!(branch_str, "]").unwrap();
754 }
755
756 branch_str
757}
758
759enum Occ {
761 Commit(usize, usize), Range(usize, usize, usize, usize), }
775
776impl Occ {
777 fn overlaps(&self, (start, end): &(usize, usize)) -> bool {
778 match self {
779 Occ::Commit(_, col) => start <= col && end >= col,
780 Occ::Range(_, _, s, e) => s <= end && e >= start,
781 }
782 }
783}
784
785fn sorted(v1: usize, v2: usize) -> (usize, usize) {
787 if v2 > v1 {
788 (v1, v2)
789 } else {
790 (v2, v1)
791 }
792}
793
794#[derive(Clone, Copy)]
796struct GridCell {
797 character: u8,
799 color: u8,
801 pers: u8,
803}
804
805impl GridCell {
806 pub fn char(&self, characters: &Characters) -> char {
807 characters.chars[self.character as usize]
808 }
809}
810
811struct Grid {
815 width: usize,
816 height: usize,
817
818 data: Vec<GridCell>,
820}
821
822impl Grid {
823 pub fn new(width: usize, height: usize, initial: GridCell) -> Self {
824 Grid {
825 width,
826 height,
827 data: vec![initial; width * height],
828 }
829 }
830
831 pub fn reverse(&mut self) {
832 self.data.reverse();
833 }
834 pub fn index(&self, x: usize, y: usize) -> usize {
836 y * self.width + x
837 }
838 pub fn get_tuple(&self, x: usize, y: usize) -> (u8, u8, u8) {
839 let v = self.data[self.index(x, y)];
840 (v.character, v.color, v.pers)
841 }
842 pub fn set(&mut self, x: usize, y: usize, character: u8, color: u8, pers: u8) {
843 let idx = self.index(x, y);
844 self.data[idx] = GridCell {
845 character,
846 color,
847 pers,
848 };
849 }
850 pub fn set_opt(
851 &mut self,
852 x: usize,
853 y: usize,
854 character: Option<u8>,
855 color: Option<u8>,
856 pers: Option<u8>,
857 ) {
858 let idx = self.index(x, y);
859 let cell = &mut self.data[idx];
860 if let Some(character) = character {
861 cell.character = character;
862 }
863 if let Some(color) = color {
864 cell.color = color;
865 }
866 if let Some(pers) = pers {
867 cell.pers = pers;
868 }
869 }
870}