Tauri 2 desktop app for deep Git repository analysis. Rust backend, React/TypeScript frontend, SQLite local database. Solo app, fully local, no auth, no export in v1.
| Layer | Choice |
|---|---|
| Shell | Tauri 2 |
| Backend | Rust (git2, sqlx async, evalexpr) |
| Database | SQLite via sqlx with migrations |
| Frontend | React 19 + TypeScript strict |
| Charts | Recharts |
| Styling | Tailwind CSS v4 |
gitpulse/
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs
│ │ ├── lib.rs
│ │ ├── db/
│ │ │ ├── mod.rs
│ │ │ ├── migrations.rs
│ │ │ └── migrations/
│ │ │ ├── 001_initial.sql
│ │ │ └── 002_aggregates.sql
│ │ ├── git/
│ │ │ ├── mod.rs
│ │ │ ├── scanner.rs # commit parsing, worktree management
│ │ │ ├── incremental.rs # diff since last HEAD
│ │ │ └── rename.rs # git log --follow tracking
│ │ ├── models/
│ │ │ ├── mod.rs
│ │ │ ├── repo.rs
│ │ │ ├── developer.rs
│ │ │ ├── commit.rs
│ │ │ └── stats.rs
│ │ ├── aggregation/
│ │ │ ├── mod.rs
│ │ │ ├── engine.rs # recalcul SQL des agrégats
│ │ │ └── formulas.rs # evalexpr integration
│ │ └── commands/
│ │ ├── mod.rs
│ │ ├── repos.rs
│ │ ├── developers.rs
│ │ ├── stats.rs
│ │ └── boxscore.rs
│ ├── Cargo.toml
│ └── tauri.conf.json
├── src/ # React frontend
│ ├── App.tsx
│ ├── main.tsx
│ ├── components/
│ ├── pages/
│ ├── hooks/
│ └── types/
├── CLAUDE.md # this file
├── PRD.md # product requirements
├── ARCHITECTURE.md # technical decisions
└── package.json
commits— one row per Git commitcommit_file_changes— one row per (commit, file) pair
Rule: these tables are the source of truth. Only appended to during Git scans. Never updated or deleted.
developers,aliases— canonical identities + git name/email mappingsfiles,file_name_history— file tracking with rename chainrepos,workspaces,workspace_reposmetric_formulas— editable scoring formulas stored as text expressions
stats_daily_developer— per (developer, repo, date)stats_daily_file— per (file, date)stats_daily_directory— per (repo, directory_path, date)stats_developer_global— all-time per developerstats_file_global— all-time per filestats_directory_global— all-time per directory
Recalc triggers:
- Alias merge → SQL-only recalc, no Git re-parse
- Formula change → SQL-only recalc
- New commits → Git re-parse (incremental), then recalc
- Use
thiserrorfor all error types, neveranyhowin library code - All public functions must have doc comments (
///) - No
unwrap()orexpect()in production paths — propagate with? - Use
tracingfor logging, notprintln! - Prefer
async fnwithtokioruntime throughout
// Every module defines its own error type
#[derive(Debug, thiserror::Error)]
pub enum GitError {
#[error("repository not found: {path}")]
NotFound { path: String },
#[error("git2 error: {0}")]
Git2(#[from] git2::Error),
}// Always in src-tauri/src/commands/<module>.rs
// Must be registered in lib.rs
#[tauri::command]
pub async fn command_name(
state: tauri::State<'_, AppState>,
param: ParamType,
) -> Result<ReturnType, String> {
// Map errors to String for Tauri serialization
inner(state, param).await.map_err(|e| e.to_string())
}- Prefer
sqlxcompile-time checked macros (query!,query_as!) for stable queries when the project has the required offline/DB setup. Runtimesqlx::queryis acceptable for dynamic SQL and existing migration-backed SQLite queries. - Migrations in
src-tauri/src/db/migrations/*.sql, numbered sequentially - Keep raw string queries centralized and covered by tests when compile-time checked macros are not practical.
- All writes wrapped in transactions
- All models derive
serde::Serialize,serde::Deserialize,sqlx::FromRow - Use
chrono::DateTime<Utc>for all timestamps - UUIDs via
uuid::Uuid
<type>(<scope>): <short description>
Types: feat | fix | refactor | test | chore | docs | perf
Scopes: db | git | aggregation | commands | ui | config
Examples:
feat(db): add migrations for aggregate tables
feat(git): implement worktree-based scanner
fix(aggregation): correct churn score formula
refactor(commands): extract stats queries to repository pattern
- One logical change per commit — never bundle unrelated changes
- Migrations always committed alone (never with application code)
- Frontend and backend changes in separate commits when possible
- Never commit:
.env,*.db,target/,node_modules/,.gitpulse-worktree/
main — stable, always builds
feat/<scope> — new features
fix/<scope> — bug fixes
refactor/<scope>
Implement in this strict order. Never skip ahead.
- Database foundation — migrations (layer 1 + 2 tables),
AppState,DbPool - Git scanner — worktree creation, commit parsing into raw tables
- Alias system — developer/alias CRUD + merge logic
- Aggregation engine — SQL recalc for all
stats_*tables - Tauri commands — expose scanner, alias, stats to frontend
- Frontend scaffold — routing, layout, Tauri invoke wrappers
- Stats pages — dashboard, files, developers
- Box Score — daily cards, player score formula, streaks
// Create worktree for analysis, never touch working tree
let worktree_path = repo_path.join(".gitpulse-worktree");
repo.worktree("gitpulse-analysis", &worktree_path, None)?;Use git log --follow --diff-filter=ACDMR --name-status and build file_name_history entries for each rename. The canonical_id on files stays stable across renames.
Store last_indexed_commit_sha on repos. On rescan: walk from HEAD back to that SHA, only process new commits. Insert in chronological order (oldest first).
When aliases are merged, commit.author_alias_id already points to the right alias row. Recalc is just re-running the aggregate SQL GROUP BY alias.developer_id. No data migration needed.
Default formula stored in metric_formulas:
(commits * 10) + (insertions * 0.5) - (deletions * 0.3) + (files_touched * 2) + (streak_bonus * 3)
Evaluated via evalexpr crate with variables injected per row.
- Do not use
gitCLI subprocess — usegit2crate exclusively - Do not store computed stats in layer 1 tables
- Do not recalculate streaks in Rust — compute them in SQL window functions
- Do not add indexes preemptively — add only when a query is provably slow
- Do not implement export features (v2 scope)
- Do not add authentication or multi-user features