1use std::path::Path;
2use std::process::Command;
3
4#[derive(Debug, Clone)]
5pub struct DiffFile {
6 pub path: String,
7 pub status: char,
8 pub additions: usize,
9 pub deletions: usize,
10}
11
12#[derive(Debug, Clone, Default)]
13pub struct RepoStats {
14 pub additions: usize,
15 pub deletions: usize,
16 pub file_count: usize,
17}
18
19fn run_git(repo_path: &Path, args: &[&str]) -> Option<String> {
20 let output = Command::new("git")
21 .arg("-C")
22 .arg(repo_path)
23 .args(args)
24 .output()
25 .ok()?;
26 output
27 .status
28 .success()
29 .then(|| String::from_utf8_lossy(&output.stdout).into_owned())
30}
31
32fn resolve_base_ref(repo_path: &Path, base: &str) -> String {
33 let check_ref =
34 |r: &str| -> bool { run_git(repo_path, &["rev-parse", "--verify", r]).is_some() };
35
36 if check_ref(base) {
37 return base.to_string();
38 }
39
40 let origin_ref = format!("origin/{}", base);
41 if check_ref(&origin_ref) {
42 return origin_ref;
43 }
44
45 base.to_string()
46}
47
48pub fn get_diff_files(repo_path: &Path, base: &str) -> Vec<DiffFile> {
49 let base_ref = resolve_base_ref(repo_path, base);
50 let range = format!("{}..HEAD", base_ref);
51 let Some(stdout) = run_git(repo_path, &["diff", "--numstat", &range]) else {
52 return Vec::new();
53 };
54
55 stdout
56 .lines()
57 .filter_map(|line| {
58 let parts: Vec<&str> = line.split('\t').collect();
59 if parts.len() >= 3 {
60 let additions = parts[0].parse().unwrap_or(0);
61 let deletions = parts[1].parse().unwrap_or(0);
62 let path = parts[2].to_string();
63 let status = get_file_status(repo_path, &base_ref, &path);
64 Some(DiffFile {
65 path,
66 status,
67 additions,
68 deletions,
69 })
70 } else {
71 None
72 }
73 })
74 .collect()
75}
76
77fn get_file_status(repo_path: &Path, base_ref: &str, file_path: &str) -> char {
78 let range = format!("{}..HEAD", base_ref);
79 run_git(
80 repo_path,
81 &["diff", "--name-status", &range, "--", file_path],
82 )
83 .and_then(|s| s.chars().next())
84 .unwrap_or('M')
85}
86
87pub fn get_repo_stats(repo_path: &Path, base: &str) -> RepoStats {
88 let base_ref = resolve_base_ref(repo_path, base);
89 let range = format!("{}..HEAD", base_ref);
90 run_git(repo_path, &["diff", "--shortstat", &range])
91 .map(|s| parse_shortstat(&s))
92 .unwrap_or_default()
93}
94
95fn parse_shortstat(s: &str) -> RepoStats {
96 let mut stats = RepoStats::default();
97
98 for part in s.split(',') {
99 let part = part.trim();
100 if part.contains("file") {
101 if let Some(n) = part.split_whitespace().next() {
102 stats.file_count = n.parse().unwrap_or(0);
103 }
104 } else if part.contains("insertion") {
105 if let Some(n) = part.split_whitespace().next() {
106 stats.additions = n.parse().unwrap_or(0);
107 }
108 } else if part.contains("deletion")
109 && let Some(n) = part.split_whitespace().next()
110 {
111 stats.deletions = n.parse().unwrap_or(0);
112 }
113 }
114
115 stats
116}
117
118pub fn get_diff_content(repo_path: &Path, base: &str, file_path: &str) -> Vec<String> {
119 let base_ref = resolve_base_ref(repo_path, base);
120 let range = format!("{}..HEAD", base_ref);
121 run_git(
122 repo_path,
123 &["diff", "--color=always", &range, "--", file_path],
124 )
125 .map(|s| s.lines().map(String::from).collect())
126 .unwrap_or_default()
127}