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
36type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
37
38pub 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
202fn 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
235fn 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
266fn 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
399fn 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
495fn 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
541fn 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
562pub 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
650enum 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
665fn sorted(v1: usize, v2: usize) -> (usize, usize) {
667 if v2 > v1 {
668 (v1, v2)
669 } else {
670 (v2, v1)
671 }
672}
673
674#[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}