git_graph/print/
unicode.rs

1//! Create graphs in SVG format (Scalable Vector Graphics).
2
3use 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
36type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
37
38/// Creates a text-based visual representation of a graph.
39pub fn print_unicode(graph: &GitGraph, settings: &Settings) -> Result<UnicodeGraphInfo, String> {
40    let num_cols = 2 * graph
41        .all_branches
42        .iter()
43        .map(|b| b.visual.column.unwrap_or(0))
44        .max()
45        .unwrap()
46        + 1;
47
48    let head_idx = graph.indices.get(&graph.head.oid);
49
50    let inserts = get_inserts(graph, settings.compact);
51
52    let (indent1, indent2) = if let Some((_, ind1, ind2)) = settings.wrapping {
53        (" ".repeat(ind1.unwrap_or(0)), " ".repeat(ind2.unwrap_or(0)))
54    } else {
55        ("".to_string(), "".to_string())
56    };
57
58    let wrap_options = if let Some((width, _, _)) = settings.wrapping {
59        create_wrapping_options(width, &indent1, &indent2, num_cols + 4)?
60    } else {
61        None
62    };
63
64    let mut index_map = vec![];
65    let mut text_lines = vec![];
66    let mut offset = 0;
67    for (idx, info) in graph.commits.iter().enumerate() {
68        index_map.push(idx + offset);
69        let cnt_inserts = if let Some(inserts) = inserts.get(&idx) {
70            inserts
71                .iter()
72                .filter(|vec| {
73                    vec.iter().all(|occ| match occ {
74                        Occ::Commit(_, _) => false,
75                        Occ::Range(_, _, _, _) => true,
76                    })
77                })
78                .count()
79        } else {
80            0
81        };
82
83        let head = if head_idx.map_or(false, |h| h == &idx) {
84            Some(&graph.head)
85        } else {
86            None
87        };
88
89        let lines = format(
90            &settings.format,
91            graph,
92            info,
93            head,
94            settings.colored,
95            &wrap_options,
96        )?;
97
98        let num_lines = if lines.is_empty() { 0 } else { lines.len() - 1 };
99        let max_inserts = max(cnt_inserts, num_lines);
100        let add_lines = max_inserts - num_lines;
101
102        text_lines.extend(lines.into_iter().map(Some));
103        text_lines.extend((0..add_lines).map(|_| None));
104
105        offset += max_inserts;
106    }
107
108    let mut grid = Grid::new(
109        num_cols,
110        graph.commits.len() + offset,
111        [SPACE, WHITE, settings.branches.persistence.len() as u8 + 2],
112    );
113
114    for (idx, info) in graph.commits.iter().enumerate() {
115        if let Some(trace) = info.branch_trace {
116            let branch = &graph.all_branches[trace];
117            let column = branch.visual.column.unwrap();
118            let idx_map = index_map[idx];
119
120            let branch_color = branch.visual.term_color;
121
122            grid.set(
123                column * 2,
124                idx_map,
125                if info.is_merge { CIRCLE } else { DOT },
126                branch_color,
127                branch.persistence,
128            );
129
130            for p in 0..2 {
131                if let Some(par_oid) = info.parents[p] {
132                    if let Some(par_idx) = graph.indices.get(&par_oid) {
133                        let par_idx_map = index_map[*par_idx];
134                        let par_info = &graph.commits[*par_idx];
135                        let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
136                        let par_column = par_branch.visual.column.unwrap();
137
138                        let (color, pers) = if info.is_merge {
139                            (par_branch.visual.term_color, par_branch.persistence)
140                        } else {
141                            (branch_color, branch.persistence)
142                        };
143
144                        if branch.visual.column == par_branch.visual.column {
145                            if par_idx_map > idx_map + 1 {
146                                vline(&mut grid, (idx_map, par_idx_map), column, color, pers);
147                            }
148                        } else {
149                            let split_index = super::get_deviate_index(graph, idx, *par_idx);
150                            let split_idx_map = index_map[split_index];
151                            let inserts = &inserts[&split_index];
152                            for (insert_idx, sub_entry) in inserts.iter().enumerate() {
153                                for occ in sub_entry {
154                                    match occ {
155                                        Occ::Commit(_, _) => {}
156                                        Occ::Range(i1, i2, _, _) => {
157                                            if *i1 == idx && i2 == par_idx {
158                                                vline(
159                                                    &mut grid,
160                                                    (idx_map, split_idx_map + insert_idx),
161                                                    column,
162                                                    color,
163                                                    pers,
164                                                );
165                                                hline(
166                                                    &mut grid,
167                                                    split_idx_map + insert_idx,
168                                                    (par_column, column),
169                                                    info.is_merge && p > 0,
170                                                    color,
171                                                    pers,
172                                                );
173                                                vline(
174                                                    &mut grid,
175                                                    (split_idx_map + insert_idx, par_idx_map),
176                                                    par_column,
177                                                    color,
178                                                    pers,
179                                                );
180                                            }
181                                        }
182                                    }
183                                }
184                            }
185                        }
186                    }
187                }
188            }
189        }
190    }
191
192    if settings.reverse_commit_order {
193        text_lines.reverse();
194        grid.reverse();
195    }
196
197    let lines = print_graph(&settings.characters, &grid, text_lines, settings.colored);
198
199    Ok((lines.0, lines.1, index_map))
200}
201
202/// Create `textwrap::Options` from width and indent.
203fn create_wrapping_options<'a>(
204    width: Option<usize>,
205    indent1: &'a str,
206    indent2: &'a str,
207    graph_width: usize,
208) -> Result<Option<Options<'a>>, String> {
209    let wrapping = if let Some(width) = width {
210        Some(
211            textwrap::Options::new(width)
212                .initial_indent(indent1)
213                .subsequent_indent(indent2),
214        )
215    } else if atty::is(atty::Stream::Stdout) {
216        let width = crossterm::terminal::size()
217            .map_err(|err| err.to_string())?
218            .0;
219        let width = if width as usize > graph_width {
220            width as usize - graph_width
221        } else {
222            1
223        };
224        Some(
225            textwrap::Options::new(width)
226                .initial_indent(indent1)
227                .subsequent_indent(indent2),
228        )
229    } else {
230        None
231    };
232    Ok(wrapping)
233}
234
235/// Draws a vertical line
236fn vline(grid: &mut Grid, (from, to): (usize, usize), column: usize, color: u8, pers: u8) {
237    for i in (from + 1)..to {
238        let (curr, _, old_pers) = grid.get_tuple(column * 2, i);
239        let (new_col, new_pers) = if pers < old_pers {
240            (Some(color), Some(pers))
241        } else {
242            (None, None)
243        };
244        match curr {
245            DOT | CIRCLE => {}
246            HOR => {
247                grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
248            }
249            HOR_U | HOR_D => {
250                grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
251            }
252            CROSS | VER | VER_L | VER_R => grid.set_opt(column * 2, i, None, new_col, new_pers),
253            L_D | L_U => {
254                grid.set_opt(column * 2, i, Some(VER_L), new_col, new_pers);
255            }
256            R_D | R_U => {
257                grid.set_opt(column * 2, i, Some(VER_R), new_col, new_pers);
258            }
259            _ => {
260                grid.set_opt(column * 2, i, Some(VER), new_col, new_pers);
261            }
262        }
263    }
264}
265
266/// Draws a horizontal line
267fn hline(
268    grid: &mut Grid,
269    index: usize,
270    (from, to): (usize, usize),
271    merge: bool,
272    color: u8,
273    pers: u8,
274) {
275    if from == to {
276        return;
277    }
278    let from_2 = from * 2;
279    let to_2 = to * 2;
280    if from < to {
281        for column in (from_2 + 1)..to_2 {
282            if merge && column == to_2 - 1 {
283                grid.set(column, index, ARR_R, color, pers);
284            } else {
285                let (curr, _, old_pers) = grid.get_tuple(column, index);
286                let (new_col, new_pers) = if pers < old_pers {
287                    (Some(color), Some(pers))
288                } else {
289                    (None, None)
290                };
291                match curr {
292                    DOT | CIRCLE => {}
293                    VER => grid.set_opt(column, index, Some(CROSS), None, None),
294                    HOR | CROSS | HOR_U | HOR_D => {
295                        grid.set_opt(column, index, None, new_col, new_pers)
296                    }
297                    L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
298                    L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
299                    _ => {
300                        grid.set_opt(column, index, Some(HOR), new_col, new_pers);
301                    }
302                }
303            }
304        }
305
306        let (left, _, old_pers) = grid.get_tuple(from_2, index);
307        let (new_col, new_pers) = if pers < old_pers {
308            (Some(color), Some(pers))
309        } else {
310            (None, None)
311        };
312        match left {
313            DOT | CIRCLE => {}
314            VER => grid.set_opt(from_2, index, Some(VER_R), new_col, new_pers),
315            VER_L => grid.set_opt(from_2, index, Some(CROSS), None, None),
316            VER_R => {}
317            HOR | L_U => grid.set_opt(from_2, index, Some(HOR_U), new_col, new_pers),
318            _ => {
319                grid.set_opt(from_2, index, Some(R_D), new_col, new_pers);
320            }
321        }
322
323        let (right, _, old_pers) = grid.get_tuple(to_2, index);
324        let (new_col, new_pers) = if pers < old_pers {
325            (Some(color), Some(pers))
326        } else {
327            (None, None)
328        };
329        match right {
330            DOT | CIRCLE => {}
331            VER => grid.set_opt(to_2, index, Some(VER_L), None, None),
332            VER_L | HOR_U => grid.set_opt(to_2, index, None, new_col, new_pers),
333            HOR | R_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
334            _ => {
335                grid.set_opt(to_2, index, Some(L_U), new_col, new_pers);
336            }
337        }
338    } else {
339        for column in (to_2 + 1)..from_2 {
340            if merge && column == to_2 + 1 {
341                grid.set(column, index, ARR_L, color, pers);
342            } else {
343                let (curr, _, old_pers) = grid.get_tuple(column, index);
344                let (new_col, new_pers) = if pers < old_pers {
345                    (Some(color), Some(pers))
346                } else {
347                    (None, None)
348                };
349                match curr {
350                    DOT | CIRCLE => {}
351                    VER => grid.set_opt(column, index, Some(CROSS), None, None),
352                    HOR | CROSS | HOR_U | HOR_D => {
353                        grid.set_opt(column, index, None, new_col, new_pers)
354                    }
355                    L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
356                    L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
357                    _ => {
358                        grid.set_opt(column, index, Some(HOR), new_col, new_pers);
359                    }
360                }
361            }
362        }
363
364        let (left, _, old_pers) = grid.get_tuple(to_2, index);
365        let (new_col, new_pers) = if pers < old_pers {
366            (Some(color), Some(pers))
367        } else {
368            (None, None)
369        };
370        match left {
371            DOT | CIRCLE => {}
372            VER => grid.set_opt(to_2, index, Some(VER_R), None, None),
373            VER_R => grid.set_opt(to_2, index, None, new_col, new_pers),
374            HOR | L_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
375            _ => {
376                grid.set_opt(to_2, index, Some(R_U), new_col, new_pers);
377            }
378        }
379
380        let (right, _, old_pers) = grid.get_tuple(from_2, index);
381        let (new_col, new_pers) = if pers < old_pers {
382            (Some(color), Some(pers))
383        } else {
384            (None, None)
385        };
386        match right {
387            DOT | CIRCLE => {}
388            VER => grid.set_opt(from_2, index, Some(VER_L), new_col, new_pers),
389            VER_R => grid.set_opt(from_2, index, Some(CROSS), None, None),
390            VER_L => grid.set_opt(from_2, index, None, new_col, new_pers),
391            HOR | R_D => grid.set_opt(from_2, index, Some(HOR_D), new_col, new_pers),
392            _ => {
393                grid.set_opt(from_2, index, Some(L_D), new_col, new_pers);
394            }
395        }
396    }
397}
398
399/// Calculates required additional rows
400fn get_inserts(graph: &GitGraph, compact: bool) -> HashMap<usize, Vec<Vec<Occ>>> {
401    let mut inserts: HashMap<usize, Vec<Vec<Occ>>> = HashMap::new();
402
403    for (idx, info) in graph.commits.iter().enumerate() {
404        let column = graph.all_branches[info.branch_trace.unwrap()]
405            .visual
406            .column
407            .unwrap();
408
409        inserts.insert(idx, vec![vec![Occ::Commit(idx, column)]]);
410    }
411
412    for (idx, info) in graph.commits.iter().enumerate() {
413        if let Some(trace) = info.branch_trace {
414            let branch = &graph.all_branches[trace];
415            let column = branch.visual.column.unwrap();
416
417            for p in 0..2 {
418                if let Some(par_oid) = info.parents[p] {
419                    if let Some(par_idx) = graph.indices.get(&par_oid) {
420                        let par_info = &graph.commits[*par_idx];
421                        let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
422                        let par_column = par_branch.visual.column.unwrap();
423                        let column_range = sorted(column, par_column);
424
425                        if column != par_column {
426                            let split_index = super::get_deviate_index(graph, idx, *par_idx);
427                            match inserts.entry(split_index) {
428                                Occupied(mut entry) => {
429                                    let mut insert_at = entry.get().len();
430                                    for (insert_idx, sub_entry) in entry.get().iter().enumerate() {
431                                        let mut occ = false;
432                                        for other_range in sub_entry {
433                                            if other_range.overlaps(&column_range) {
434                                                match other_range {
435                                                    Occ::Commit(target_index, _) => {
436                                                        if !compact
437                                                            || !info.is_merge
438                                                            || idx != *target_index
439                                                            || p == 0
440                                                        {
441                                                            occ = true;
442                                                            break;
443                                                        }
444                                                    }
445                                                    Occ::Range(o_idx, o_par_idx, _, _) => {
446                                                        if idx != *o_idx && par_idx != o_par_idx {
447                                                            occ = true;
448                                                            break;
449                                                        }
450                                                    }
451                                                }
452                                            }
453                                        }
454                                        if !occ {
455                                            insert_at = insert_idx;
456                                            break;
457                                        }
458                                    }
459                                    let vec = entry.get_mut();
460                                    if insert_at == vec.len() {
461                                        vec.push(vec![Occ::Range(
462                                            idx,
463                                            *par_idx,
464                                            column_range.0,
465                                            column_range.1,
466                                        )]);
467                                    } else {
468                                        vec[insert_at].push(Occ::Range(
469                                            idx,
470                                            *par_idx,
471                                            column_range.0,
472                                            column_range.1,
473                                        ));
474                                    }
475                                }
476                                Vacant(entry) => {
477                                    entry.insert(vec![vec![Occ::Range(
478                                        idx,
479                                        *par_idx,
480                                        column_range.0,
481                                        column_range.1,
482                                    )]]);
483                                }
484                            }
485                        }
486                    }
487                }
488            }
489        }
490    }
491
492    inserts
493}
494
495/// Creates the complete graph visualization, incl. formatter commits.
496fn print_graph(
497    characters: &Characters,
498    grid: &Grid,
499    text_lines: Vec<Option<String>>,
500    color: bool,
501) -> (Vec<String>, Vec<String>) {
502    let mut g_lines = vec![];
503    let mut t_lines = vec![];
504
505    for (row, line) in grid.data.chunks(grid.width).zip(text_lines.into_iter()) {
506        let mut g_out = String::new();
507        let mut t_out = String::new();
508
509        if color {
510            for arr in row {
511                if arr[0] == SPACE {
512                    write!(g_out, "{}", characters.chars[arr[0] as usize])
513                } else {
514                    write!(
515                        g_out,
516                        "{}",
517                        Paint::fixed(arr[1], characters.chars[arr[0] as usize])
518                    )
519                }
520                .unwrap();
521            }
522        } else {
523            let str = row
524                .iter()
525                .map(|arr| characters.chars[arr[0] as usize])
526                .collect::<String>();
527            write!(g_out, "{}", str).unwrap();
528        }
529
530        if let Some(line) = line {
531            write!(t_out, "{}", line).unwrap();
532        }
533
534        g_lines.push(g_out);
535        t_lines.push(t_out);
536    }
537
538    (g_lines, t_lines)
539}
540
541/// Format a commit.
542fn format(
543    format: &CommitFormat,
544    graph: &GitGraph,
545    info: &CommitInfo,
546    head: Option<&HeadInfo>,
547    color: bool,
548    wrapping: &Option<Options>,
549) -> Result<Vec<String>, String> {
550    let commit = graph
551        .repository
552        .find_commit(info.oid)
553        .map_err(|err| err.message().to_string())?;
554
555    let branch_str = format_branches(graph, info, head, color);
556
557    let hash_color = if color { Some(HASH_COLOR) } else { None };
558
559    crate::print::format::format(&commit, branch_str, wrapping, hash_color, format)
560}
561
562/// Format branches and tags.
563pub fn format_branches(
564    graph: &GitGraph,
565    info: &CommitInfo,
566    head: Option<&HeadInfo>,
567    color: bool,
568) -> String {
569    let curr_color = info
570        .branch_trace
571        .map(|branch_idx| &graph.all_branches[branch_idx].visual.term_color);
572
573    let mut branch_str = String::new();
574
575    let head_str = "HEAD ->";
576    if let Some(head) = head {
577        if !head.is_branch {
578            if color {
579                write!(branch_str, " {}", Paint::fixed(HEAD_COLOR, head_str))
580            } else {
581                write!(branch_str, " {}", head_str)
582            }
583            .unwrap();
584        }
585    }
586
587    if !info.branches.is_empty() {
588        write!(branch_str, " (").unwrap();
589
590        let branches = info.branches.iter().sorted_by_key(|br| {
591            if let Some(head) = head {
592                head.name != graph.all_branches[**br].name
593            } else {
594                false
595            }
596        });
597
598        for (idx, branch_index) in branches.enumerate() {
599            let branch = &graph.all_branches[*branch_index];
600            let branch_color = branch.visual.term_color;
601
602            if let Some(head) = head {
603                if idx == 0 && head.is_branch {
604                    if color {
605                        write!(branch_str, "{} ", Paint::fixed(14, head_str))
606                    } else {
607                        write!(branch_str, "{} ", head_str)
608                    }
609                    .unwrap();
610                }
611            }
612
613            if color {
614                write!(branch_str, "{}", Paint::fixed(branch_color, &branch.name))
615            } else {
616                write!(branch_str, "{}", &branch.name)
617            }
618            .unwrap();
619
620            if idx < info.branches.len() - 1 {
621                write!(branch_str, ", ").unwrap();
622            }
623        }
624        write!(branch_str, ")").unwrap();
625    }
626
627    if !info.tags.is_empty() {
628        write!(branch_str, " [").unwrap();
629        for (idx, tag_index) in info.tags.iter().enumerate() {
630            let tag = &graph.all_branches[*tag_index];
631            let tag_color = curr_color.unwrap_or(&tag.visual.term_color);
632
633            if color {
634                write!(branch_str, "{}", Paint::fixed(*tag_color, &tag.name[5..]))
635            } else {
636                write!(branch_str, "{}", &tag.name[5..])
637            }
638            .unwrap();
639
640            if idx < info.tags.len() - 1 {
641                write!(branch_str, ", ").unwrap();
642            }
643        }
644        write!(branch_str, "]").unwrap();
645    }
646
647    branch_str
648}
649
650/// Occupied row ranges
651enum Occ {
652    Commit(usize, usize),
653    Range(usize, usize, usize, usize),
654}
655
656impl Occ {
657    fn overlaps(&self, (start, end): &(usize, usize)) -> bool {
658        match self {
659            Occ::Commit(_, col) => start <= col && end >= col,
660            Occ::Range(_, _, s, e) => s <= end && e >= start,
661        }
662    }
663}
664
665/// Sorts two numbers in ascending order
666fn sorted(v1: usize, v2: usize) -> (usize, usize) {
667    if v2 > v1 {
668        (v1, v2)
669    } else {
670        (v2, v1)
671    }
672}
673
674/// Two-dimensional grid with 3 layers, used to produce the graph representation.
675#[allow(dead_code)]
676struct Grid {
677    width: usize,
678    height: usize,
679    data: Vec<[u8; 3]>,
680}
681
682impl Grid {
683    pub fn new(width: usize, height: usize, initial: [u8; 3]) -> Self {
684        Grid {
685            width,
686            height,
687            data: vec![initial; width * height],
688        }
689    }
690
691    pub fn reverse(&mut self) {
692        self.data.reverse();
693    }
694    pub fn index(&self, x: usize, y: usize) -> usize {
695        y * self.width + x
696    }
697    pub fn get_tuple(&self, x: usize, y: usize) -> (u8, u8, u8) {
698        let v = self.data[self.index(x, y)];
699        (v[0], v[1], v[2])
700    }
701    pub fn set(&mut self, x: usize, y: usize, character: u8, color: u8, pers: u8) {
702        let idx = self.index(x, y);
703        self.data[idx] = [character, color, pers];
704    }
705    pub fn set_opt(
706        &mut self,
707        x: usize,
708        y: usize,
709        character: Option<u8>,
710        color: Option<u8>,
711        pers: Option<u8>,
712    ) {
713        let idx = self.index(x, y);
714        let arr = &mut self.data[idx];
715        if let Some(character) = character {
716            arr[0] = character;
717        }
718        if let Some(color) = color {
719            arr[1] = color;
720        }
721        if let Some(pers) = pers {
722            arr[2] = pers;
723        }
724    }
725}