Skip to main content

git_stats/
app.rs

1//! Application: the thin coordinator. Follows the Logic Sandwich pattern,
2//! reading from the repository, transforming with pure logic, and reading again
3//! for numstats, then rendering. It owns no business rules of its own.
4
5use crate::error::Result;
6use crate::logic::{aggregate, filter, render, sort};
7use crate::model::Options;
8use crate::repo::{self, Repo, WalkedCommit};
9
10/// Run the report and return its rendered text: the stats table followed by the
11/// optional reviews table. Returns an empty string when there is nothing to show.
12///
13/// # Errors
14///
15/// Returns an error if the revision range cannot be resolved, a date or author
16/// pattern is invalid, or a commit's diff cannot be read.
17pub fn run(repo: &Repo, opts: &Options) -> Result<String> {
18    // READ: walk the range once to get commit metadata.
19    let walked = repo.walk(&opts.range)?;
20
21    let mut output = String::new();
22
23    output.push_str(&stats_section(repo, opts, &walked)?);
24
25    // Reviews intentionally use the full range, ignoring the author/date
26    // filters, to match the original tool's behavior.
27    if opts.reviews {
28        let reviews = aggregate::aggregate_reviews(walked.iter().map(|w| &w.meta), opts.email);
29        if !reviews.is_empty() {
30            output.push('\n');
31            output.push_str(&render::render_reviews(&reviews));
32            output.push('\n');
33        }
34    }
35
36    Ok(output)
37}
38
39fn stats_section(repo: &Repo, opts: &Options, walked: &[WalkedCommit]) -> Result<String> {
40    // PROCESS: author/date filter.
41    let authors = filter::compile_authors(&opts.authors)?;
42    let since = repo::parse_date(opts.since.as_deref())?;
43    let until = repo::parse_date(opts.until.as_deref())?;
44    let kept_idx = filter::keep_indices(walked.iter().map(|w| &w.meta), &authors, since, until);
45    let kept: Vec<&WalkedCommit> = kept_idx.iter().map(|&i| &walked[i]).collect();
46
47    // READ: numstat only the survivors.
48    let diffs = repo.numstats(&kept)?;
49
50    // PROCESS: aggregate, total, sort.
51    let rows: Vec<aggregate::CommitStat> = kept
52        .iter()
53        .zip(diffs)
54        .map(|(w, diff)| aggregate::CommitStat {
55            author_key: aggregate::author_key(&w.meta.author, opts.email),
56            diff,
57        })
58        .collect();
59    let mut stats = aggregate::aggregate(&rows);
60    if stats.is_empty() {
61        return Ok(String::new());
62    }
63    sort::sort_stats(&mut stats, opts.sort, opts.reverse);
64    stats.push(aggregate::compute_totals(&stats));
65
66    let mut section = render::render_stats(&stats);
67    section.push('\n');
68    Ok(section)
69}