Analysis pipeline¶
Every GitIntel command follows the same three phases: resolve → analyze → render.
sequenceDiagram
autonumber
participant U as You
participant CLI as cli.py
participant R as git.resolver
participant P as analysis.pipeline
participant T as reports.terminal
U->>CLI: gitintel hotspots . --format json
CLI->>R: resolve_repository(source)
R-->>CLI: (pygit2.Repository, RepositoryContext)
CLI->>P: analyze_repository(repository, context)
P-->>CLI: AnalysisContext (nothing computed yet)
CLI->>T: print_hotspots(analysis.hotspots, context, format)
T->>P: read .hotspots
P->>P: commits → changes → ownership → hotspots
T-->>U: rendered report
Phase 1 — Resolve¶
resolve_repository(source) decides how to obtain a repository:
- Remote (
https://github.com/...orhttp://github.com/...): the URL is normalized to end in.git, then cloned into the cache — or reused if already cached — and opened. - Local (anything else): the path is opened directly with
pygit2.Repository. A path that is not a Git repository raisesValueError: Invalid Git repository: <path>.
Metadata is then extracted into a RepositoryContext: branch shorthand from HEAD, remote URL
from origin (or the first remote), owner and repository name parsed out of the remote path,
and source_type set to GitHub when the remote host is github.com.
Details: Repository sources and cache.
Phase 2 — Analyze (lazily)¶
analyze_repository() returns an AnalysisContext. Creating it does no work: every metric
is a cached property computed on first access.
| Property | Depends on | Produces |
|---|---|---|
commits |
repository | list[Commit] walked from HEAD in time order, each with its file changes |
contributors |
commits |
list[Contributor] sorted by commit count |
summary |
commits, contributors |
RepositorySummary (commits, contributors, distinct files) |
ownership |
commits |
list[FileOwnership] per file |
changes |
commits |
per-file {modifications, lines_changed, contributors} |
ownership_map |
ownership |
{file path: last modifier} |
hotspots |
commits, changes, ownership_map |
list[Hotspot] sorted by risk score |
flowchart TD
commits[commits] --> contributors
commits --> ownership
commits --> changes
contributors --> summary
commits --> summary
ownership --> ownership_map[ownership_map]
changes --> hotspots
ownership_map --> hotspots
Because the properties memoize, gitintel analyze walks history once even though the summary,
contributor table, file-activity table, and health table all need the commit list.
Cost model¶
The commit walk is the expensive step: for each commit, libgit2 computes a diff against the first parent. Runtime therefore scales with commits × files changed per commit, not with the size of the working tree. Repositories with tens of thousands of commits take noticeably longer; see Best practices.
Merge commits and the initial commit¶
- A merge commit is diffed against its first parent only, so changes that arrive through a merge are attributed to the commits on the merged branch, not to the merge itself.
- The initial commit has no parent. GitIntel walks its tree and records every blob as an addition of the number of newlines it contains. Binary files therefore contribute a meaningless "line" count in that one commit.
Phase 3 — Render¶
The command hands the computed values to a print_* function in reports/terminal.py, which
dispatches on --format:
print_analysis → print_analysis_json | print_analysis_markdown | print_analysis_table
print_ownership → print_ownership_json | print_ownership_markdown | print_ownership_table
print_hotspots → print_hotspots_json | print_hotspots_markdown | print_hotspots_table
All three formats are written to stdout through a Rich Console. Progress spinners are written
by run_with_progress() and can be disabled with the global --quiet flag.
Cleanup¶
After rendering, commands remove the analyzed checkout only if the context is marked
temporary and the directory name starts with gitintel_. Cached clones of GitHub URLs live
in a stable cache directory and are intentionally preserved between runs.