# 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 -M` parse. Rename tracking (including `src/{old => new}/f.py` forms) maps a file's whole history onto its current name. Mailmap support (repo `.mailmap` plus `--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

```bash
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

```bash
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](docs/algorithms.md); every metric and its caveats are in [docs/metrics.md](docs/metrics.md).

## Tests

```bash
.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; `--blame` fixes that at the cost of one `git blame` per 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 -M` detects (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.
