1use crate::stats::{AuthorStats, UserStats};
2use std::io::{self, Write};
3use std::time::Instant;
4
5pub fn print_table(
7 data: Vec<(String, AuthorStats)>,
8 total_loc: usize,
9 total_commits: usize,
10 total_files: usize,
11) {
12 println!(
13 "| {:<28} | {:>7} | {:>7} | {:>7} | {:<15} |",
14 "Author", "loc", "coms", "fils", "distribution"
15 );
16 println!(
17 "|:{:-<28}|{:->8}|{:->8}|{:->8}|:{:-<16}|",
18 "", "", "", "", ""
19 );
20
21 for (author, stats) in &data {
22 let loc_dist = if total_loc > 0 {
23 (stats.loc as f32 / total_loc as f32) * 100.0
24 } else {
25 0.0
26 };
27 let coms_dist = if total_commits > 0 {
28 (stats.commits as f32 / total_commits as f32) * 100.0
29 } else {
30 0.0
31 };
32 let fils_dist = if total_files > 0 {
33 (stats.files.len() as f32 / total_files as f32) * 100.0
34 } else {
35 0.0
36 };
37
38 let distribution_str = format!("{:.1}/{:.1}/{:.1}", loc_dist, coms_dist, fils_dist);
39
40 println!(
41 "| {:<28} | {:>7} | {:>7} | {:>7} | {:<15} |",
42 author,
43 stats.loc,
44 stats.commits,
45 stats.files.len(),
46 distribution_str
47 );
48 }
49}
50
51pub fn print_user_ownership(rows: &[(String, usize, usize, f32)]) {
54 println!(
55 "| {:>4} | {:<60} | {:>7} | {:>7} | {:>6} |",
56 "No.", "File", "userLOC", "fileLOC", "%own"
57 );
58 println!(
59 "|{:->6}|:{:-<60}|{:->9}|{:->9}|{:->8}|",
60 "", "", "", "", ""
61 );
62 for (i, (file, u, f, pct)) in rows.iter().enumerate() {
63 println!(
64 "| {:>4} | {:<60} | {:>7} | {:>7} | {:>5.1} |",
65 i + 1,
66 truncate(file, 60),
67 u,
68 f,
69 pct
70 );
71 }
72}
73
74fn truncate(s: &str, max: usize) -> String {
76 if s.len() <= max {
77 s.to_string()
78 } else if max > 3 {
79 let cut = max - 3;
80 let mut end = cut;
83 if end > 0 && end < s.len() {
84 let prev = &s[end.saturating_sub(1)..end];
85 let cur = &s[end..end + 1];
86 if prev != "-" && cur == "-" {
87 end += 1;
88 }
89 }
90 let mut out = s[..end].to_string();
91 out.push_str("...");
92 out
93 } else {
94 s[..max].to_string()
95 }
96}
97
98pub fn print_progress(processed: usize, total: usize, start_time: Instant) {
100 const BAR_WIDTH: usize = 50;
101 let percentage = processed as f32 / total as f32;
102 let filled_width = (percentage * BAR_WIDTH as f32) as usize;
103 let elapsed = start_time.elapsed().as_secs_f32();
104 let files_per_second = if elapsed > 0.0 {
105 processed as f32 / elapsed
106 } else {
107 0.0
108 };
109 let bar: String = (0..BAR_WIDTH)
110 .map(|i| if i < filled_width { '#' } else { ' ' })
111 .collect();
112 print!(
113 "\rProcessing: {:3.0}%|{}| {}/{} [{:.2} file/s]",
114 percentage * 100.0,
115 bar,
116 processed,
117 total,
118 files_per_second
119 );
120 io::stdout().flush().unwrap();
121}
122
123pub fn print_user_stats(username: &str, stats: &UserStats) {
125 println!("\nStatistics for user: {}", username);
126 println!("---------------------------------");
127 println!("Merged Pull Requests: {}", stats.pull_requests);
128
129 if !stats.tags.is_empty() {
130 println!("\nAuthored in the following tags:");
131 let mut sorted_tags: Vec<_> = stats.tags.iter().collect();
132 sorted_tags.sort();
133
134 let tag_count = sorted_tags.len();
135 if tag_count <= 6 {
136 for tag in sorted_tags {
137 println!(" - {}", tag);
138 }
139 } else {
140 for tag in sorted_tags.iter().take(5) {
141 println!(" - {}", tag);
142 }
143 println!(" ... ({} more tags)", tag_count - 6);
144 if let Some(last_tag) = sorted_tags.last() {
145 println!(" - {}", last_tag);
146 }
147 }
148 } else {
149 println!("\nNo tags found where this user is an author.");
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::stats::{AuthorStats, UserStats};
157 use std::collections::HashSet;
158 use std::time::Instant;
159
160 #[test]
161 fn test_print_table() {
162 let mut data = Vec::new();
163 let mut files = HashSet::new();
164 files.insert("file1.rs".to_string());
165 data.push((
166 "test_author".to_string(),
167 AuthorStats {
168 loc: 100,
169 commits: 10,
170 files,
171 },
172 ));
173 print_table(data, 100, 10, 1);
175 }
176
177 #[test]
178 fn test_print_progress() {
179 let start_time = Instant::now();
180 print_progress(50, 100, start_time);
182 }
183
184 #[test]
185 fn test_print_user_stats() {
186 let mut tags = HashSet::new();
187 tags.insert("v1.0".to_string());
188 tags.insert("v1.1".to_string());
189 let stats = UserStats {
190 pull_requests: 5,
191 tags,
192 };
193 print_user_stats("test_user", &stats);
195 }
196
197 #[test]
198 fn test_print_user_stats_no_tags() {
199 let stats = UserStats {
200 pull_requests: 2,
201 tags: HashSet::new(),
202 };
203 print_user_stats("test_user_no_tags", &stats);
205 }
206
207 #[test]
208 fn test_print_user_ownership() {
209 let rows = vec![
210 ("src/lib.rs".to_string(), 10, 20, 50.0),
211 ("README.md".to_string(), 5, 5, 100.0),
212 ];
213 super::print_user_ownership(&rows);
215 }
216
217 #[test]
218 fn test_truncate() {
219 assert_eq!(super::truncate("short", 10), "short");
220 assert_eq!(super::truncate("exactlyten", 10), "exactlyten");
221 assert_eq!(super::truncate("this-is-long", 10), "this-is-...");
222 }
223}