{"id":48446210,"url":"https://github.com/jacobarthurs/gitrisk","last_synced_at":"2026-04-06T18:01:50.127Z","repository":{"id":343119363,"uuid":"1176198826","full_name":"JacobArthurs/gitrisk","owner":"JacobArthurs","description":"Analyze PR risk using git history: churn, ownership fragmentation, bug-fix proximity, and change coupling","archived":false,"fork":false,"pushed_at":"2026-03-09T00:04:17.000Z","size":8,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-09T05:25:04.191Z","etag":null,"topics":["cli","cobra","developer-tools","git","github-actions","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JacobArthurs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-08T18:47:02.000Z","updated_at":"2026-03-09T00:04:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/JacobArthurs/gitrisk","commit_stats":null,"previous_names":["jacobarthurs/gitrisk"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/JacobArthurs/gitrisk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JacobArthurs%2Fgitrisk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JacobArthurs%2Fgitrisk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JacobArthurs%2Fgitrisk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JacobArthurs%2Fgitrisk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JacobArthurs","download_url":"https://codeload.github.com/JacobArthurs/gitrisk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JacobArthurs%2Fgitrisk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31483380,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","cobra","developer-tools","git","github-actions","golang"],"created_at":"2026-04-06T18:01:46.358Z","updated_at":"2026-04-06T18:01:50.121Z","avatar_url":"https://github.com/JacobArthurs.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gitrisk (Work in progress)\n\n## What it is\n\nA CLI binary that analyzes the files changed in your current branch or staged diff and outputs a risk score for each file, ranked by danger. It answers the question a reviewer should ask before opening a PR: **\"which of these changes are actually risky?\"**\n\nUses only local git history. No network, no account, no external service. Runs on private repos with no data leaving the machine. Integrates into any CI pipeline in under 30 seconds.\n\n---\n\n## The core insight\n\nA one-line change to a file that has been modified 40 times in the last 3 months by 8 different authors, and has 5 bug-fix commits touching it, is more dangerous than a 500-line change to a test file nobody has touched in a year. Lines changed is a bad proxy for risk. Git history is a far better one.\n\nThis is the idea Adam Tornhill wrote an entire book about (*Your Code as a Crime Scene*), that CodeScene built a company on, and that no free local CLI tool implements at PR time.\n\n---\n\n## The four signals\n\nEvery file in the diff gets scored across four independent dimensions:\n\n**1. Churn rate**\nHow frequently has this file changed in the last 90 days. High churn = unstable code, unclear ownership, or a load-bearing file everyone touches constantly. Churn = commit count, computed from `git log --no-merges --oneline -- \u003cfile\u003e`.\n\n**2. Ownership fragmentation**\nHow many distinct authors have touched this file in the last 90 days. One author = clear ownership. Eight authors = coordination risk, inconsistent patterns, nobody fully understands it. Computed from `git log --no-merges --format=\"%ae\"`. Author identity uses email deduplication; acknowledged limitation: engineers with multiple emails or inconsistent `user.email` configs will be over-counted. Document this rather than silently treating it as reliable.\n\n**3. Bug-fix proximity**\nHow many commits touching this file in the last 90 days contained bug-fix language in the message. A file that required 6 bug fixes recently is structurally troubled.\n\nFetched via `git log --no-merges --after=\"90 days ago\" --format=\"%s\" -- \u003cfile\u003e` (subject line only), then filtered Go-side using `regexp.MustCompile(`(?i)\\b(fix|bug|hotfix|patch|defect)\\b`)`. This avoids using `--grep` with `-P` (PCRE), which is not available on all git versions. Go-side filtering is fully portable and gives complete control over the pattern.\n\n**4. Change coupling**\nFiles that historically change together but are absent from this diff. If files A and B have co-changed in 80% of commits over the past 6 months, and your PR touches A but not B, that's a warning that you may have forgotten something. Computed from co-occurrence frequency across `git log --no-merges`. Coupling is opt-in via `--coupling` flag due to performance cost on large repos (see Technical Implementation).\n\n---\n\n## Scoring and normalization\n\nEach signal is scored 0–10 using **repo-relative normalization**: the highest observed value across all files in the repo's recent history anchors the top of the scale. This ensures scores are meaningful on both low-activity and high-activity repos rather than against arbitrary absolute ceilings.\n\nNormalization uses a floor on the denominator to prevent misleading scores on new or sparse repos:\n\n```text\nscore = (raw_value / max(signal_max, minimum_floor)) * 10\n```\n\nDefault floors: churn = 10 commits, fragmentation = 3 authors, bugfix = 3 commits. These are configurable. Without a floor, a repo with a single file at 2 churn commits would score that file 10.0, which is meaningless.\n\n```text\nrisk = (churn × 0.30) + (fragmentation × 0.25) + (bugfix × 0.30) + (coupling × 0.15)\n```\n\nWhen `--coupling` is not enabled, coupling's 0.15 weight is redistributed proportionally across the other three signals so the composite still sums to 10:\n\n```text\nchurn × 0.353 + fragmentation × 0.294 + bugfix × 0.353\n```\n\nThis is computed automatically. The user never sees adjusted weights, but the score is not artificially capped at 8.5.\n\nWeights are configurable. Output buckets:\n\n| Score | Bucket   |\n|-------|----------|\n| 0–3.9 | LOW      |\n| 4–6.9 | MEDIUM   |\n| 7–8.9 | HIGH     |\n| 9–10  | CRITICAL |\n\nChange coupling outputs separately as a warning, not a file score — \"you changed X, historically Y also changes with it, Y is not in this diff.\"\n\n---\n\n## How it is used\n\n**Basic usage — run on current branch before opening PR:**\n\n```bash\ngitrisk analyze\n```\n\nDiffs current branch against main, scores every changed file, outputs ranked risk table.\n\n**Specific branch comparison:**\n\n```bash\ngitrisk analyze --base main --head feature/payment-refactor\n```\n\n**Staged changes only:**\n\n```bash\ngitrisk analyze --staged\n```\n\n**Include coupling analysis:**\n\n```bash\ngitrisk analyze --coupling\n```\n\n**CI integration — fail on high risk:**\n\n```bash\ngitrisk analyze --base main --format json\ngitrisk analyze --base main --threshold high --exit-code\n```\n\nReturns exit code 1 if any file scores HIGH or above. Plugs into GitHub Actions, Jenkins, any CI pipeline.\n\n**Output:**\n\n```text\n┌─────────────────────────────────────────────────────────┐\n│ gitrisk — PR Risk Analysis                              │\n├─────────────────────────────┬───────┬───────────────────┤\n│ File                        │ Risk  │ Signals           │\n├─────────────────────────────┼───────┼───────────────────┤\n│ src/payments/processor.go   │ HIGH  │ churn             │\n│ src/auth/jwt.go             │ HIGH  │ bugfix            │\n│ internal/db/migrations.go   │ MED   │ authors           │\n│ cmd/cli/flags.go            │ LOW   │ —                 │\n│ README.md                   │ LOW   │ —                 │\n└─────────────────────────────┴───────┴───────────────────┘\n\n⚠ Coupling warning:\n  src/payments/processor.go historically co-changes with\n  src/payments/validator.go (87% of commits), and is not in this diff\n```\n\n---\n\n## Configuration file\n\n`.gitrisk.yml` at repo root:\n\n```yaml\nlookback_days: 90           # history window for churn, fragmentation, bugfix signals\ncoupling_lookback_days: 180 # history window for coupling (longer = more signal)\ncoupling_threshold: 0.75    # co-change frequency to trigger warning\nweights:\n  churn: 0.30\n  fragmentation: 0.25\n  bugfix: 0.30\n  coupling: 0.15\nignore:\n  - \"**/*_test.go\"\n  - \"docs/**\"\n  - \"*.md\"\nthresholds:\n  high: 7.0\n  critical: 9.0\n```\n\nZero config required, all defaults are sensible out of the box.\n\n---\n\n## CLI surface\n\n```bash\ngitrisk analyze                     run analysis on current branch vs main\ngitrisk analyze --staged            run analysis on staged changes only\ngitrisk analyze --coupling          include change coupling analysis (slower)\ngitrisk analyze --base \u003cref\u003e        set base ref for diff (default: main)\ngitrisk analyze --head \u003cref\u003e        set head ref for diff (default: current branch)\ngitrisk analyze --format json       output as JSON instead of table\ngitrisk analyze --threshold \u003clevel\u003e set minimum risk level to report (low/medium/high/critical)\ngitrisk analyze --exit-code         return exit code 1 if any file meets or exceeds threshold\ngitrisk explain \u003cfile\u003e              show full signal breakdown for one file\ngitrisk hotspots                    show top 10 riskiest files repo-wide (full lookback, not PR-scoped)\ngitrisk coupling \u003cfile\u003e             show all historically coupled files for one file\n```\n\n`explain` shows the raw numbers behind the score:\n\n```bash\n$ gitrisk explain src/payments/processor.go\n\nsrc/payments/processor.go\n─────────────────────────\nChurn (last 90d):        43 commits    → score 8.6  (repo max: 50)\nUnique authors (90d):    7 authors     → score 7.0  (repo max: 10)\nBug-fix commits (90d):   4 commits     → score 6.0  (repo max: 7)\nCoupled files:           validator.go  → 87% co-change rate\n\nComposite risk: 7.4 / 10  [HIGH]\n```\n\nThe repo max shown per-signal makes the normalization transparent.\n\n---\n\n## Technical implementation in Go\n\n**Git operations**: Shell out to git directly via `os/exec`. No go-git library — native git is faster, always available, and the commands needed are stable.\n\n**All `git log` commands include `--no-merges`** to prevent merge commits from inflating churn and author counts on merge-heavy workflows.\n\n**Diff detection**: `git diff --name-only \u003cbase\u003e...\u003chead\u003e`\n\n**Churn**: `git log --no-merges --after=\"90 days ago\" --oneline -- \u003cfile\u003e` — count output lines per file.\n\n**Fragmentation**: `git log --no-merges --after=\"90 days ago\" --format=\"%ae\" -- \u003cfile\u003e` — deduplicate, count unique emails.\n\n**Bug-fix proximity**: `git log --no-merges --after=\"90 days ago\" --format=\"%s\" -- \u003cfile\u003e` — pipe commit subjects through Go-side regex `(?i)\\b(fix|bug|hotfix|patch|defect)\\b`, count matches. Avoids `--grep -P` (PCRE) which is unavailable on some git distributions, including macOS Xcode git.\n\n**Normalization**: Before scoring, run churn/fragmentation/bugfix queries across all files touched in the lookback window to establish per-signal maximums. Score each file as `(raw_value / max(signal_max, floor)) * 10`. Default floors: 10 (churn), 3 (fragmentation), 3 (bugfix). Cache this pass — it runs once per invocation, not per file.\n\n**Change coupling**: `git log --no-merges --after=\"180 days ago\" --name-only --format=\"\"` gives all commits with their changed files. Build a co-occurrence matrix in memory. For each file in the diff, find all files with co-change frequency above threshold that are absent from the current diff. This is opt-in via `--coupling` flag.\n\n**Performance**: File-level git commands run concurrently with a goroutine worker pool (configurable, default 8 workers). Target under 3 seconds on repos with 10k commits for the default (no-coupling) path. Coupling on large repos (50k+ commits) will be slower — document expected runtime and suggest capping `coupling_lookback_days` for monorepos.\n\n**Dependencies:**\n\n- `github.com/spf13/cobra` — CLI structure\n- `github.com/charmbracelet/lipgloss` — terminal table styling\n- `gopkg.in/yaml.v3` — config file\n- Everything else is standard library\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjacobarthurs%2Fgitrisk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjacobarthurs%2Fgitrisk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjacobarthurs%2Fgitrisk/lists"}