A native macOS app (SwiftUI) for analyzing a local git repository: how it grew over time, who contributed, which files churn the most, who owns what, and a fast, syntax-highlighted, commit-by-commit diff browser — including uncommitted work, branch comparisons, and GitHub pull requests.
Point it at any folder that's a git repo and explore. All data comes from
shelling out to git; there's no server, indexing daemon, or account.
- Overview — key stats plus charts: cumulative lines of code over time, tracked-file count over time, commits per week, and a conventional-commit type breakdown.
- Activity — a commit heatmap (weekday × hour punch card), by-weekday and by-hour breakdowns, author-activity-over-time (stacked area), and a code-frequency (adds vs deletes per week) chart.
- Contributors — sortable table of authors with commit counts, lines added/removed, and last-active date, plus a commit-share chart.
- Files — a file-type pie chart (by count or churn) and a sortable, searchable table of the most-churned files. Double-click a file to inspect its history and blame.
- Ownership — bus-factor, single-owner-file risk, and a per-file table of primary owner + ownership share.
- History — searchable commit list with a unified/side-by-side, line-
numbered, syntax-highlighted diff viewer that also highlights the exact
words changed within a modified line. Each file's header stays pinned
while its changes scroll past; a find field filters to files containing a
match, highlights them, and steps through matches (▲/▼); a files menu jumps
to any changed file; each file header offers Copy Path / Reveal in Finder /
Open;
j/kmove between commits; and right-clicking a commit copies its hash. When viewing the checked-out branch, an Uncommitted changes entry appears at the top showing the working-tree diff vs HEAD plus any untracked files. - Compare — pick any two refs to see ahead/behind counts, the commits unique
to the compare ref, and the combined diff between them. The compare side can
also be a GitHub pull request: the newest open PRs (discovered via
refs/pull/*) appear in the picker, fetched on demand and diffed against the base you choose. A selected PR whose commits are already contained in the base is flagged Merged (detects merge / fast-forward merges from git alone; squash- and rebase-merges create new commits and can't be detected). - Branch picker — analyze any local branch, remote-tracking branch, or tag.
- Fetch — a toolbar button (shown when the repo has remotes) runs
git fetch --all --pruneand re-analyzes, so remote branches, tags, and pull requests reflect the server. - Scope filters — narrow the whole analysis to a date range and/or a subdirectory (cancellable, with a session cache so switching back is instant).
- Open recent / drag-and-drop — opens to a Welcome screen where you pick a repo; reopen recents there or from the File menu, or drop a folder onto the window.
- Export — save a Markdown report or a CSV of file stats (File menu).
- Settings (⌘,) — code font size, syntax theme per appearance, and default diff view (unified/split). Diffs re-highlight live when you switch light/dark.
- macOS 14.0+
- Xcode 16+ (built/tested with Xcode 26)
giton the systemPATH(uses/usr/bin/git)
One SPM package: Highlightr (highlight.js via JavaScriptCore) powers the diff syntax highlighting. Everything else is native (SwiftUI + Swift Charts), and all git data comes from shelling out.
The Xcode project is generated from project.yml with
XcodeGen:
xcodegen generate # regenerate GitStats.xcodeproj after adding files
open GitStats.xcodeproj # then Run (⌘R) in XcodeOr from the command line:
xcodebuild -project GitStats.xcodeproj -scheme GitStats -configuration Debug build
xcodebuild test -project GitStats.xcodeproj -scheme GitStats -destination 'platform=macOS'Once running, use File ▸ Open Repository… (⌘O), the Welcome screen, or drag a folder onto the window to pick a repo.
Sources/
GitStatsApp.swift @main App + menu commands + Settings scene
ContentView.swift NavigationSplitView shell, sidebar sections, scope filter
SettingsView.swift Preferences + environment values (font size, theme…)
Models/Models.swift value types: CommitInfo, FileChangeStat, DiffFile, …
Services/
GitRunner.swift async Process wrapper around `git` (cancellable)
GitService.swift analysis pipeline, diffs, blame, compare, parsers
DiffParser.swift unified-diff → DiffFile/DiffHunk/DiffLine
SyntaxHighlighter.swift Highlightr-backed, off-main, contrast-aware
ReportGenerator.swift Markdown / CSV export
ViewModels/
RepositoryViewModel.swift @Observable state, task-owned analysis, LRU caches
Views/ one file per screen/component
Utilities/Formatting.swift formatters + helpers
Tests/ GitStatsTests target (see Tests)
The GitStatsTests target compiles the app sources directly (excluding
GitStatsApp.swift, whose @main can't live in a test bundle) rather than
hosting the app.
All data comes from shelling out to git. The core analysis fans several
independent invocations out concurrently:
git log --numstat …→ commits, contributor totals, per-file churn, per-file ownership, the commit heatmap, code frequency, and lines-of-code over time.git log --reverse -M --name-status …→ tracked-file count over time (A/C add a file, D removes one).git for-each-ref …→ local branches, remotes, and tags.git fetch --all --prune→ the toolbar Fetch action; refreshes remote- tracking branches and tags before re-analyzing.git ls-tree -r <ref>→ tracked-file count at the analyzed ref.git show <hash> --patch -M→ the diff shown in the History tab.git status --porcelain+git diff HEAD→ the uncommitted-changes entry.git rev-list --left-right --count A...B+git diff A...B→ branch compare.git ls-remote <remote> refs/pull/*/head→ the pull-request list; selecting one runsgit fetch <remote> refs/pull/<n>/head:refs/gitstats/pr/<n>(a namespaced ref that never clobbers your own) and then the branch-compare commands against it. The two network calls setGIT_TERMINAL_PROMPT=0so a missing credential can't hang the app.git log --follow --numstat -- <path>→ single-file history.git blame --line-porcelain <ref> -- <path>→ per-line authorship.--since/--until/-- <path>are appended to the log passes when a scope filter is active.
Commit dates use %at (Unix epoch) rather than ISO strings, so they parse with
a cheap integer conversion instead of ISO8601DateFormatter.
The app is built to stay responsive on large repositories and large diffs:
- Concurrent git — the independent invocations in an analysis run in
parallel (
async let), and the whole analysis is cancellable (it terminates the underlyinggitprocess). - Session caches — analyses (LRU, by repo/ref/scope) and diffs (LRU) are cached, so switching back to a branch or commit is instant.
- Row-lazy diffs — the whole diff (every file, hunk, and line) is flattened into one lazy list of sticky-header sections, so only the rows near the viewport are ever built; scrolling stays smooth no matter how large a single file's diff is.
- Chunked diffs — a large file renders ~300 lines with a "Show more" control, so one huge diff never builds thousands of row views at once.
- Cached highlighting — syntax highlighting runs off the main thread, one file at a time over just each file's visible chunk, and is cached so scrolling a file off-screen and back never re-highlights it; diffs colorize progressively and re-colorize live on a light/dark or theme switch.
- Contrast-aware colors — theme colors that don't contrast enough with the app background fall back to the system label color, so text stays readable.
Tests/ holds the GitStatsTests target (61 tests):
DiffParserTests— unified-diff parsing (adds/deletes, hunks, line numbers, binary files).WordDiffTests— word-level intra-line diff: changed-span detection, total rewrites, and replaced-line pairing within a hunk.GitServiceParsingTests—git log/ blame parsers, ownership aggregation, downsampling, and pull-requestls-remoteparsing.DiffChunkerTests— large-file chunk truncation.DiffRowsTests— the flattened diff builder: per-file sections, chunking / show-more, collapse, split-view pairing, and stable row identity.ReportGeneratorTests— Markdown / CSV export: summary totals, ownership percentage, scope, and CSV escaping.RepositoryViewModelTests— drives the view model with a fakeRepositoryService: diff/analysis caching, LRU eviction, and error surfacing.SyntaxHighlighterTests— colored output, contrast enforcement, identity preservation.GitServiceIntegrationTests— creates a throwaway repo and exercises the full pipeline end-to-end (totals, scope filters, blame, working tree, compare).
Three dev-only tools (they never ship in the app), wired through a Makefile:
| Command | Runs |
|---|---|
make format |
swift-format — auto-format all sources in place |
make lint |
swift-format + SwiftLint, both --strict — the CI gate |
make deadcode |
Periphery — dead-code audit |
make hooks |
install the pre-commit hook (one-time per clone) |
swift-format owns formatting (.swift-format); SwiftLint owns semantics
(.swiftlint.yml); rules that overlap are disabled so the two never fight. After
editing Swift, run make format then make lint.
The pre-commit hook (.githooks/pre-commit, enabled by make hooks) runs the
lint gate on staged files — fast, no build — and blocks the commit on any issue.
CI runs the same make lint on every push/PR, plus the tests and a Periphery scan.
Bypass a commit with git commit --no-verify; keep a test-only declaration alive
with a // periphery:ignore comment above it.
- The app is not sandboxed, and deliberately so: it works by shelling out to
/usr/bin/git, which the App Sandbox blocks (and which rules out Mac App Store distribution anyway). Distribute via Developer ID + notarization (e.g. a Homebrew cask), where the sandbox is optional and unwanted. - Line/file time series are downsampled to ~500 points for smooth charting.
- Reopening a repository restores the section (Overview / History / …) you last viewed for it.
- Accessibility: the diff viewer, file headers, untracked files, commit rows, and the compare ahead/behind/merged indicators expose combined VoiceOver labels (change kind + line number + code), so add/remove isn't conveyed by color alone. Every chart labels its marks, so VoiceOver announces each data point (its date or category plus value) as you navigate the plot.
- Merge commits show no single-parent diff (git behavior) and are labeled as such in the history list.
This project is licensed under the terms of the MIT License. See LICENSE for details.