gitfetch_rs/utils/
timeline.rs

1use anyhow::Result;
2use regex::Regex;
3use std::process::Command;
4
5pub fn get_git_timeline_graph(vertical: bool) -> Result<String> {
6  let output = Command::new("git")
7    .args(&[
8      "--no-pager",
9      "log",
10      "--color=always",
11      "--graph",
12      "--all",
13      "--pretty=format:\"\"",
14    ])
15    .output()?;
16
17  if !output.status.success() {
18    return Err(anyhow::anyhow!("Failed to execute git log"));
19  }
20
21  let mut text = String::from_utf8_lossy(&output.stdout)
22    .to_string()
23    .replace("\"", "");
24
25  if vertical {
26    return Ok(text);
27  }
28
29  // Horizontal mode: rotate the graph
30  text = text
31    .chars()
32    .map(|c| match c {
33      '\\' => '/',
34      '/' => '\\',
35      '|' => '-',
36      _ => c,
37    })
38    .collect();
39
40  let ansi_pattern = Regex::new(r"\x1b\[[0-9;]*m")?;
41
42  let lines: Vec<_> = text.lines().collect();
43  let mut parsed_lines = Vec::new();
44
45  for line in lines {
46    let parts: Vec<_> = ansi_pattern.split(line).collect();
47    let codes: Vec<_> = ansi_pattern.find_iter(line).map(|m| m.as_str()).collect();
48
49    let mut current_color = String::new();
50    let mut parsed = Vec::new();
51
52    for (i, seg) in parts.iter().enumerate() {
53      for ch in seg.chars() {
54        parsed.push((ch, current_color.clone()));
55      }
56      if i < codes.len() {
57        let code = codes[i];
58        current_color = if code == "\x1b[0m" {
59          String::new()
60        } else {
61          code.to_string()
62        };
63      }
64    }
65    parsed_lines.push(parsed);
66  }
67
68  if parsed_lines.is_empty() {
69    return Ok(String::new());
70  }
71
72  let max_len = parsed_lines
73    .iter()
74    .map(|line| line.len())
75    .max()
76    .unwrap_or(0);
77  let padded: Vec<_> = parsed_lines
78    .iter()
79    .map(|line| {
80      let mut padded = line.clone();
81      padded.resize(max_len, (' ', String::new()));
82      padded
83    })
84    .collect();
85
86  let mut rotated = Vec::new();
87  for col in (0..max_len).rev() {
88    let mut new_row = Vec::new();
89    for row in 0..padded.len() {
90      new_row.push(padded[row][col].clone());
91    }
92    rotated.push(new_row);
93  }
94
95  let mut out_lines = Vec::new();
96  for row in rotated {
97    let mut cur_color = String::new();
98    let mut out_line = String::new();
99
100    for (ch, color) in row {
101      if color != cur_color {
102        out_line.push_str(&color);
103        cur_color = color.clone();
104      }
105      out_line.push(ch);
106    }
107
108    if !cur_color.is_empty() {
109      out_line.push_str("\x1b[0m");
110    }
111    out_lines.push(out_line);
112  }
113
114  Ok(out_lines.join("\n"))
115}