Operational guide for extracting git metrics and generating stats at any scale.
# Build
make build
# Extract + stats in one go
./gitcortex extract --repo /path/to/repo
./gitcortex stats
# Run tests
make check./gitcortex extract --repo /path/to/repoProduces git_data.jsonl and git_state in the current directory.
The default branch is auto-detected (origin/HEAD > main > master > HEAD). Override with:
./gitcortex extract --repo /path/to/repo --branch developUnify developer identities using the repo's .mailmap file:
./gitcortex extract --repo /path/to/repo --mailmapWithout --mailmap, the same person with different emails appears as separate contributors, splitting their stats. With it, git normalizes names and emails before extraction.
Requires a .mailmap file in the repo root or ~/.mailmap. See man gitmailmap for format details.
Filter out generated files, lock files, or vendored code:
./gitcortex extract --repo /path/to/repo --ignore package-lock.json --ignore yarn.lock
./gitcortex extract --repo /path/to/repo --ignore "*.min.js" --ignore "*.generated.go"
./gitcortex extract --repo /path/to/repo --ignore "dist/*" --ignore "vendor/*"Matches against filename (package-lock.json), full path, and directory prefix (dist/*, vendor/). Uses glob patterns (not regex). Commit additions/deletions are recalculated without the ignored files.
Messages are excluded by default to save space. Enable with:
./gitcortex extract --repo /path/to/repo --include-commit-messages./gitcortex extract --repo /path/to/repo --output /data/project.jsonl --state-file /data/project.stateFor repos with 100K+ commits, increase the checkpoint interval to reduce state file writes:
./gitcortex extract --repo /path/to/linux --batch-size 50000Expected performance:
| Repo size | Time | Memory |
|---|---|---|
| 1K commits | <1s | ~15 MB |
| 10K commits | ~5s | ~20 MB |
| 100K commits | ~1 min | ~20 MB |
| 1M+ commits | ~12 min | ~20 MB |
If extraction is interrupted (Ctrl+C, disk full, crash), just re-run the same command. It resumes from the last checkpoint:
# Interrupted at 500K commits
./gitcortex extract --repo /path/to/repo --output data.jsonl
# Resume — appends to data.jsonl from where it stopped
./gitcortex extract --repo /path/to/repo --output data.jsonlThe state file tracks progress. To start fresh, delete both files:
rm git_data.jsonl git_state# Resume after a known commit SHA
./gitcortex extract --repo /path/to/repo --start-sha abc123...
# Resume from a numeric offset (legacy)
./gitcortex extract --repo /path/to/repo --start-offset 50000Works with bare repos (no working tree):
git clone --bare https://github.com/org/repo.git /tmp/repo.git
./gitcortex extract --repo /tmp/repo.git --branch master./gitcortex stats --input data.jsonlSection headers go to stderr, data to stdout. To capture only data:
./gitcortex stats --input data.jsonl 2>/dev/null./gitcortex stats --input data.jsonl --stat summary
./gitcortex stats --input data.jsonl --stat contributors --top 20
./gitcortex stats --input data.jsonl --stat hotspots --top 30
./gitcortex stats --input data.jsonl --stat activity --granularity week
./gitcortex stats --input data.jsonl --stat busfactor --top 20
./gitcortex stats --input data.jsonl --stat coupling --top 20
./gitcortex stats --input data.jsonl --stat churn-risk --top 20
./gitcortex stats --input data.jsonl --stat working-patterns
./gitcortex stats --input data.jsonl --stat dev-network --top 20
./gitcortex stats --input data.jsonl --stat profile
./gitcortex stats --input data.jsonl --stat profile --email alice@company.com
./gitcortex stats --input data.jsonl --stat top-commits --top 20
./gitcortex stats --input data.jsonl --stat structure --tree-depth 3
./gitcortex stats --input data.jsonl --stat extensions --top 15./gitcortex stats --since 7d # last 7 days
./gitcortex stats --since 4w --stat contributors # last 4 weeks
./gitcortex stats --since 3m --stat hotspots # last 3 months
./gitcortex stats --since 1y --stat activity # last year
./gitcortex report --since 30d --output monthly.html # monthly reportSupported units: d (days), w (weeks), m (months), y (years).
For a closed window (arbitrary start/end dates, e.g. a past quarter), use --from and --to instead of --since on both stats and report:
./gitcortex stats --from 2026-01-01 --to 2026-03-31 --stat contributors # Q1 contributors
./gitcortex report --from 2026-01-01 --to 2026-03-31 --output q1.html # Q1 HTML report
./gitcortex stats --from 2025-06-01 # from date X onward
./gitcortex report --to 2024-12-31 --output pre-2025.html # up to date YDates are YYYY-MM-DD and both boundaries are inclusive — --from 2026-01-01 --to 2026-03-31 captures every commit on 2026-01-01 through 2026-03-31 at UTC day boundaries. --since and --from/--to are mutually exclusive (they both specify a window, combining them is ambiguous).
# Human-readable table (default)
./gitcortex stats --input data.jsonl --format table
# CSV — pipe to file or other tools
./gitcortex stats --input data.jsonl --stat contributors --format csv > contributors.csv
# JSON — single unified object
./gitcortex stats --input data.jsonl --format json > report.jsonAggregate stats across repositories:
./gitcortex stats --input svc-a.jsonl --input svc-b.jsonl --input svc-c.jsonlFile paths are prefixed automatically (svc-a:src/main.go). Contributors are deduped by email across repos.
Coupling analysis:
# Skip commits with >30 files (stricter noise filter)
./gitcortex stats --stat coupling --coupling-max-files 30
# Require at least 10 co-changes (fewer, higher-confidence results)
./gitcortex stats --stat coupling --coupling-min-changes 10Churn risk:
# Faster decay — 30-day half-life (focus on very recent changes)
./gitcortex stats --stat churn-risk --churn-half-life 30
# Slower decay — 180-day half-life (6-month window)
./gitcortex stats --stat churn-risk --churn-half-life 180Developer network:
# Only show strong connections (10+ shared files)
./gitcortex stats --stat dev-network --network-min-files 10./gitcortex diff --input data.jsonl \
--from 2024-01-01 --to 2024-06-30 \
--vs-from 2024-07-01 --vs-to 2024-12-31Shows side-by-side delta for summary, contributors, and hotspots.
# All stats for March 2024 only
./gitcortex diff --input data.jsonl --from 2024-03-01 --to 2024-03-31./gitcortex diff --input data.jsonl \
--from 2024-01-01 --to 2024-03-31 \
--vs-from 2024-04-01 --vs-to 2024-06-30 \
--format json > q1_vs_q2.json./gitcortex report --input data.jsonl --output report.html
./gitcortex report --input data.jsonl --output report.html --top 30Self-contained HTML file with all stats visualized. Open directly in a browser, share via Slack/email, or upload as a CI artifact.
./gitcortex report --input data.jsonl --email alice@company.com --output alice.htmlShareable HTML page for one developer. Includes summary cards, scope, contribution type, collaboration, top files, activity heatmap, and working hours heatmap.
- name: Extract git metrics
run: gitcortex extract --repo . --output data.jsonl
- name: Check quality gates
run: gitcortex ci --input data.jsonl --fail-on-busfactor 1 --fail-on-churn-risk 500 --format github-actionscode_quality:
script:
- gitcortex extract --repo . --output data.jsonl
- gitcortex ci --input data.jsonl --fail-on-busfactor 1 --format gitlab > gl-code-quality-report.json
artifacts:
reports:
codequality: gl-code-quality-report.json# Only flag bus factor (ignore churn)
./gitcortex ci --input data.jsonl --fail-on-busfactor 1
# Only flag churn risk
./gitcortex ci --input data.jsonl --fail-on-churn-risk 500
# Both rules, stricter churn decay
./gitcortex ci --input data.jsonl --fail-on-busfactor 1 --fail-on-churn-risk 300 --churn-half-life 60#!/bin/bash
REPO=/path/to/repo
DATA=/data/team.jsonl
./gitcortex extract --repo "$REPO" --output "$DATA" --state-file /data/team.state
./gitcortex stats --input "$DATA" --stat summary
./gitcortex stats --input "$DATA" --stat contributors --top 10
./gitcortex stats --input "$DATA" --stat hotspots --top 10./gitcortex stats --input data.jsonl --stat coupling --top 30 --coupling-min-changes 5Look for:
- Controller + Test at 90%+: healthy, tests co-evolve with code
- Interface + Implementation at 100%: expected for clean architecture
- Unrelated modules at 50%+: hidden dependency, refactoring opportunity
- Config + Multiple modules: shared config file is a bottleneck
./gitcortex stats --input data.jsonl --stat busfactor --top 20Files with bus factor 1 and high churn are the biggest risk. Cross-reference with:
./gitcortex stats --input data.jsonl --stat churn-risk --top 20Files appearing in both lists need knowledge transfer.
./gitcortex diff --input data.jsonl \
--from 2024-01-01 --to 2024-03-31 \
--vs-from 2024-04-01 --vs-to 2024-06-30 \
--top 15Check:
- Did commit velocity change?
- Did new hotspots emerge?
- Did contributor distribution shift?
Extract each repo separately, then analyze:
for repo in /repos/*/; do
name=$(basename "$repo")
./gitcortex extract --repo "$repo" --output "/data/${name}.jsonl" --state-file "/data/${name}.state"
echo "=== $name ===" >> /data/summary.txt
./gitcortex stats --input "/data/${name}.jsonl" --stat summary >> /data/summary.txt 2>/dev/null
done./gitcortex stats --input data.jsonl --stat contributors --format csv > contributors.csv
./gitcortex stats --input data.jsonl --stat hotspots --format csv > hotspots.csv
./gitcortex stats --input data.jsonl --stat coupling --format csv > coupling.csvThe branch doesn't exist. Check available branches:
git -C /path/to/repo branch -aOverride with --branch:
./gitcortex extract --repo /path/to/repo --branch masterIf extraction appears stuck, check if git log is processing a very large commit (e.g., a vendor directory import). The --debug flag shows per-commit progress:
./gitcortex extract --repo /path/to/repo --debugResume after freeing space. The state file tracks progress, and the output file is appended to:
# After freeing space, just re-run
./gitcortex extract --repo /path/to/repo --output data.jsonlCoupling excludes:
- Single-file commits (no coupling information)
- Commits with >50 files (bulk operations, configurable via
--coupling-max-files) - Merge commits (no file records in extraction)
This is by design — these filters remove noise from the coupling analysis.
Delete it and re-extract:
rm git_state
./gitcortex extract --repo /path/to/repoEach line is a JSON object with a type field:
{
"type": "commit",
"sha": "40-char hex",
"tree": "40-char hex",
"parents": ["sha1", "sha2"],
"author_name": "string",
"author_email": "string",
"author_date": "RFC3339",
"committer_name": "string",
"committer_email": "string",
"committer_date": "RFC3339",
"message": "string (empty unless --include-commit-messages)",
"additions": 0,
"deletions": 0,
"files_changed": 0
}{
"type": "commit_parent",
"sha": "commit SHA",
"parent_sha": "parent SHA"
}{
"type": "commit_file",
"commit": "commit SHA",
"path_current": "current file path (empty for deletes)",
"path_previous": "previous file path",
"status": "M|A|D|R100|C075",
"old_hash": "blob hash",
"new_hash": "blob hash",
"old_size": 0,
"new_size": 0,
"additions": 0,
"deletions": 0
}{
"type": "dev",
"dev_id": "SHA256 of lowercase email",
"name": "string",
"email": "string"
}- Git 2.31+
- Go 1.21+ (build only)