Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "loki-cli"
version = "1.7.0"
version = "1.9.0"
authors = ["Kyle W. Rader"]
description = "Loki: 🚀 A Git productivity tool"
homepage = "https://github.com/kyle-rader/loki-cli"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ lk x -- commit -m "Update Readme without running hooks"
```

### `repo stats`
Analyze first-parent commits to see who has been landing work in a repository. All of the filtering flags operate on commit dates.
Analyze commits reachable from HEAD to see who has been landing work in a repository. All of the filtering flags operate on commit dates.

- `--name` filters by author display name (repeatable, case-insensitive).
- `--email` filters by author email (repeatable, case-insensitive).
- `--first-parent` limits the analysis to first-parent commits.

#### Example
```
Expand Down
31 changes: 21 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ struct RepoStatsOptions {
#[clap(long, default_value_t = 20)]
top: usize,

/// Only include first-parent commits.
#[clap(long, default_value = "false")]
first_parent: bool,

/// Only include commits authored by these names (repeatable, case-insensitive fuzzy match).
#[clap(long = "name", value_name = "NAME")]
names: Vec<String>,
Expand All @@ -85,7 +89,7 @@ struct RepoStatsOptions {

#[derive(Debug, Subcommand)]
enum RepoSubcommand {
/// Analyze first-parent commits by author over time.
/// Analyze commits by author over time.
#[clap(name = "stats")]
Stats(RepoStatsOptions),
}
Expand Down Expand Up @@ -231,11 +235,11 @@ fn repo_stats(options: &RepoStatsOptions) -> Result<(), String> {
let email_filters_lower: Vec<String> =
options.emails.iter().map(|s| s.to_lowercase()).collect();

let mut git_args: Vec<String> = vec![
"log".to_string(),
"--first-parent".to_string(),
"--pretty=format:%ct%x09%an%x09%ae".to_string(),
];
let mut git_args: Vec<String> = vec!["log".to_string()];
if options.first_parent {
git_args.push("--first-parent".to_string());
}
git_args.push("--pretty=format:%ct%x09%an%x09%ae".to_string());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-author activity stats assume chronological ordering that no longer holds

Medium Severity

The per-author timestamp tracking at lines 325-328 relies on commits arriving in reverse-chronological order (as documented in the comments at lines 318 and 322-324). With --first-parent, this was guaranteed. Without it, git log uses topological ordering where commits from merged branches may not appear in strict date order. This causes latest_commit_ts_by_author and oldest_commit_ts_by_author to potentially hold swapped or incorrect values, leading to wrong "commits per week" rates and "active span" displays. When latest_ts < oldest_ts, saturating_sub returns 0, artificially inflating the rate calculation.

Additional Locations (1)

Fix in Cursor Fix in Web

if let Some(start_ts) = range.start_ts {
git_args.push(format!("--since=@{start_ts}"));
}
Expand Down Expand Up @@ -339,10 +343,17 @@ fn repo_stats(options: &RepoStatsOptions) -> Result<(), String> {
progress.finish();

if totals.is_empty() {
println!(
"No first-parent commits found between {} and {}.",
range.start_label, range.end_label
);
if options.first_parent {
println!(
"No first-parent commits found between {} and {}.",
range.start_label, range.end_label
);
} else {
println!(
"No commits found between {} and {}.",
range.start_label, range.end_label
);
}
return Ok(());
}

Expand Down
Loading