git_insights/
output.rs

1use crate::stats::{AuthorStats, UserStats};
2use std::io::{self, Write};
3use std::time::Instant;
4
5/// Prints a formatted table of author statistics.
6pub 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 
51/// Prints a file ownership table for a user.
52/// Rows: (file, user_loc, file_loc, pct)
53pub 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
74// helper to truncate long file paths for table display
75fn 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        // Prefer to keep a trailing '-' if we cut right before it
81        // e.g., "this-is-long", max=10 -> "this-is-..."
82        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 
98/// Renders a progress bar to the console.
99pub 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
123/// Prints a formatted table of user statistics.
124pub 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        // Should not panic
174        print_table(data, 100, 10, 1);
175    }
176
177    #[test]
178    fn test_print_progress() {
179        let start_time = Instant::now();
180        // Should not panic
181        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        // Should not panic
194        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        // Should not panic
204        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        // Should not panic
214        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}