Skip to content

Repository files navigation

GitStats

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.

Features

  • 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/k move 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 --prune and 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.

Requirements

  • macOS 14.0+
  • Xcode 16+ (built/tested with Xcode 26)
  • git on the system PATH (uses /usr/bin/git)

Dependencies

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.

Build & run

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 Xcode

Or 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.

Project structure

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.

How it works

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 runs git 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 set GIT_TERMINAL_PROMPT=0 so 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.

Performance

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 underlying git process).
  • 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

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.
  • GitServiceParsingTestsgit log / blame parsers, ownership aggregation, downsampling, and pull-request ls-remote parsing.
  • 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 fake RepositoryService: 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).

Code quality

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.

Notes

  • 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.

License

This project is licensed under the terms of the MIT License. See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages