Give credit where it's due -- precise per-author contribution stats that see through squash merges.
A contribution analysis tool that accurately attributes lines of code to individual authors, even across squash-merged pull requests, with support for file exclusion filters.
- Walk. git-credit walks the history (
HEAD, or--rev A..B) and diffs each commit against its parent, with rename detection, counting only the files not matched by--exclude. Merge commits are skipped: their lines are already credited through the commits they merge. - Detect squash merges. A commit whose first line contains
(#N), as GitHub writes it when squash-merging PR#N, is a squash-merge candidate. - Expand through GitHub. For each candidate, git-credit lists the PR's commits (merge commits skipped) and splits the squash commit's lines among their authors, in proportion to the non-excluded lines each author changed in the PR. Emails are compared case-insensitively, so one author committing under two spellings is credited once. Shares are rounded down.
- Fall back. If the PR can't be fetched, the squash commit is credited to
its own author and flagged
accurate: falseso a consumer can retry later.
brew tap mscheltienne/tap
brew install mscheltienne/tap/git-creditcargo install git-creditPre-built binaries for Linux, macOS, and Windows are available on the GitHub Releases page.
git clone https://github.com/mscheltienne/git-credit.git
cd git-credit
cargo install --path .# Analyze the current repository
git-credit
# Analyze a specific repository
git-credit --repo /path/to/repo
# Limit to a commit range (A..B only)
git-credit --rev HEAD~50..HEAD
# Only include commits authored on or after a date (midnight UTC)
git-credit --since 2025-01-01
# Exclude files from stats (repeatable); `*` stays within one directory,
# `**/` spans directories
git-credit --exclude "**/*.lock" --exclude "docs/**"
# Output as JSON instead of a table
git-credit --format json
# Include bot accounts (dependabot, pre-commit-ci, etc. are excluded by default)
git-credit --bots
# Use an external .mailmap (overrides any .mailmap inside the repository)
git-credit --mailmap-file /path/to/.mailmap
# Skip mailmap resolution entirely and report identities as recorded
git-credit --no-mailmap
# Skip GitHub API lookups (faster, but each squash merge is then credited to its
# own author)
git-credit --no-githubBy default git-credit applies the repository's mailmap, which libgit2 builds
from the worktree .mailmap, the mailmap.blob config and the mailmap.file
config. Pass --mailmap-file <PATH> to use an external file instead — useful
when invoking git-credit against a clone you don't want to mutate, or when
aggregating mailmap entries across many repositories outside git-credit.
The external mailmap replaces the repository's; the two are not merged.
Pass --no-mailmap to skip mailmap resolution entirely, for both the git
commit authors and the PR commit authors fetched from GitHub. The JSON output
then carries the identities as recorded, which suits a consumer that applies
the mailmap at read time. --no-mailmap and --mailmap-file are mutually
exclusive.
Bot accounts (identified by [bot]@ in their email address) are excluded from
the output by default. Use --bots to include them. When bots are excluded,
the summary line shows how many were filtered.
To attribute squash-merged PRs to individual authors, git-credit needs a GitHub token. It resolves the token in this order:
--tokenflagGITHUB_TOKENenvironment variableGH_TOKENenvironment variablegh auth token(the GitHub CLI)
If no token is found, or the origin remote is not on GitHub, git-credit runs
in --no-github mode automatically with a warning.
When GitHub rate-limits the run (a 429, or a 403 with an exhausted quota),
git-credit stops calling the API and every remaining squash merge falls back
to accurate: false.
╭──────────────────────────┬───────────────┬─────┬───────┬─────┬───────╮
│ Author ┆ Contributions ┆ PRs ┆ + ┆ - ┆ Total │
╞══════════════════════════╪═══════════════╪═════╪═══════╪═════╪═══════╡
│ Alice <alice@example.com>┆ 12 ┆ 8 ┆ 1,542 ┆ 389 ┆ 1,931 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ Bob <bob@example.com> ┆ 8 ┆ 5 ┆ 876 ┆ 201 ┆ 1,077 │
╰──────────────────────────┴───────────────┴─────┴───────┴─────┴───────╯
2 authors (3 bots excluded), 20 commits walked, 13 squash merges expanded
--format json emits one record per walked commit (merge commits are not
walked). For a squash merge expanded through GitHub (is_squash_pr: true),
attributions carries one entry per PR author, each with is_pr_author: true;
otherwise the array has a single entry. commits[] is sorted by author_date
(UTC, YYYY-MM-DDTHH:MM:SSZ) ascending, with sha as a tie-breaker.
Each commit also carries an accurate flag. It is false when a squash merge
could not be expanded because the GitHub API rate-limited the run, returned an
error, or listed no commits for the PR. The commit is then credited to the
squash commit's own author, and consumers can retry it once the API recovers.
Every other commit is accurate: true, including squash merges reported as
regular commits because GitHub lookups were off (--no-github, no token, or
no GitHub remote).
The summary counts the commits reported before bot filtering, the squash
merges expanded, and the distinct bot emails removed.
{
"commits": [
{
"sha": "abc1234567890abcdef1234567890abcdef12345",
"author_date": "2026-04-17T14:23:51Z",
"is_squash_pr": false,
"attributions": [
{
"name": "Alice Smith",
"email": "alice@example.com",
"additions": 42,
"deletions": 7,
"is_pr_author": false
}
],
"accurate": true
},
{
"sha": "def987...",
"author_date": "2026-04-18T09:12:00Z",
"is_squash_pr": true,
"attributions": [
{
"name": "Alice Smith",
"email": "alice@example.com",
"additions": 75,
"deletions": 12,
"is_pr_author": true
},
{
"name": "Bob Jones",
"email": "bob@example.com",
"additions": 25,
"deletions": 4,
"is_pr_author": true
}
],
"accurate": true
},
{
"sha": "fed321...",
"author_date": "2026-04-19T16:05:22Z",
"is_squash_pr": false,
"attributions": [
{
"name": "Alice Smith",
"email": "alice@example.com",
"additions": 50,
"deletions": 10,
"is_pr_author": false
}
],
"accurate": false
}
],
"summary": {
"total_commits_walked": 3,
"squash_merges_expanded": 1,
"bots_excluded": 0
}
}In the example above, the third commit was a squash merge whose PR expansion
failed; git-credit emits it with is_squash_pr: false, the squash commit's
author as the sole attribution, and accurate: false so the caller knows to
retry.
- GitHub lists at most 250 commits per PR, so later commits of a larger PR don't weigh on the split.
- Only the first page of files (up to 300) of each PR commit is read, so the weight of a larger commit is undercounted.
--revaccepts only a range (A..B), not a single revision orA...B.
- Rust (MSRV: 1.88)
- pre-commit
- cargo-deny (optional, for dependency auditing)
- cargo-llvm-cov (optional, for coverage)
git clone https://github.com/mscheltienne/git-credit.git
cd git-credit
pre-commit install
cargo buildcargo build # Build
cargo test # Run all tests
cargo clippy --all-targets -- -D warnings # Lint, as CI does
cargo fmt # Format
pre-commit run --all-files # typos, yamllint, fmt, clippy
cargo deny check # Audit dependencies
cargo llvm-cov # Coverage reportdoc/ is an Obsidian vault covering
the attribution pipeline, the CLI and JSON contract, and development in more
depth. Open the folder as a vault in Obsidian, or read the Markdown directly.