Skip to content

Commit c283411

Browse files
authored
Merge pull request #32 from kyle-rader/cursor/repo-stats-commit-filtering-0167
2 parents 6a3ec58 + f475231 commit c283411

3 files changed

Lines changed: 82 additions & 28 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "loki-cli"
3-
version = "1.5.2"
3+
version = "1.6.0"
44
authors = ["Kyle W. Rader"]
55
description = "Loki: 🚀 A Git productivity tool"
66
homepage = "https://github.com/kyle-rader/loki-cli"

src/main.rs

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ struct RepoStatsOptions {
7070
to: Option<NaiveDate>,
7171

7272
/// Limit the output to the top N contributors.
73-
#[clap(long)]
74-
top: Option<usize>,
73+
#[clap(long, default_value_t = 20)]
74+
top: usize,
7575

7676
/// Only include commits authored by these names (repeatable, case-insensitive fuzzy match).
7777
#[clap(long = "name", value_name = "NAME")]
@@ -205,10 +205,8 @@ fn repo_stats(options: &RepoStatsOptions) -> Result<(), String> {
205205
let progress = start_delayed_progress_meter("Computing repo stats...", Duration::from_secs(1));
206206

207207
let range = resolve_time_range(options)?;
208-
if let Some(top) = options.top {
209-
if top == 0 {
210-
return Err(String::from("--top must be greater than zero."));
211-
}
208+
if options.top == 0 {
209+
return Err(String::from("--top must be greater than zero."));
212210
}
213211

214212
let mut totals: HashMap<String, usize> = HashMap::new();
@@ -345,11 +343,8 @@ fn repo_stats(options: &RepoStatsOptions) -> Result<(), String> {
345343

346344
let total_commits: usize = author_counts.iter().map(|(_, count)| *count).sum();
347345
let unique_authors = author_counts.len();
348-
let display_author_counts: Vec<(String, usize)> = if let Some(top_n) = options.top {
349-
author_counts.iter().take(top_n).cloned().collect()
350-
} else {
351-
author_counts.clone()
352-
};
346+
let display_author_counts: Vec<(String, usize)> =
347+
author_counts.iter().take(options.top).cloned().collect();
353348

354349
let resolved_end_label = if range.end_is_latest {
355350
latest_commit_date_in_range
@@ -454,6 +449,27 @@ fn active_weeks_inclusive(latest_ts: i64, oldest_ts: i64) -> f64 {
454449
(active_days as f64) / 7.0
455450
}
456451

452+
fn active_days_inclusive(latest_ts: i64, oldest_ts: i64) -> i64 {
453+
let span_seconds = latest_ts.saturating_sub(oldest_ts).max(0);
454+
(span_seconds / 86_400) + 1
455+
}
456+
457+
fn format_active_span(latest_ts: i64, oldest_ts: i64) -> String {
458+
// Use an average Gregorian year/month to avoid jumpy “calendar” math.
459+
let days = active_days_inclusive(latest_ts, oldest_ts) as f64;
460+
let years = days / 365.25;
461+
if years >= 1.0 {
462+
let rounded = (years * 10.0).round() / 10.0;
463+
let unit = if (rounded - 1.0).abs() < 1e-9 { "year" } else { "years" };
464+
format!("{rounded:.1} {unit}")
465+
} else {
466+
let months = days / (365.25 / 12.0);
467+
let rounded = (months * 10.0).round() / 10.0;
468+
let unit = if (rounded - 1.0).abs() < 1e-9 { "month" } else { "months" };
469+
format!("{rounded:.1} {unit}")
470+
}
471+
}
472+
457473
fn print_author_graph(
458474
author_counts: &[(String, usize)],
459475
latest_commit_ts_by_author: &HashMap<String, i64>,
@@ -463,6 +479,8 @@ fn print_author_graph(
463479
return;
464480
}
465481

482+
const MIN_COMMITS_FOR_RATE: usize = 3;
483+
466484
println!("Commits by author:");
467485
for (author_display, count) in author_counts {
468486
// Color the count green
@@ -482,25 +500,40 @@ fn print_author_graph(
482500
author_display.yellow().to_string()
483501
};
484502

485-
let email_key = extract_email_key(author_display);
486-
let commits_per_week_suffix = email_key
487-
.and_then(|email| {
488-
let latest_ts = latest_commit_ts_by_author.get(email)?;
489-
let oldest_ts = oldest_commit_ts_by_author.get(email)?;
490-
let weeks = active_weeks_inclusive(*latest_ts, *oldest_ts);
491-
let commits_per_week = (*count as f64) / weeks;
492-
Some(format!("({commits_per_week:.1}/wk)").purple())
493-
})
494-
.unwrap_or_else(|| "(?/wk)".purple());
495-
496-
println!("({count_str}) {colored_author} {commits_per_week_suffix}");
503+
let commits_per_week_suffix: String = if *count >= MIN_COMMITS_FOR_RATE {
504+
let email_key = extract_email_key(author_display);
505+
email_key
506+
.and_then(|email| {
507+
let latest_ts = latest_commit_ts_by_author.get(email)?;
508+
let oldest_ts = oldest_commit_ts_by_author.get(email)?;
509+
let weeks = active_weeks_inclusive(*latest_ts, *oldest_ts);
510+
if weeks <= 0.0 {
511+
return None;
512+
}
513+
let commits_per_week = (*count as f64) / weeks;
514+
let span = format_active_span(*latest_ts, *oldest_ts);
515+
Some(format!("({commits_per_week:.1}/wk over {span})").purple().to_string())
516+
})
517+
.unwrap_or_default()
518+
} else {
519+
String::new()
520+
};
521+
522+
if commits_per_week_suffix.is_empty() {
523+
println!("({count_str}) {colored_author}");
524+
} else {
525+
println!("({count_str}) {colored_author} {commits_per_week_suffix}");
526+
}
497527
}
498528
}
499529

500530
fn extract_email_key(author_display: &str) -> Option<&str> {
501-
let start = author_display.find('<')?;
502-
let end = author_display.rfind('>')?;
503-
(start < end).then(|| author_display[start + 1..end].trim())
531+
if let Some(start) = author_display.find('<') {
532+
let end = author_display.rfind('>')?;
533+
(start < end).then(|| author_display[start + 1..end].trim())
534+
} else {
535+
Some(author_display.trim())
536+
}
504537
}
505538

506539
fn resolve_time_range(options: &RepoStatsOptions) -> Result<TimeRange, String> {
@@ -1069,4 +1102,25 @@ mod tests {
10691102
let weeks = active_weeks_inclusive(latest, oldest);
10701103
assert!((weeks - (8.0 / 7.0)).abs() < 1e-9, "weeks={weeks}");
10711104
}
1105+
1106+
#[test]
1107+
fn format_active_span_uses_months_below_one_year() {
1108+
// 6 months-ish
1109+
let oldest = 1_700_000_000;
1110+
let latest = oldest + (183 * 86_400);
1111+
let span = format_active_span(latest, oldest);
1112+
assert!(
1113+
span.contains("month"),
1114+
"expected months in span, got `{span}`"
1115+
);
1116+
}
1117+
1118+
#[test]
1119+
fn format_active_span_uses_years_at_or_above_one_year() {
1120+
// ~400 days
1121+
let oldest = 1_700_000_000;
1122+
let latest = oldest + (399 * 86_400);
1123+
let span = format_active_span(latest, oldest);
1124+
assert!(span.contains("year"), "expected years in span, got `{span}`");
1125+
}
10721126
}

0 commit comments

Comments
 (0)