CLI · commit 7af1891

gitpulse

Repository health & socio-technical analytics

Download gitpulse.zip
Quickstart
unzip gitpulse.zip && cd gitpulse
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

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

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