gitpulse
Engineering leads ask the same three questions: where is the risky code, who knows it, and what is secretly coupled? gitpulse answers them from nothing but git log --numstat. It finds hotspots (churn times complexity), knowledge silos (ownership, bus factor, what you lose when someone leaves) and hidden coupling (files that always change together), then renders everything as a single self-contained HTML report. No GitPython, no network, two dependencies (click, rich).
Features
- History extraction: one streaming
git log --no-merges --numstat -Mparse. Rename tracking (includingsrc/{old => new}/f.pyforms) maps a file's whole history onto its current name. Mailmap support (repo.mailmapplus--mailmap FILE),--since/--until, include/exclude globs, and bulk-commit filtering (--max-files). - SQLite cache keyed by HEAD sha with incremental updates (only new commits are parsed; a rewritten history triggers a full rescan).
- Complexity: language-agnostic indentation complexity, real cyclomatic complexity for Python via
ast, own LOC counter. - Hotspots with score = relative churn x relative complexity, plus complexity trend of any file over time.
- Temporal coupling: support, confidence (both directions), coupling degree, minimum thresholds, cross-module filtering and a module-to-module summary.
- Knowledge: ownership by lines added or by surviving lines (
--blame), main developer, knowledge loss for departed authors, greedy bus factor per directory. - Team metrics: weekday x hour heatmap, age of code when modified, first-time contributors, commit size distribution, fix-commit ratio.
- Output as rich tables, JSON or CSV on every command, and an HTML report with an own squarified treemap, coupling matrix and heatmap (inline SVG, no scripts, light and dark theme).
Install
cd gitpulse
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate
Requires Python 3.12+ and git.
Quickstart
python examples/make_demo_repo.py /tmp/demo # deterministic: 600 commits, 8 authors
gitpulse scan -r /tmp/demo
gitpulse hotspots -r /tmp/demo
gitpulse report -r /tmp/demo -o report.html --departed dana@shop.example --module-depth 3 --bus-depth 3
A pre-built report for the demo repo is checked in at examples/demo-report.html.
Usage
Every command takes the common options below, so -r is the repository (default .).
| Option | Meaning |
|---|---|
-r, --repo PATH |
repository to analyse |
--since / --until DATE |
ISO dates; window for all metrics (first-seen dates still use the full history) |
-i, --include GLOB / -x, --exclude GLOB |
repeatable path globs; * also matches / and a pattern without a slash matches the file name |
--max-files N |
ignore commits touching more than N files (default 50, 0 disables) |
--mailmap FILE |
extra mailmap, on top of the repo's .mailmap |
--cache FILE / --no-cache |
cache location (default ~/.cache/gitpulse/, or $GITPULSE_CACHE_DIR) |
-f, --format table\|json\|csv |
output format |
The cache lives outside the repository, so analysing a repo never writes into it. Multi-section commands (team, busfactor --departed, owners DIR, coupling --modules) emit one JSON object keyed by section, or CSV blocks separated by # section lines.
scan
$ gitpulse scan -r /tmp/demo
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ metric ┃ value ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ repository │ /tmp/demo │
│ head │ eeedc49f1ba9 │
│ cache │ full scan │
│ commits │ 598 │
│ authors │ 8 │
│ first_commit │ 2024-01-08 │
│ last_commit │ 2025-10-28 │
│ fix_ratio │ 0.2 │
│ lines_added │ 11822 │
│ lines_deleted │ 4909 │
│ files_tracked │ 87 │
└───────────────┴────────────────────────────────────────────────────────────────────────────────────────────────┘
hotspots
--metric indent (default) or cyclomatic (Python files only), --top N.
$ gitpulse hotspots -r /tmp/demo --top 6
Hotspots (indent complexity)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┓
┃ path ┃ score ┃ revisions ┃ loc ┃ complexity ┃ cyclomatic ┃ authors ┃ fixes ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━┩
│ src/shop/orders.py │ 100 │ 147 │ 2475 │ 7137 │ 1334 │ 5 │ 50 │
│ tests/test_orders.py │ 43.3 │ 101 │ 1601 │ 4500 │ 857 │ 7 │ 33 │
│ src/shop/billing/charge.py │ 7.8 │ 98 │ 483 │ 839 │ 208 │ 4 │ 21 │
│ src/shop/invoicing/render.py │ 4.7 │ 91 │ 332 │ 538 │ 149 │ 4 │ 17 │
│ src/shop/db/queries.py │ 4.2 │ 84 │ 308 │ 518 │ 127 │ 5 │ 17 │
│ src/shop/api/routes.py │ 2.4 │ 70 │ 228 │ 359 │ 105 │ 4 │ 12 │
└──────────────────────────────┴───────┴───────────┴──────┴────────────┴────────────┴─────────┴───────┘
orders.py is the god file: most revisions and the deepest nesting. The score is relative, so 100 means "top of this repo", not an absolute threshold.
trend
Complexity at evenly spaced commits that touched a file. History survives renames: util.py became helpers/text.py mid-history.
$ gitpulse trend src/shop/util.py -r /tmp/demo --samples 5
Complexity trend: src/shop/helpers/text.py
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ date ┃ sha ┃ loc ┃ indent_complexity ┃ cyclomatic ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 2024-01-08 │ 96a5e683 │ 14 │ 22 │ 7 │
│ 2024-07-02 │ 8eb03ae4 │ 29 │ 50 │ 16 │
│ 2024-10-29 │ 046681a7 │ 58 │ 92 │ 32 │
│ 2025-04-01 │ 2958eb28 │ 109 │ 170 │ 53 │
│ 2025-10-16 │ 3317c0e3 │ 183 │ 306 │ 84 │
└────────────┴──────────┴─────┴───────────────────┴────────────┘
coupling
--min-shared, --min-degree, --module-depth, --cross-module, --modules.
$ gitpulse coupling -r /tmp/demo --module-depth 3 --modules --top 5 --min-degree 20
Temporal coupling
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ file_a ┃ file_b ┃ degree_pct ┃ shared ┃ revs_a ┃ revs_b ┃ a_to_b_pct ┃ b_to_a_pct ┃ cross_module ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ src/shop/billing/charge.py │ src/shop/invoicing/render. │ 91 │ 86 │ 98 │ 91 │ 87.8 │ 94.5 │ True │
│ │ py │ │ │ │ │ │ │ │
│ src/shop/api/routes.py │ src/shop/db/queries.py │ 71.4 │ 55 │ 70 │ 84 │ 78.6 │ 65.5 │ True │
│ src/shop/orders.py │ tests/test_orders.py │ 62.1 │ 77 │ 147 │ 101 │ 52.4 │ 76.2 │ True │
└────────────────────────────┴────────────────────────────┴────────────┴────────┴────────┴────────┴────────────┴────────────┴──────────────┘
Module coupling
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ module_a ┃ module_b ┃ shared_revisions ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ src/shop/billing │ src/shop/invoicing │ 86 │
│ src/shop │ tests │ 77 │
│ src/shop/api │ src/shop/db │ 55 │
└──────────────────┴────────────────────┴──────────────────┘
billing/charge.py and invoicing/render.py change together in 91% of their revisions even though they live in different modules, an architectural leak worth a conversation. routes.py reaching into db/queries.py is the classic layering smell.
owners, busfactor
$ gitpulse owners src/shop/orders.py -r /tmp/demo
Ownership of src/shop/orders.py
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ author ┃ email ┃ lines ┃ ownership_pct ┃ commits ┃ last_change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━┩
│ Alice Nguyen │ alice@shop.example │ 2812 │ 67.4 │ 99 │ 2025-10-24 │
│ Bob Martin │ bob@shop.example │ 741 │ 17.8 │ 27 │ 2025-10-20 │
│ Carol Diaz │ carol@shop.example │ 403 │ 9.7 │ 14 │ 2025-09-29 │
│ Farid Haddad │ farid@shop.example │ 129 │ 3.1 │ 3 │ 2025-07-31 │
│ Gustavo Lima │ gus@shop.example │ 85 │ 2 │ 4 │ 2025-10-14 │
└──────────────┴────────────────────┴───────┴───────────────┴─────────┴─────────────┘
$ gitpulse busfactor -r /tmp/demo --depth 3 --departed dana@shop.example
Bus factor
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ directory ┃ files ┃ bus_factor ┃ authors ┃ top_owners ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ tests │ 3 │ 1 │ 7 │ Alice Nguyen 54% │
│ src/shop/billing │ 2 │ 1 │ 4 │ Bob Martin 69% │
│ src/shop/inventory │ 2 │ 1 │ 4 │ Dana Whitfield 62% │
│ docs │ 1 │ 1 │ 5 │ Erin Walsh 69% │
│ src/shop/invoicing │ 1 │ 1 │ 4 │ Bob Martin 71% │
│ . │ 17 │ 2 │ 8 │ Alice Nguyen 33%, Bob Martin 20% │
│ src │ 11 │ 2 │ 8 │ Alice Nguyen 27%, Bob Martin 25% │
│ src/shop │ 11 │ 2 │ 8 │ Alice Nguyen 27%, Bob Martin 25% │
│ src/shop/api │ 2 │ 2 │ 4 │ Carol Diaz 42%, Alice Nguyen 35% │
│ src/shop/db │ 2 │ 2 │ 5 │ Carol Diaz 43%, Alice Nguyen 35% │
│ src/shop/helpers │ 1 │ 2 │ 4 │ Bob Martin 48%, Gustavo Lima 25% │
└────────────────────┴───────┴────────────┴─────────┴──────────────────────────────────┘
Files at risk if dana@shop.example leave(s)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ path ┃ departed_share_pct ┃ main_developer ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ src/shop/inventory/search.py │ 71.9 │ Dana Whitfield │
│ src/shop/inventory/products.py │ 52.7 │ Dana Whitfield │
└────────────────────────────────┴────────────────────┴────────────────┘
The departed author owns most of inventory/, which is exactly where the bus factor is 1.
churn, author, team
$ gitpulse churn -r /tmp/demo --by year
Churn by year
┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━┓
┃ period ┃ commits ┃ authors ┃ files ┃ added ┃ deleted ┃
┡━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━┩
│ 2024 │ 336 │ 7 │ 17 │ 6726 │ 2725 │
│ 2025 │ 262 │ 8 │ 16 │ 5096 │ 2184 │
└────────┴─────────┴─────────┴───────┴───────┴─────────┘
$ gitpulse author hana -r /tmp/demo
Author: Hana Sato
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ metric ┃ value ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ author │ Hana Sato <hana@shop.example> │
│ commits │ 12 │
│ share_of_commits_pct │ 2 │
│ first_commit │ 2025-02-17 │
│ last_commit │ 2025-10-08 │
│ active_months │ 8 │
│ lines_added │ 121 │
│ lines_deleted │ 77 │
│ fix_commit_ratio_pct │ 16.7 │
│ files_mainly_owned │ 0 │
└──────────────────────┴───────────────────────────────┘
Most touched files
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ path ┃ commits ┃ ownership_pct ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ README.md │ 3 │ 16 │
│ src/shop/api/routes.py │ 3 │ 3.6 │
│ src/shop/db/queries.py │ 3 │ 4.5 │
│ src/shop/api/handlers.py │ 2 │ 13.2 │
│ tests/test_billing.py │ 2 │ 5 │
│ tests/test_orders.py │ 1 │ 1.1 │
│ docs/architecture.md │ 1 │ 3.8 │
└──────────────────────────┴─────────┴───────────────┘
Where they work
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ directory ┃ file_changes ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ src/shop │ 8 │
│ tests │ 3 │
│ . │ 3 │
│ docs │ 1 │
└───────────┴──────────────┘
gitpulse team prints the cadence heatmap, commit size buckets, code age, newcomers and fix ratio.
report
gitpulse report -o report.html writes one HTML file with a hotspot treemap (area = LOC, colour = score), complexity trends, coupling table and module matrix, bus factor, knowledge loss, heatmap, monthly commits and newcomers. Extra options: --departed EMAIL (repeatable), --blame, --metric, --module-depth, --bus-depth.
On a real repository
Run read-only against a sibling showcase repo (a single squashed commit, so the interesting columns are flat, but the pipeline is the same):
$ gitpulse busfactor -r ../benchdiff --depth 1 # condensed
. 31 files bus factor 1 Akshay Prabhu 100%
src 18 files bus factor 1 Akshay Prabhu 100%
tests 5 files bus factor 1 Akshay Prabhu 100%
Repositories whose initial import touches more than 50 files report "no commits match"; pass --max-files 0 to include it.
Configuration
There is no config file; everything is a flag. Environment: GITPULSE_CACHE_DIR moves the cache directory.
Architecture
| Module | Role |
|---|---|
gitlog.py |
stream git log, parse numstat and {a => b} rename syntax |
store.py |
SQLite cache, incremental sync by HEAD |
history.py |
rename identity resolution, mailmap, filters, windowed views |
mailmap.py |
.mailmap parser |
complexity.py |
LOC, indentation complexity, Python cyclomatic |
hotspots.py, coupling.py, knowledge.py, team.py |
the analyses (pure functions over commits) |
treemap.py |
squarified treemap and nested layout |
report.py |
HTML/SVG renderer |
output.py, cli.py, context.py |
tables/JSON/CSV, commands, shared option handling |
Design notes and worked examples are in docs/algorithms.md; every metric and its caveats are in docs/metrics.md.
Tests
.venv/bin/pytest -q
The suite covers numstat and rename parsing, mailmap, coupling math, bus factor, treemap invariants, complexity, the incremental cache and the CLI end to end against the synthetic repo.
Limitations
- Temporal coupling and ownership are only as good as commit hygiene: squashed or huge commits blur both.
- Ownership by lines added overstates people who wrote a lot of code that was later rewritten;
--blamefixes that at the cost of onegit blameper file. - Indentation complexity is a proxy; it does not understand languages (Python's cyclomatic number is the only real parse). Data formats such as JSON are scored by indentation too.
- Renames are what
git -Mdetects (about 50% similarity); a rewrite-and-move looks like a delete plus an add. - Files deleted before HEAD are excluded from hotspots, coupling and ownership.
- Author identity is the (mailmap-resolved) email; unmapped aliases count as different people.