@@ -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+
457473fn 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
500530fn 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
506539fn 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