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 regex::Regex;
6
7use crate::error::Result;
8use crate::logic::{aggregate, filter, render, sort};
9use crate::model::Options;
10use crate::repo::{self, Repo, WalkedCommit};
11
12/// Run the report and return its rendered text: the stats table followed by the
13/// optional reviews table. Returns an empty string when there is nothing to show.
14///
15/// # Errors
16///
17/// Returns an error if the revision range cannot be resolved, a date or author
18/// pattern is invalid, or a commit's diff cannot be read.
19pub fn run(repo: &Repo, opts: &Options) -> Result<String> {
20    // PROCESS: validate the filters first. They are cheap to compile, and a
21    // typo'd pattern or date should error immediately, not after a walk that
22    // can take minutes on a large history.
23    let authors = filter::compile_authors(&opts.authors)?;
24    let since = repo::parse_date(opts.since.as_deref())?;
25    let until = repo::parse_date(opts.until.as_deref())?;
26
27    // READ: walk the range once to get commit metadata.
28    let walked = repo.walk(&opts.range, opts.reviews)?;
29
30    let mut output = String::new();
31
32    output.push_str(&stats_section(repo, opts, &authors, since, until, &walked)?);
33
34    // Reviews intentionally use the full range, ignoring the author/date
35    // filters, to match the original tool's behavior.
36    if opts.reviews {
37        let reviews = aggregate::aggregate_reviews(walked.iter().map(|w| &w.meta), opts.email);
38        if !reviews.is_empty() {
39            // The blank line only separates the tables; without a stats table
40            // the reviews must start at the top.
41            if !output.is_empty() {
42                output.push('\n');
43            }
44            output.push_str(&render::render_reviews(&reviews));
45            output.push('\n');
46        }
47    }
48
49    Ok(output)
50}
51
52fn stats_section(
53    repo: &Repo,
54    opts: &Options,
55    authors: &[Regex],
56    since: Option<i64>,
57    until: Option<i64>,
58    walked: &[WalkedCommit],
59) -> Result<String> {
60    // PROCESS: author/date filter.
61    let kept_idx = filter::keep_indices(walked.iter().map(|w| &w.meta), authors, since, until);
62    let kept: Vec<&WalkedCommit> = kept_idx.iter().map(|&i| &walked[i]).collect();
63
64    // READ: numstat only the survivors.
65    let diffs = repo.numstats(&kept)?;
66
67    // PROCESS: aggregate, total, sort.
68    let rows: Vec<aggregate::CommitStat> = kept
69        .iter()
70        .zip(diffs)
71        .map(|(w, diff)| aggregate::CommitStat {
72            author_key: aggregate::author_key(&w.meta.author, opts.email),
73            diff,
74        })
75        .collect();
76    let mut stats = aggregate::aggregate(&rows);
77    if stats.is_empty() {
78        return Ok(String::new());
79    }
80    sort::sort_stats(&mut stats, opts.sort, opts.reverse);
81    stats.push(aggregate::compute_totals(&stats));
82
83    let mut section = render::render_stats(&stats);
84    section.push('\n');
85    Ok(section)
86}