commitmap
Turn git history into a map of where work actually happened.

Why
git log tells you what happened, one commit at a time. It does not tell you
which parts of a codebase soak up the effort, which branches carried the
work, or who works where. commitmap reads the history once and folds it
into a tree you can scan in a few seconds: by directory, by Conventional
Commits scope, by merged branch, or by author. It shells out to your own
git, so it sees exactly what git log sees, including .mailmap.
Install
go install github.com/Mr-hunt-007/commitmap@latest
go install puts the binary in $(go env GOPATH)/bin (usually ~/go/bin). If your shell says command not found, add that directory to your PATH:
echo 'export PATH="$PATH:$(go env GOPATH)/bin"' >> ~/.zshrc && source ~/.zshrc # bash: ~/.bashrc
On Windows the Go installer adds %USERPROFILE%\go\bin to PATH for you.
Or build from source:
git clone https://github.com/Mr-hunt-007/commitmap
cd commitmap
go build -o commitmap .
Requires git 2.31 or newer on PATH. No other dependencies.
Usage
commitmap [flags] [repo]
repo defaults to the current directory. All examples below are real runs
against a small example repository (44 non-merge commits, 12 merges).
By directory (default)
$ commitmap --top 4
main (44 commits, 4 authors, 2024-01-01 → 2024-05-21)
│
├── auth 15 commits 2 authors last 2y
│ ├── login 12 commits 1 author last 2y
│ └── oauth 3 commits 1 author last 2y
├── dashboard 12 commits 2 authors last 2y
│ ├── styles 6 commits 1 author last 2y
│ └── widgets 6 commits 1 author last 2y
├── payments 10 commits 1 author last 2y
│ └── stripe 6 commits 1 author last 2y
├── docs 6 commits 1 author last 2y
└── … 1 more hidden by --min/--top
skipped 4 changes to vendored, build or lock files (--include-all counts them)
A commit counts once per directory no matter how many files it touches
there. payments has 10 commits while payments/stripe has 6 because four
commits only touched files directly inside payments/. Files at the top of
the repository are grouped under (root). A rename counts for both the old
and the new directory.
By Conventional Commits scope
$ commitmap --by scope
main (44 commits, 4 authors, 2024-01-01 → 2024-05-21)
│
├── auth 15 commits 2 authors last 2y feat 9, fix 6
│ ├── login 12 commits 1 author last 2y feat 6, fix 6
│ └── oauth 3 commits 1 author last 2y feat 3
├── dashboard 12 commits 2 authors last 2y feat 6, style 6
├── payments 10 commits 1 author last 2y feat 6, fix 4
│ └── stripe 6 commits 1 author last 2y feat 6
└── (unscoped) 7 commits 2 authors last 2y other 6, chore 1
types: feat 21, fix 10, other 6, style 6, chore 1
feat(auth/login): ... and fix(auth:login): ... both land under
auth then login. chore(api,web): ... counts for both scopes. Scopes are
lower-cased so Auth and auth merge. Commits with no scope go under
(unscoped); commits that are not Conventional Commits at all show as type
other.
By merged branch
$ commitmap --by branch
main (44 commits, 4 authors, 2024-01-01 → 2024-05-21)
│
├── (direct) 19 commits 2 authors last 2y
├── feature 15 commits 2 authors last 2y
│ └── auth 15 commits 2 authors last 2y
│ ├── login-1 3 commits 2 authors last 2y
│ ├── login-2 3 commits 2 authors last 2y
│ ├── login-3 3 commits 2 authors last 2y
│ ├── login-4 2 commits 1 author last 2y
│ ├── login-5 2 commits 1 author last 2y
│ └── login-6 2 commits 1 author last 2y
└── fix 10 commits 1 author last 2y
├── payments-1 2 commits 1 author last 2y
├── payments-2 2 commits 1 author last 2y
├── payments-3 2 commits 1 author last 2y
├── payments-4 2 commits 1 author last 2y
├── payments-5 1 commit 1 author last 2y
└── payments-6 1 commit 1 author last 2y
For every merge that landed on --rev (the merges on its first-parent
chain), commitmap reads the branch name from the subject and counts the
commits that merge brought in, the same set as git rev-list A^1..A^2.
Commits committed straight onto the branch go under (direct). Because each
commit enters the first-parent chain exactly once, top-level counts add up
to the total and nothing is counted twice. Back-merges such as
Merge branch 'main' into feature/x inside a feature branch do not create a
fake main node.
Recognised subjects:
| Subject |
Branch |
Merge branch 'feature/x' (optionally into main or of <url>) |
feature/x |
Merge branches 'a', 'b' and 'c' (octopus) |
a, b, c |
Merge pull request #12 from owner/feature/x |
feature/x |
Merge remote-tracking branch 'origin/feature/x' |
feature/x |
Merged in feature/x (pull request #3) |
feature/x |
Merge feature/x into main |
feature/x |
Anything else is counted under (unparsed merges) and reported in a footer.
By author
$ commitmap --by author --min 5
main (44 commits, 4 authors, 2024-01-01 → 2024-05-21)
│
├── Ines Park 13 commits 1 author last 2y
│ ├── payments 10 commits 1 author last 2y
│ └── … 1 more hidden by --min/--top
├── Leo Grant 12 commits 1 author last 2y
│ ├── dashboard 6 commits 1 author last 2y
│ └── docs 6 commits 1 author last 2y
├── Sam Ortiz 12 commits 1 author last 2y
│ └── auth 12 commits 1 author last 2y
└── Maya Chen 7 commits 1 author last 2y
├── dashboard 6 commits 1 author last 2y
└── … 1 more hidden by --min/--top
skipped 4 changes to vendored, build or lock files (--include-all counts them)
Authors are identified by email after .mailmap is applied (%aN/%aE),
case-insensitively. If one name appears with several emails that .mailmap
does not join, each gets its own node labelled Name <email>.
More examples
commitmap --since "6 months ago" --top 8 # recent hot spots
commitmap --path services/api --depth 3 # zoom into one area
commitmap --rev v1.4.0..v1.5.0 --by scope # what went into a release
commitmap --by branch --rev origin/main --depth 1
commitmap --svg # writes <repo>-commitmap.svg
commitmap ~/code/api --svg map.svg --html map.html
commitmap --json | jq '.root.children[] | {path, commits}'
Use with AI agents
The CLI is already agent-friendly: --json has a stable, documented shape and
the exit codes below are stable. For agents that speak the Model Context
Protocol, commitmap --mcp runs an MCP server on stdio.
The client starts commitmap itself, so it must be on the PATH the client
sees. Editors launched from a dock or start menu often do not inherit your
shell's PATH; if the server fails to start, use the absolute path of the
binary instead (print it with echo "$(go env GOPATH)/bin/commitmap").
Claude Code (add --scope user to enable it in every project):
claude mcp add commitmap -- commitmap --mcp
Codex CLI:
codex mcp add commitmap -- commitmap --mcp
or in ~/.codex/config.toml:
[mcp_servers.commitmap]
command = "commitmap"
args = ["--mcp"]
Cursor, in .cursor/mcp.json (or ~/.cursor/mcp.json for every project):
{"mcpServers": {"commitmap": {"command": "commitmap", "args": ["--mcp"]}}}
VS Code, in .vscode/mcp.json:
{"servers": {"commitmap": {"type": "stdio", "command": "commitmap", "args": ["--mcp"]}}}
Gemini CLI, in ~/.gemini/settings.json (or .gemini/settings.json in a project):
{"mcpServers": {"commitmap": {"command": "commitmap", "args": ["--mcp"]}}}
Tools:
| Tool |
Kind |
What it answers |
commitmap_map |
read-only |
Where work has happened, who works where, what went into a release. Takes the same filters as the CLI (dir, by, depth, rev, since, until, author, paths, min, top, merges, include_all) and returns the same JSON as --json. top defaults to 10 to keep the answer small; a node's hidden count says what was left out, and top: 0 returns everything. |
commitmap_render |
writes files |
Writes the SVG and/or HTML version (svg, html, optional svg_path, html_path) and returns the absolute paths. Without a path the files are <repo>-commitmap.svg/.html in the server's working directory. It creates new files and only replaces files commitmap wrote before; any other existing file is refused. |
Every tool is read-only or only creates its own output files, so there is
nothing to unlock: --allow-destructive is accepted for consistency with
related tools and changes nothing.
An Agent Skill that teaches agents when and how to run the CLI is in
skills/commitmap. Install it for Claude Code
or Codex:
mkdir -p ~/.claude/skills && cp -r skills/commitmap ~/.claude/skills/
mkdir -p ~/.agents/skills && cp -r skills/commitmap ~/.agents/skills/
Contributors and agents working on this repository: see AGENTS.md.
Flags
| Flag |
Default |
Meaning |
--by MODE |
path |
path, scope, branch or author |
--depth N |
path 2, branch 3, author 1, scope 0 |
Tree depth. 0 means unlimited. In author mode it is the directory depth under each author. |
--rev REV |
HEAD |
Revision or range, for example main or v1.0..v2.0 |
--since DATE |
|
Passed to git log --since |
--until DATE |
|
Passed to git log --until |
--author PATTERN |
|
Passed to git log --author |
--path PATH |
|
Only commits touching PATH (relative to the repo). Repeatable. |
--min N |
0 |
Hide nodes with fewer than N commits |
--top N |
0 |
Keep only the N largest children of each node |
--merges |
off |
Count merge commits too. Their file changes are taken against the first parent. |
--include-all |
off |
Do not skip vendored, build and lock files |
--json |
off |
Print JSON instead of the tree |
--svg [FILE] |
|
Also write an SVG diagram. Without FILE: <repo>-commitmap.svg in the current directory |
--html [FILE] |
|
Also write a single self-contained HTML page. Without FILE: <repo>-commitmap.html |
--no-color |
off |
No ANSI colour. NO_COLOR is honoured too. Colour is only used when stdout is a terminal. |
--mcp |
off |
Run as an MCP server on stdio (see Use with AI agents). Other flags are ignored. |
--allow-destructive |
off |
Only valid with --mcp. commitmap has no destructive tools, so it changes nothing. |
--version |
|
Print the version |
-h, --help |
|
Show help |
Flags may appear before or after the repository path. --svg and --html
overwrite the named file if it exists. They take the next argument as the file
name only when it ends in .svg (or .html/.htm), so commitmap --svg ~/code/api maps ~/code/api and saves api-commitmap.svg. Use --svg=FILE
for any other name.
Skipped paths
Unless --include-all is given, file changes under these directories are
ignored: node_modules, vendor, dist, bower_components, __pycache__,
.next, .nuxt, .svelte-kit. So are these files wherever they appear:
package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml,
bun.lock, bun.lockb, deno.lock, go.sum, go.work.sum, Cargo.lock,
Gemfile.lock, composer.lock, poetry.lock, Pipfile.lock, uv.lock,
pdm.lock, mix.lock, pubspec.lock, Package.resolved, flake.lock,
packages.lock.json, gradle.lockfile, plus *.min.js and *.min.css.
The number of skipped changes is printed below the tree. A commit whose only
changes were skipped still counts in the total at the top.
SVG and HTML
commitmap --svg --html # <repo>-commitmap.svg and <repo>-commitmap.html
<repo> is the name of the repository's top-level directory, even when you
run commitmap from a subdirectory. Each file written is reported on stderr, so
the tree on stdout (or --json) is unchanged.
--svg writes a horizontal tree: one row per leaf, a bar per node whose
length is proportional to its commit count, colours per top-level group. It
uses a monospace font so label widths are predictable, shortens names longer
than 40 characters (the full path is in the hover title), and follows the
viewer's light or dark scheme.
--html writes one file with the same diagram inline, plus a collapsible
tree with author counts, last-commit dates and type chips in scope mode. It
has no external requests (no CDN, no fonts, no scripts from elsewhere), so it
works offline and can be attached to a ticket or mailed around.
JSON
--json prints one object. Shape (from a real run with --depth 1 --top 2,
children trimmed):
{
"version": "0.1.0",
"mode": "path",
"rev": "HEAD",
"label": "main",
"depth": 1,
"filters": {
"top": 2,
"merges": false,
"include_all": false
},
"ignored_changes": 4,
"root": {
"name": "main",
"path": "",
"commits": 44,
"authors": 4,
"first": "2024-01-01T10:00:00Z",
"last": "2024-05-21T10:00:00Z",
"hidden": 3,
"children": [
{
"name": "auth",
"path": "auth",
"commits": 15,
"authors": 2,
"first": "2024-01-04T10:00:00Z",
"last": "2024-05-07T10:00:00Z",
"hidden": 0,
"children": []
}
]
}
}
| Field |
Notes |
version, mode, rev, label, depth |
label is the branch name when rev is HEAD. depth 0 means unlimited. |
filters |
since, until, author, paths, min, top appear only when set. merges and include_all are always present. |
ignored_changes |
File changes skipped as vendored, build or lock files. |
merges, unparsed_merges |
Branch mode only: merges that brought in at least one counted commit, and how many of those named no branch. |
node name, path |
path joins names with /; empty for the root. |
node key |
Author mode only: the identity key (lower-cased email). |
node commits, authors |
Distinct commits and distinct authors at or below this node. |
node first, last |
RFC 3339 UTC author dates. Omitted when there are no commits. |
node types |
Scope mode only: commit count per Conventional Commits type. |
node hidden |
Children removed by --min or --top. |
node children |
Always an array, sorted by commits (descending) then name. |
Fields are only added, never renamed or removed, within 0.x minor versions.
Exit codes
| Code |
Meaning |
| 0 |
Success, including an empty repository or a filter that matched nothing |
| 1 |
git failed or is missing, the path is not a git repository, or an output file could not be written |
| 2 |
Invalid flags or arguments |
How it works
commitmap runs git log -z --format=%H%x1f%aN%x1f%aE%x1f%at%x1f%P%x1f%s --name-status -M and parses the NUL-separated stream as it arrives, so it
never holds more than one commit's file list in memory. Paths with spaces,
quotes, newlines or non-ASCII characters come through unquoted. Merge
commits are skipped with --no-merges unless --merges is set, so work on a
branch is not counted a second time when it is merged.
Branch mode loads the commit graph once with git rev-list --parents and
walks the first-parent chain oldest first, which yields every merge's
A^1..A^2 set in a single linear pass instead of one git rev-list per merge.
On a 37,000-commit repository with 15,000 merges, path mode takes about 6
seconds (most of it git's rename detection) with a Go heap under 10 MB.
Branch mode takes under 2 seconds; it keeps the commit graph in memory, about
45 MB of heap for that repository.
Limitations
- Counts are distinct commits, not lines changed. A one-character fix and a
10,000-line rewrite weigh the same.
- In path, scope and author modes a commit can count under several siblings,
so children do not have to add up to their parent. Branch mode is the
exception: top-level nodes partition the commits.
- Dates are author dates.
--since and --until are passed to git, which
filters on committer date, so a rebased commit can pass the filter while
showing an older author date.
- Branch mode only knows what merge subjects say. Squash merges and rebase
merges leave no merge commit, so their commits show as
(direct). A
branch merged into develop and then develop into main is credited to
develop when mapping main; map develop to see the feature branches.
--rev takes a single revision or range. Symmetric ranges (A...B) and
multiple tips are not supported in branch mode.
- Scope mode trusts commit messages. Non-standard types are accepted only
when a scope is present (
deps(npm): ...); a bare sorbet: ... prefix is
not treated as Conventional Commits.
- Rename detection is git's default
-M similarity (50%). Very large
renames may exceed git's rename limit and show as a delete plus an add.
- The SVG estimates text width from a monospace font. If a viewer substitutes
a proportional font, long labels may sit closer to their neighbours.
- Relative ages (
last 4d) use the local clock; the header dates use the
local time zone and JSON uses UTC.
License
MIT