From b63fb1d418f5679d6869e0454c8f3afd7fa48c97 Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Thu, 21 May 2026 23:47:42 +0100 Subject: [PATCH 1/3] Extract repo-intel into standalone repo (github.com/tyom/repo-intel) repo-intel now lives in its own repo with a CLI, GitHub Action, and Homebrew formula. This removes the in-tree source and build targets: - Remove src/repo-intel/ and the repo-intel-* Makefile targets - Untrack + gitignore stow/bin/repo-intel (kept locally so ~/bin keeps working) - setup.sh: fetch the artifact into stow/bin via curl when not installed by brew - brew.sh: install tyom/tap/repo-intel non-fatally (won't block core packages) - README: point at the standalone repo --- .gitignore | 5 + Makefile | 11 +- README.md | 4 +- scripts/install/brew.sh | 8 + scripts/setup.sh | 18 + src/repo-intel/README.md | 204 --- src/repo-intel/build.py | 53 - src/repo-intel/gen_techdata.py | 302 ---- src/repo-intel/repo-intel.py | 1982 ----------------------- src/repo-intel/techdata.json | 2749 -------------------------------- src/repo-intel/template.html | 2060 ------------------------ stow/bin/repo-intel | 1982 ----------------------- 12 files changed, 33 insertions(+), 9345 deletions(-) delete mode 100644 src/repo-intel/README.md delete mode 100644 src/repo-intel/build.py delete mode 100644 src/repo-intel/gen_techdata.py delete mode 100755 src/repo-intel/repo-intel.py delete mode 100644 src/repo-intel/techdata.json delete mode 100644 src/repo-intel/template.html delete mode 100755 stow/bin/repo-intel diff --git a/.gitignore b/.gitignore index ac00d6f..ab0287e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ *.log .env + +# repo-intel is now a standalone tool (github.com/tyom/repo-intel). +# The artifact is installed via Homebrew or fetched into the stow tree +# during setup, not committed here. +stow/bin/repo-intel diff --git a/Makefile b/Makefile index e2c6674..47d0db2 100644 --- a/Makefile +++ b/Makefile @@ -47,13 +47,4 @@ docker-test-remote-local: ## Test remote install using local HTTP server docker build -f Dockerfile.remote-test -t $(IMAGE_NAME)-remote . docker run --rm $(IMAGE_NAME)-remote remote-test-local -repo-intel-build: ## Rebuild stow/bin/repo-intel from src/repo-intel - python3 src/repo-intel/build.py stow/bin/repo-intel - -repo-intel-techdata: ## Regenerate src/repo-intel/techdata.json from Linguist (needs network) - python3 src/repo-intel/gen_techdata.py - -repo-intel-dev: ## Run repo-intel from source (reads template.html + techdata.json live; pass args via ARGS=) - python3 src/repo-intel/repo-intel.py $(ARGS) - -.PHONY: help install uninstall brew docker-build docker-test docker-shell docker-setup docker-clean docker-test-remote docker-test-remote-local repo-intel-build repo-intel-techdata repo-intel-dev +.PHONY: help install uninstall brew docker-build docker-test docker-shell docker-setup docker-clean docker-test-remote docker-test-remote-local diff --git a/README.md b/README.md index 5cf01d4..fc128da 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Personal dotfiles for macOS and Linux, designed for a smooth developer experienc - **Vim**: Pre-configured with vim-plug and curated plugins - **CLI Tools**: bat (syntax-highlighted cat), fzf (fuzzy finder), git-delta (better diffs), and more via Homebrew - **Dev Tools**: Volta and Node.js; Bun (optional) -- **Bin Scripts**: Handy commands like `ungit` (clone GitHub repos/subdirs as files or text) and [`repo-intel`](./src/repo-intel/README.md) (contributor stats dashboard for any git repo) +- **Bin Scripts**: Handy commands like `ungit` (clone GitHub repos/subdirs as files or text) and [`repo-intel`](https://github.com/tyom/repo-intel) (contributor stats dashboard for any git repo — now a standalone tool, installed via Homebrew or fetched into `~/bin` during setup) - **Claude Code Plugin**: Custom commands for code review, explanation, and refactoring ![Shell screenshot](https://tyom.github.io/dotfiles/shell.png) @@ -257,8 +257,6 @@ Run `make` to see all available commands: | `make docker-clean` | Remove persistent Docker containers | | `make docker-test-remote` | Smoke test remote install (deployed URL) | | `make docker-test-remote-local` | Test remote install via local HTTP server | -| `make repo-intel-build` | Rebuild `stow/bin/repo-intel` from `src/` | -| `make repo-intel-dev` | Run `repo-intel` from source (pass `ARGS=`) | Docker commands support `VARIANT=minimal` for testing without Homebrew/Bun (e.g., `make docker-test VARIANT=minimal`). diff --git a/scripts/install/brew.sh b/scripts/install/brew.sh index ab9778b..144d2b8 100755 --- a/scripts/install/brew.sh +++ b/scripts/install/brew.sh @@ -52,4 +52,12 @@ fi print_step "Updating Homebrew" && brew update print_step "Installing Homebrew packages" && brew install "${packages[@]}" + +# repo-intel (standalone tool: github.com/tyom/repo-intel). Installed separately +# and non-fatally so a missing/unpublished tap can't abort the core packages +# above. If this is skipped, setup.sh's curl fallback installs it into ~/bin. +print_step "Installing repo-intel (tap: tyom/homebrew-tap)" && + brew install tyom/tap/repo-intel || + print_info "Skipping repo-intel via brew (setup will fetch it via curl)" + print_info "Cleaning outdating brew packages" && brew cleanup diff --git a/scripts/setup.sh b/scripts/setup.sh index 5bec4db..194eb81 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -61,6 +61,24 @@ fi print_step 'Setting up zsh' && source "$DOTFILES_DIR/scripts/zsh.sh" +# repo-intel is a standalone tool (github.com/tyom/repo-intel). On macOS it's +# installed via Homebrew (tyom/tap/repo-intel) above. Elsewhere — or if brew +# was skipped — fetch the single-file artifact into the stow tree so it gets +# symlinked to ~/bin like the other bin scripts. +if ! command -v repo-intel &>/dev/null; then + print_step 'Installing repo-intel' + if curl -fsSL https://raw.githubusercontent.com/tyom/repo-intel/main/dist/repo-intel \ + -o "$DOTFILES_DIR/stow/bin/repo-intel"; then + chmod +x "$DOTFILES_DIR/stow/bin/repo-intel" + print_success 'repo-intel fetched into stow/bin' + else + rm -f "$DOTFILES_DIR/stow/bin/repo-intel" + print_error 'Failed to fetch repo-intel (skipping)' + fi +else + print_info 'repo-intel already installed' +fi + print_step 'Symlinking dotfiles' && source "$DOTFILES_DIR/scripts/stow.sh" diff --git a/src/repo-intel/README.md b/src/repo-intel/README.md deleted file mode 100644 index a2e1cbf..0000000 --- a/src/repo-intel/README.md +++ /dev/null @@ -1,204 +0,0 @@ -# repo-intel - -Source for the `repo-intel` contributor-stats dashboard. The shipped -executable at `stow/bin/repo-intel` is built from the files here — the -HTML template is embedded into the script so the artifact is -single-file and depends only on Python 3 + `git`. - -`repo-intel` reads commit history from either the current git repo or -a remote GitHub repo and writes a self-contained HTML dashboard -showing top contributors, weekly/daily activity, time-of-day patterns, -and per-author commit feeds. It also breaks down work by **language** -(per-commit file types in the timeline tooltip, a per-author language -bar in the contributor popover, and a repo-wide "Technologies" section) -and detects **frameworks** from dependency manifests grouped by language. - -The per-file **language** breakdown needs file-level line stats that only -the local and bare-clone paths produce — the token-authenticated GraphQL -remote path omits it, so the Technologies section's language column shows a -short placeholder there instead. **Framework** detection works on every path -(on the remote path the dependency manifests are fetched over the API). - -The [GitHub CLI (`gh`)](https://cli.github.com/) is optional but -recommended: when authenticated (`gh auth login`), `repo-intel` uses -its token to fetch remote repos via the GitHub GraphQL API and to -enrich author cards with GitHub profile data (avatar, bio, follower -counts, etc.). Without `gh`, the script falls back to `$GITHUB_TOKEN` -or a bare-clone of the remote, and author cards show git data only. - -## Run without installing - -Pipe the shipped artifact straight into Python — no clone, no install: - -```bash -curl -sSL https://raw.githubusercontent.com/tyom/dotfiles/master/stow/bin/repo-intel \ - | python3 - -``` - -Everything after `python3 -` is forwarded to the script. Replace -`` with the GitHub repo you want stats for (e.g. `tyom/dotfiles`), -or drop it to run against the current directory's git repo. Append `--help` -(or `-h`) for the full flag reference: - -```bash -curl -sSL https://raw.githubusercontent.com/tyom/dotfiles/master/stow/bin/repo-intel \ - | python3 - --help -``` - -The script is self-contained (Python 3 + `git`, optional `gh`). In this mode -stdin is the script body, so the interactive subset prompt for large remote -repos is auto-skipped and the script fetches all commits — pass -`--commits N` (or `--since` / `--until`) to trim the fetch on big repos. - -## Usage - -``` -repo-intel [N] [REPO] [options] -``` - -- `N` — number of top contributors to include (default `10`). -- `REPO` — `owner/repo`, `https://github.com/owner/repo`, or - `remote:owner/repo`. Omit to use the cwd's git repo. - -Run `repo-intel --help` for the full flag reference. - -### Modes - -- **Local** — no `REPO`. Reads `git log` from the current working directory. -- **Remote (GraphQL)** — `REPO` plus a GitHub token from - `gh auth token -h github.com` or `$GITHUB_TOKEN`. Fetches via the - GitHub GraphQL API. -- **Remote (bare-clone fallback)** — `REPO` with no token. Clones to - `/tmp/repo-intel--.git` and reads locally. Subsequent - runs `git fetch` the cached bare clone. - -### Filtering commits - -| Flag | Meaning | -| --------------------- | ------------------------------------------------------------------------ | -| `--commits N` | Last `N` commits (newest) | -| `--commits A-B` | Positions `[A, B)` counted from the oldest commit (0-indexed, half-open) | -| `--since YYYY-MM-DD` | Commits on or after the date (inclusive) | -| `--until YYYY-MM-DD` | Commits on or before the date (inclusive) | - -Filters compose: date bounds apply first, then the position slice. The -run prints `filtered: X/total commits` so you can see what was kept. - -When a remote repo has more than 1000 commits and no filter flag was -passed, `repo-intel` prompts interactively for a subset (Last 500, Last -1000, Past year, or All). The prompt requires the GraphQL path -(token-authenticated), because that's where picking a subset actually -saves network — the bare-clone fallback downloads everything regardless, -so it skips the prompt and you can pass `--commits` / `--since` to trim -the display instead. Also skipped when stdin/stderr is not a TTY or when -any of `--commits` / `--since` / `--until` is given. - -### Output - -| Flag | Default | -| --------------------- | ---------------------------------------------------------------------------------------- | -| `-o, --output PATH` | `/tmp/--.html` (or `/tmp/.html` for a local repo without a GitHub origin) | -| `--no-open` | Opens the result in your default browser | - -`--output` creates parent directories if they don't exist. - -### Cache - -Remote runs cache commit nodes per repo under -`$XDG_CACHE_HOME/repo-intel` (default `~/.cache/repo-intel`), one JSON -file per repo. The next run paginates from HEAD and stops at the first -already-cached SHA, so only new commits since the last fetch hit the -network. - -- `--no-cache` — ignore the cache and re-fetch everything (also skips - `git fetch` on the bare-clone fallback). -- Delete the relevant `-.json` to force a fresh fetch for - one repo. - -The cache assumes linear history extension; force-pushes that rewrite -history may leave orphan SHAs in the cache. Pass `--no-cache` after a -known force-push if precision matters. - -### Examples - -```bash -repo-intel # cwd, top 10 -repo-intel 20 # cwd, top 20 -repo-intel tyom/dotfiles # remote, top 10 -repo-intel --commits 100 tyom/dotfiles # last 100 commits -repo-intel --commits 0-100 tyom/dotfiles # first 100 commits -repo-intel --commits 400-800 facebook/react # 400 commits at positions 400..799 -repo-intel --since 2024-01-01 --until 2024-12-31 . # all of 2024 in cwd -repo-intel --no-open -o ./stats.html tyom/dotfiles # save without opening -repo-intel --no-cache tyom/dotfiles # bypass cache -``` - -## Files - -| File | Purpose | -| ----------------- | ---------------------------------------------------------------------------- | -| `repo-intel.py` | The script. Holds `TEMPLATE` + `TECHDATA` placeholders until bundled | -| `template.html` | Dashboard HTML, with `/*__DATA_INJECTION__*/` for runtime data | -| `techdata.json` | Generated language + framework detection data (committed; embedded at build) | -| `gen_techdata.py` | Regenerates `techdata.json` from GitHub Linguist + a curated framework map | -| `build.py` | Substitutes the `TEMPLATE` / `TECHDATA` lines with their data as a `repr()` | - -### Detection data (`techdata.json`) - -Language detection (extension/filename → language, colors, vendored-path noise -filter) is generated from [GitHub Linguist](https://github.com/github-linguist/linguist) -— `languages.yml` (with fine-grained languages folded into their `group`, e.g. -`TSX`→`TypeScript`) and `vendor.yml`. Frameworks are a small curated -dependency → framework map maintained in `gen_techdata.py` (Vercel/Netlify's -lists target deploy presets, not the libraries a repo uses, so they're a poor -fit). `techdata.json` is committed and embedded into the artifact, so the -shipped tool stays offline and single-file. - -## Workflows - -**Build the shipped artifact** (run after editing source or template): - -```bash -make repo-intel-build -``` - -Writes `stow/bin/repo-intel` (chmod 0755). Commit both source and -artifact — the artifact is checked in so a fresh clone + `make install` -works without a build step. `repo-intel-build` reads the committed -`techdata.json`; it is **not** regenerated on every build (that would need -network), so builds stay offline and reproducible. - -**Refresh detection data** (only when bumping Linguist or editing the -framework map — needs network): - -```bash -make repo-intel-techdata # rewrites techdata.json; then commit it + rebuild -``` - -**Develop against the source live** (no rebuild needed between edits): - -```bash -make repo-intel-dev # uses cwd, top 10 -make repo-intel-dev ARGS="3 facebook/react" # top 3 of a remote repo -``` - -The source script auto-detects that `TEMPLATE` is still the placeholder -and falls back to reading `template.html` (and `techdata.json`) from disk. -The built artifact never hits that branch — it carries both embedded. - -## How the embedding works - -`build.py` looks for exactly one occurrence each of: - -```python -TEMPLATE = "__TEMPLATE_PLACEHOLDER__" -TECHDATA = "__TECHDATA_PLACEHOLDER__" -``` - -and replaces them with `TEMPLATE = ` and -`TECHDATA = `. The result is a valid Python file -carrying both as string literals. Templating happens at build time; the -runtime substitution of `/*__DATA_INJECTION__*/` with -`window.__DATA__ = {...}` still happens inside `main()` as before. When -unbuilt, the script detects the placeholders and reads `template.html` -and `techdata.json` from disk instead. diff --git a/src/repo-intel/build.py b/src/repo-intel/build.py deleted file mode 100644 index 466f3ee..0000000 --- a/src/repo-intel/build.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -"""Bundle src/repo-intel into a single-file executable. - -Substitutes two placeholders in repo-intel.py with their data as Python string -literals — TEMPLATE with template.html, TECHDATA with techdata.json — then -writes the result to the given output path with mode 0755. -""" - -import os -import sys -from pathlib import Path - -TEMPLATE_PLACEHOLDER = 'TEMPLATE = "__TEMPLATE_PLACEHOLDER__"' -TECHDATA_PLACEHOLDER = 'TECHDATA = "__TECHDATA_PLACEHOLDER__"' - - -def main(): - if len(sys.argv) != 2: - sys.exit("usage: build.py OUTPUT_PATH") - out_path = Path(sys.argv[1]) - - src_dir = Path(__file__).resolve().parent - script = (src_dir / "repo-intel.py").read_text() - template = (src_dir / "template.html").read_text() - - techdata_path = src_dir / "techdata.json" - if not techdata_path.exists(): - sys.exit( - f"error: {techdata_path} not found — run `make repo-intel-techdata` " - "(needs network) to generate it, then commit it." - ) - techdata = techdata_path.read_text() - - for name, placeholder in ( - ("template.html", TEMPLATE_PLACEHOLDER), - ("techdata.json", TECHDATA_PLACEHOLDER), - ): - if script.count(placeholder) != 1: - sys.exit(f"error: expected exactly one {placeholder!r} line in repo-intel.py") - - bundled = ( - script - .replace(TEMPLATE_PLACEHOLDER, f"TEMPLATE = {template!r}") - .replace(TECHDATA_PLACEHOLDER, f"TECHDATA = {techdata!r}") - ) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(bundled) - out_path.chmod(0o755) - print(f"built {out_path} ({os.path.getsize(out_path):,} bytes)") - - -if __name__ == "__main__": - main() diff --git a/src/repo-intel/gen_techdata.py b/src/repo-intel/gen_techdata.py deleted file mode 100644 index 439bd28..0000000 --- a/src/repo-intel/gen_techdata.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python3 -"""Generate techdata.json — language + framework detection data for repo-intel. - -Languages are generated from GitHub Linguist (the canonical, maintained source -for extension→language mappings and the official colors); frameworks are a -small curated map. We evaluated scraping Vercel's `frameworks.ts` for the JS -side but it targets *deployment-framework CLIs* (`next`, `react-scripts`), not -the libraries a repo uses — `react`/`express`/`vue` aren't in it — so it's the -wrong shape for "what does this repo use" and the curated map stays on-target. - -Writes a single committed JSON snapshot that repo-intel.py loads (and that -build.py embeds into the artifact): - - - Languages: Linguist `languages.yml` (extension/filename → language, colors, - with fine-grained languages folded into their `group`, e.g. TSX→TypeScript) - + `vendor.yml` (vendored-path regexes for the noise filter). - - Frameworks: curated dependency → framework maps (web + backend) below. - -Run via `make repo-intel-techdata` (needs network). Stdlib-only. -""" - -import json -import re -import sys -import urllib.request -from pathlib import Path - -LANGUAGES_YML = "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/languages.yml" -VENDOR_YML = "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/vendor.yml" - -OUT = Path(__file__).resolve().parent / "techdata.json" - -TYPE_RANK = {"programming": 0, "markup": 1, "data": 2, "prose": 3, "": 4} - -# Ambiguous extensions claimed by several languages. Linguist resolves these in -# code (a classifier + popularity), not data, so derivation alone mis-assigns -# (e.g. `.md` → "GCC Machine Description"). This tiebreaker layer pins the few -# that users actually notice; the chosen name must be a real Linguist language. -EXT_OVERRIDE = { - "md": "Markdown", "markdown": "Markdown", "h": "C", "m": "Objective-C", - "r": "R", "pl": "Perl", "t": "Perl", "l": "Common Lisp", "v": "Verilog", - "f": "Fortran", "for": "Fortran", "cls": "Apex", "pro": "Prolog", - "ts": "TypeScript", "rs": "Rust", "cs": "C#", "sql": "SQL", -} - -# Generic extensions whose canonical Linguist owner is the colorless "Text" -# language. Because color-less languages are dropped from the tables, a niche -# *colored* claimant would otherwise win the slot (e.g. `.txt` → "Adblock Filter -# List") — Linguist itself resolves these via a content classifier we can't run, -# so we leave them unassigned and let classify_path bucket them as "Other". -EXT_EXCLUDE = {"txt"} - -# Curated web/npm dependency → framework display name. Vercel/Netlify answer a -# different question (deploy presets), so this is maintained directly. -CURATED_WEB = { - "react": "React", "react-dom": "React", "next": "Next.js", - "vue": "Vue", "nuxt": "Nuxt", "@angular/core": "Angular", - "svelte": "Svelte", "@sveltejs/kit": "SvelteKit", - "solid-js": "SolidJS", "preact": "Preact", "astro": "Astro", - "gatsby": "Gatsby", "@remix-run/react": "Remix", - "express": "Express", "koa": "Koa", "fastify": "Fastify", - "@nestjs/core": "NestJS", "@hapi/hapi": "hapi", - "electron": "Electron", "react-native": "React Native", - "expo": "Expo", "@ionic/core": "Ionic", - "vite": "Vite", "webpack": "webpack", "rollup": "Rollup", - "esbuild": "esbuild", "parcel": "Parcel", - "tailwindcss": "Tailwind CSS", "bootstrap": "Bootstrap", - "@mui/material": "MUI", "@chakra-ui/react": "Chakra UI", - "styled-components": "styled-components", - "jest": "Jest", "vitest": "Vitest", "mocha": "Mocha", - "playwright": "Playwright", "@playwright/test": "Playwright", - "cypress": "Cypress", "puppeteer": "Puppeteer", "testcafe": "TestCafe", - "@testing-library/react": "Testing Library", - "@testing-library/vue": "Testing Library", - "@testing-library/dom": "Testing Library", - "eslint": "ESLint", "prettier": "Prettier", "@biomejs/biome": "Biome", - # Storybook ships across many scoped packages; the framework adapters below - # cover both apps that embed it and addons that declare it as a peer dep. - "storybook": "Storybook", "@storybook/react": "Storybook", - "@storybook/vue3": "Storybook", "@storybook/angular": "Storybook", - "@storybook/svelte": "Storybook", "@storybook/html": "Storybook", - "@storybook/web-components": "Storybook", "@storybook/preact": "Storybook", - # Monorepo / task runners. - "turbo": "Turborepo", "nx": "Nx", "@nx/workspace": "Nx", - # Transpilers. - "@swc/core": "SWC", "@babel/core": "Babel", - "redux": "Redux", "@reduxjs/toolkit": "Redux", "zustand": "Zustand", - "@apollo/client": "Apollo", "graphql": "GraphQL", - "@trpc/server": "tRPC", "@trpc/client": "tRPC", - "prisma": "Prisma", "@prisma/client": "Prisma", - "drizzle-orm": "Drizzle", "typeorm": "TypeORM", - "mongoose": "Mongoose", "sequelize": "Sequelize", - "three": "three.js", "d3": "D3", "chart.js": "Chart.js", -} - -# Web/JS sentinel files: basename → framework (assigned to the JS/TS bucket). -CURATED_SENTINELS_JS = [ - ["next.config.js", "Next.js"], ["next.config.ts", "Next.js"], - ["next.config.mjs", "Next.js"], ["nuxt.config.js", "Nuxt"], - ["nuxt.config.ts", "Nuxt"], ["svelte.config.js", "Svelte"], - ["astro.config.mjs", "Astro"], ["vue.config.js", "Vue"], - ["gatsby-config.js", "Gatsby"], ["angular.json", "Angular"], -] - -# Backend frameworks Vercel/Netlify don't cover — keyed by language, then -# dependency name → display name. Matched as whole words in manifest text. -CURATED_BACKEND = { - "Python": { - "django": "Django", "djangorestframework": "Django REST", - "flask": "Flask", "fastapi": "FastAPI", "starlette": "Starlette", - "tornado": "Tornado", "aiohttp": "aiohttp", "sanic": "Sanic", - "pyramid": "Pyramid", "sqlalchemy": "SQLAlchemy", "pydantic": "Pydantic", - "celery": "Celery", "scrapy": "Scrapy", "numpy": "NumPy", - "pandas": "pandas", "scipy": "SciPy", "scikit-learn": "scikit-learn", - "tensorflow": "TensorFlow", "torch": "PyTorch", "keras": "Keras", - "transformers": "Transformers", "matplotlib": "Matplotlib", - "pytest": "pytest", "click": "Click", "typer": "Typer", - "requests": "Requests", "httpx": "HTTPX", - }, - "Ruby": { - "rails": "Rails", "sinatra": "Sinatra", "hanami": "Hanami", - "rspec": "RSpec", "sidekiq": "Sidekiq", "puma": "Puma", "devise": "Devise", - }, - "Go": { - "github.com/gin-gonic/gin": "Gin", "github.com/labstack/echo": "Echo", - "github.com/gofiber/fiber": "Fiber", "github.com/gorilla/mux": "Gorilla", - "gorm.io/gorm": "GORM", "github.com/spf13/cobra": "Cobra", - "github.com/go-chi/chi": "chi", "google.golang.org/grpc": "gRPC", - }, - "Rust": { - "actix-web": "Actix Web", "axum": "Axum", "rocket": "Rocket", - "warp": "warp", "tokio": "Tokio", "serde": "Serde", "diesel": "Diesel", - "tonic": "Tonic", "clap": "clap", "bevy": "Bevy", "tauri": "Tauri", - }, - "PHP": { - "laravel/framework": "Laravel", "symfony/symfony": "Symfony", - "symfony/framework-bundle": "Symfony", "slim/slim": "Slim", - "cakephp/cakephp": "CakePHP", "yiisoft/yii2": "Yii", - }, -} - -# Colors for synthetic framework groups Linguist doesn't define a language for. -# Purple keeps "Tools" distinct from the grey "Other" bucket on the same page. -SYNTHETIC_COLORS = {"Tools": "#a371f7"} - -# Backend / non-JS sentinel files: basename (or sub-path) → (framework, language). -# The "Tools" bucket surfaces build/devops tooling that's present as a config -# file rather than a dependency — it'd otherwise hide in the long tail of the -# language bar (Dockerfile/Makefile are tiny by line count). -CURATED_SENTINELS = [ - ["manage.py", "Django", "Python"], - ["artisan", "Laravel", "PHP"], - ["config/application.rb", "Rails", "Ruby"], - ["Dockerfile", "Docker", "Tools"], - ["docker-compose.yml", "Docker Compose", "Tools"], - ["docker-compose.yaml", "Docker Compose", "Tools"], - ["compose.yml", "Docker Compose", "Tools"], - ["compose.yaml", "Docker Compose", "Tools"], - ["Makefile", "Make", "Tools"], - ["GNUmakefile", "Make", "Tools"], - ["pnpm-lock.yaml", "pnpm", "Tools"], - ["yarn.lock", "Yarn", "Tools"], - ["bun.lockb", "Bun", "Tools"], - ["bun.lock", "Bun", "Tools"], - [".gitlab-ci.yml", "GitLab CI", "Tools"], - ["vercel.json", "Vercel", "Tools"], - ["netlify.toml", "Netlify", "Tools"], - # Trailing slash → directory-prefix match (no single file to key on). - [".github/workflows/", "GitHub Actions", "Tools"], -] - - -def fetch(url): - req = urllib.request.Request(url, headers={"User-Agent": "repo-intel-gen"}) - with urllib.request.urlopen(req, timeout=10) as resp: - return resp.read().decode("utf-8") - - -def parse_languages_yml(text): - """Line-parse Linguist languages.yml (machine-generated, regular). - - Returns {name: {"type", "color", "extensions": [...], "filenames": [...]}}. - """ - langs = {} - cur = None - listkey = None - for raw in text.splitlines(): - if not raw or raw.lstrip().startswith("#"): - continue - if not raw[0].isspace(): # column-0 language header - m = re.match(r'^(?:"([^"]+)"|\'([^\']+)\'|([^:]+)):\s*$', raw) - if m: - name = m.group(1) or m.group(2) or m.group(3) - cur = {"type": "", "color": "", "group": "", - "extensions": [], "filenames": []} - langs[name] = cur - listkey = None - else: - cur = None - continue - if cur is None: - continue - item = re.match(r'^ - (.*)$', raw) - if item and listkey: - val = item.group(1).strip().strip('"').strip("'") - cur[listkey].append(val) - continue - prop = re.match(r'^ (\w+):\s*(.*)$', raw) - if prop: - key, val = prop.group(1), prop.group(2).strip() - if key in ("extensions", "filenames") and val == "": - listkey = key - else: - listkey = None - if key == "color": - cur["color"] = val.strip('"').strip("'") - elif key == "type": - cur["type"] = val - elif key == "group": - cur["group"] = val.strip('"').strip("'") - return langs - - -def build_language_tables(langs): - """ext/filename → language name and name → color, colored languages only. - - Fine-grained languages are folded into their `group` (TSX→TypeScript) so the - bar doesn't fragment; ambiguous extensions are pinned via EXT_OVERRIDE. - """ - name_color = {n: i["color"] for n, i in langs.items() if i.get("color")} - ext_lang, ext_meta, filename_lang = {}, {}, {} - for name, info in langs.items(): - if not info.get("color"): - continue - # Roll fine-grained langs into their parent, but only when that parent - # is itself a colored language — otherwise a group like "Checksums" - # would seed color-less entries. - group = info.get("group") - eff = group if group and group in name_color else name - rank = TYPE_RANK.get(info.get("type", ""), 4) - for idx, ext in enumerate(info.get("extensions", [])): - key = ext[1:].lower() if ext.startswith(".") else ext.lower() - if not key: - continue - primary = idx == 0 - if key not in ext_lang: - ext_lang[key] = eff - ext_meta[key] = (rank, primary) - else: - prank, pprimary = ext_meta[key] - # Prefer better type rank; then a primary extension over secondary. - if rank < prank or (rank == prank and primary and not pprimary): - ext_lang[key] = eff - ext_meta[key] = (rank, primary) - for fn in info.get("filenames", []): - filename_lang.setdefault(fn.lower(), eff) - for ext, lang in EXT_OVERRIDE.items(): - if lang in name_color: - ext_lang[ext] = lang - for ext in EXT_EXCLUDE: - ext_lang.pop(ext, None) - name_color.update(SYNTHETIC_COLORS) # synthetic buckets Linguist doesn't color - return name_color, ext_lang, filename_lang - - -def parse_vendor_yml(text): - out = [] - for line in text.splitlines(): - m = re.match(r'^- (.*)$', line) - if m: - out.append(m.group(1).strip()) - return out - - -def main(): - print("fetching Linguist languages.yml…", file=sys.stderr) - langs = parse_languages_yml(fetch(LANGUAGES_YML)) - name_color, ext_lang, filename_lang = build_language_tables(langs) - print(f" {len(name_color)} colored languages, {len(ext_lang)} extensions", - file=sys.stderr) - - print("fetching Linguist vendor.yml…", file=sys.stderr) - vendor = parse_vendor_yml(fetch(VENDOR_YML)) - print(f" {len(vendor)} vendor patterns", file=sys.stderr) - - fw_deps = {"npm": CURATED_WEB} - fw_deps.update(CURATED_BACKEND) - - data = { - "_source": {"languages": LANGUAGES_YML, "vendor": VENDOR_YML}, - "lang": {"ext": ext_lang, "filename": filename_lang, "color": name_color}, - "vendor": vendor, - "fw_deps": fw_deps, - "fw_sentinels_js": CURATED_SENTINELS_JS, - "fw_sentinels_other": CURATED_SENTINELS, - } - OUT.write_text(json.dumps(data, ensure_ascii=False, indent=1, sort_keys=True)) - print(f"wrote {OUT} ({OUT.stat().st_size:,} bytes)", file=sys.stderr) - - -if __name__ == "__main__": - main() diff --git a/src/repo-intel/repo-intel.py b/src/repo-intel/repo-intel.py deleted file mode 100755 index 850d825..0000000 --- a/src/repo-intel/repo-intel.py +++ /dev/null @@ -1,1982 +0,0 @@ -#!/usr/bin/env python3 -"""repo-intel — generate a contributor stats dashboard for a git repo.""" - -HELP = """\ -repo-intel — generate a contributor stats dashboard for a git repo. - -Usage: - repo-intel [N] [REPO] [-o PATH] [--no-open] [--clone] - repo-intel -h | --help - -Arguments: - N Number of top contributors to include (default: 10) - REPO A GitHub repository, in any of these forms: - owner/repo - https://github.com/owner/repo - remote:owner/repo - When omitted, the current working directory is used as a local git repo. - -Options: - -o, --output PATH Write the dashboard to PATH instead of /tmp/--.html. - --no-open Don't open the result in a browser. - --no-cache Ignore the local cache and re-fetch all commits. - --clone For a remote REPO, analyse a bare `git clone` instead of - the GitHub GraphQL API (alias: --bare). Slower to fetch - but unlocks per-author language churn the API can't give. - --commits SPEC Filter commits by position. SPEC is either N (last N - commits, newest) or A-B (positions [A, B), 0-indexed - from oldest, half-open like Python slicing). - --since DATE Only include commits on or after DATE (YYYY-MM-DD, inclusive). - --until DATE Only include commits on or before DATE (YYYY-MM-DD, inclusive). - -h, --help Show this help message and exit. - -Examples: - repo-intel # local repo (cwd), top 10 - repo-intel 20 # local repo, top 20 - repo-intel facebook/react # remote, top 10 - repo-intel 15 facebook/react # remote, top 15 - repo-intel -o ./stats.html # write to a specific path - repo-intel --no-open # generate without launching browser - repo-intel facebook/react --clone # analyse via bare clone, not the API - repo-intel --commits 100 # only the last 100 commits - repo-intel --commits 0-100 # the first 100 commits - repo-intel --commits 400-800 # commits at positions 400..799 (oldest-first) - repo-intel --since 2024-01-01 # commits since 2024-01-01 - repo-intel --since 2024-01-01 --until 2024-06-30 # H1 2024 - -Remote auth: - The GitHub CLI (`gh`, https://cli.github.com/) is optional but - recommended — when authenticated it unlocks GraphQL remote fetching - and author hovercard enrichment (avatar, bio, follower counts). - Lookup order: `gh auth token -h github.com`, then $GITHUB_TOKEN. - Falls back to `git clone --bare` into /tmp if neither is available; - pass --clone to force that bare-clone path even when a token is present. - -Output: - /tmp/--.html (or --output PATH), opened in default browser - unless --no-open is given. Falls back to /tmp/.html for local - repos without a github.com origin. - -Cache: - Remote commit nodes are cached under - $XDG_CACHE_HOME/repo-intel (default ~/.cache/repo-intel) as one JSON - file per repo. Re-runs only fetch new commits. -""" - -import hashlib -import json -import os -import re -import subprocess -import sys -import time -import urllib.request -import webbrowser -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from pathlib import Path - -TEMPLATE = "__TEMPLATE_PLACEHOLDER__" -PLACEHOLDER = "/*__DATA_INJECTION__*/" -NOREPLY_RE = re.compile(r"(?:\d+\+)?(.+)@users\.noreply\.github\.com") -ORIGIN_RE = re.compile( - r"^(?:https?://(?P[^/]+)/|git@(?P[^:]+):)" - r"(?P[^/]+)/(?P.+?)(?:\.git)?/?$" -) -CACHE_DIR = ( - Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "repo-intel" -) - - -def parse_iso_instant(s): - """Parse an ISO 8601 timestamp to a UTC-aware datetime; epoch on failure. - - Tags mix `Z`-suffixed UTC (GraphQL) with offset-suffixed local time - (`git for-each-ref iso8601-strict`), so lex-sorting can misorder them. - """ - if not s: - return datetime(1970, 1, 1, tzinfo=timezone.utc) - try: - dt = datetime.fromisoformat(s.replace("Z", "+00:00")) - except ValueError: - return datetime(1970, 1, 1, tzinfo=timezone.utc) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.astimezone(timezone.utc) - - -def _slugify(s): - return re.sub(r"[^\w.-]+", "-", s).strip("-") - - -def cache_path(slug): - safe = _slugify(slug.lower()) or "repo" - return CACHE_DIR / f"{safe}.json" - - -def load_cache(slug): - p = cache_path(slug) - if not p.exists(): - return [], False - try: - data = json.loads(p.read_text()) - return data.get("nodes", []), bool(data.get("complete", False)) - except (json.JSONDecodeError, OSError): - return [], False - - -def save_cache(slug, nodes, complete): - CACHE_DIR.mkdir(parents=True, exist_ok=True) - cache_path(slug).write_text(json.dumps({"nodes": nodes, "complete": complete})) - - -def needs_older_fetch(have_count, cached_oldest_date, prev_complete, - commits_filter, since, until): - """Should we paginate below the oldest cached commit after top-fetch? - - have_count: len(new_nodes) + len(cached_nodes) after the top-fetch. - cached_oldest_date: YYYY-MM-DD of the oldest cached commit, "" if empty. - """ - if prev_complete: - return False - if not cached_oldest_date: - return False - if until: - return True - if commits_filter: - if commits_filter[0] == "last": - return have_count < commits_filter[1] - return True # range — slice is anchored at oldest, must walk full history - if since: - return cached_oldest_date > since - return True - - -def parse_commits_spec(val): - if re.fullmatch(r"\d+", val): - n = int(val) - if n <= 0: - raise ValueError("--commits N requires a positive integer") - return ("last", n) - m = re.fullmatch(r"(\d+)-(\d+)", val) - if m: - a, b = int(m.group(1)), int(m.group(2)) - if a >= b: - raise ValueError(f"--commits A-B requires A < B (got {a}-{b})") - return ("range", a, b) - raise ValueError(f"--commits must be N or A-B (got {val!r})") - - -def parse_date(val, flag): - if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", val): - raise ValueError(f"{flag} requires YYYY-MM-DD (got {val!r})") - return val - - -def parse_args(argv): - if any(tok in ("-h", "--help") for tok in argv): - sys.stdout.write(HELP) - sys.exit(0) - top_n, remote, output, no_open, no_cache = 10, None, None, False, False - clone = False - commits_filter, since, until = None, None, None - i = 0 - - def take_value(name): - tok = argv[i] - if tok == name: - if i + 1 >= len(argv): - sys.stderr.write(f"repo-intel: {name} requires a value\n") - sys.exit(2) - return argv[i + 1], 2 - if tok.startswith(name + "="): - return tok[len(name) + 1:], 1 - return None, 0 - - while i < len(argv): - tok = argv[i] - try: - if tok == "--no-open": - no_open = True - i += 1 - continue - if tok == "--no-cache": - no_cache = True - i += 1 - continue - if tok in ("--clone", "--bare"): - clone = True - i += 1 - continue - if tok == "-o": - if i + 1 >= len(argv): - sys.stderr.write("repo-intel: -o requires a value\n") - sys.exit(2) - output = argv[i + 1] - i += 2 - continue - val, step = take_value("--output") - if step: - output = val - i += step - continue - val, step = take_value("--commits") - if step: - commits_filter = parse_commits_spec(val) - i += step - continue - val, step = take_value("--since") - if step: - since = parse_date(val, "--since") - i += step - continue - val, step = take_value("--until") - if step: - until = parse_date(val, "--until") - i += step - continue - except ValueError as exc: - sys.stderr.write(f"repo-intel: {exc}\n") - sys.exit(2) - if tok.isdigit(): - n = int(tok) - if n <= 0: - sys.stderr.write(f"repo-intel: N must be a positive integer (got {tok!r})\n") - sys.exit(2) - top_n = n - i += 1 - continue - t = tok.removeprefix("remote:") - t = t.removeprefix("https://github.com/").removeprefix("http://github.com/") - parts = t.rstrip("/").split("/") - if ( - len(parts) >= 2 - and re.fullmatch(r"[\w.-]+", parts[0]) - and re.fullmatch(r"[\w.-]+", parts[1]) - ): - remote = f"{parts[0]}/{parts[1]}" - i += 1 - continue - sys.stderr.write(f"repo-intel: unrecognized argument: {tok!r}\n") - sys.stderr.write("Try 'repo-intel --help' for usage.\n") - sys.exit(2) - if since and until and since > until: - sys.stderr.write(f"repo-intel: --since {since} is after --until {until}\n") - sys.exit(2) - return top_n, remote, output, no_open, no_cache, clone, commits_filter, since, until - - -def login_from_email(email): - m = NOREPLY_RE.fullmatch(email or "") - return m.group(1) if m else "" - - -def avatar_url(email, override=None): - if override: - return override - login = login_from_email(email) - if login: - return f"https://github.com/{login}.png?size=64" - h = hashlib.md5(email.strip().lower().encode()).hexdigest() - return f"https://www.gravatar.com/avatar/{h}?d=mp&s=64" - - -def iso_week_label(dt): - y, w, _ = dt.isocalendar() - return f"{y}-W{w:02d}" - - -# Language + framework detection data, generated from GitHub Linguist and a -# curated framework map by gen_techdata.py (see `make repo-intel-techdata`). -# build.py inlines the JSON here; when unbuilt we read the sibling file. Used -# only on the local + bare-clone paths — the GraphQL remote path lacks per-file -# data, so these maps go unused there. -TECHDATA = "__TECHDATA_PLACEHOLDER__" -OTHER_LANG = "Other" -OTHER_COLOR = "#8b949e" - - -def _load_techdata(): - raw = TECHDATA - if raw == "__TECHDATA_PLACEHOLDER__": - sibling = Path(__file__).resolve().parent / "techdata.json" - if not sibling.exists(): - return {} - try: - return json.loads(sibling.read_text()) - except (json.JSONDecodeError, OSError): - return {} - try: - return json.loads(raw) - except (json.JSONDecodeError, ValueError): - return {} - - -_TECH = _load_techdata() -_LANG = _TECH.get("lang", {}) -EXT_LANG = _LANG.get("ext", {}) # extension (no dot, lower) -> language -FILENAME_LANG = _LANG.get("filename", {}) # lowercased filename -> language -NAME_COLOR = _LANG.get("color", {}) # language -> hex color -FW_DEPS = _TECH.get("fw_deps", {}) # {ecosystem: {dependency: framework}} -FW_SENTINELS_JS = _TECH.get("fw_sentinels_js", []) # [[basename, framework]] -FW_SENTINELS_OTHER = _TECH.get("fw_sentinels_other", []) # [[path, framework, lang]] - - -def _compile_vendor(patterns): - """One matcher from Linguist's vendor.yml regexes; skips Python-incompatible - ones (they're Ruby-flavored) so the union still compiles.""" - good = [] - for p in patterns: - try: - re.compile(p) - good.append(p) - except re.error: - continue - try: - return re.compile("|".join(f"(?:{p})" for p in good)) if good else None - except re.error: - return None - - -_VENDOR_RE = _compile_vendor(_TECH.get("vendor", [])) - -# Lockfiles Linguist classifies as *generated* (handled in code, not vendor.yml) -# — kept as a small supplement so they don't dominate the language bar. -NOISE_BASENAMES = frozenset({ - "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json", - "composer.lock", "cargo.lock", "gemfile.lock", "poetry.lock", "go.sum", - "pdm.lock", "uv.lock", "flake.lock", -}) - -# Shebang interpreter → language, for extensionless scripts Linguist can't name -# from a path alone (e.g. `bin/deploy` with `#!/usr/bin/env bash`). A small -# curated map mirroring Linguist's `interpreters:`; trailing version digits are -# stripped (`python3` → `python`) before lookup. Names must be real Linguist -# languages so they pick up a color. -SHEBANG_LANG = { - "sh": "Shell", "bash": "Shell", "zsh": "Shell", "dash": "Shell", - "ksh": "Shell", "fish": "fish", "python": "Python", "ruby": "Ruby", - "node": "JavaScript", "perl": "Perl", "awk": "Awk", "gawk": "Awk", - "lua": "Lua", "php": "PHP", "rscript": "R", "tclsh": "Tcl", - "groovy": "Groovy", "osascript": "AppleScript", -} - - -def shebang_lang(first_line): - """Language for a `#!…` first line, or None. Resolves `env interp` and pins - `python3`→Python by stripping trailing version digits from the interpreter.""" - if not first_line.startswith("#!"): - return None - interp = None - for tok in first_line[2:].split(): - name = tok.rsplit("/", 1)[-1] - if name != "env": # skip the `env` in `#!/usr/bin/env python3` - interp = name - break - if not interp: - return None - interp = interp.lower() - return SHEBANG_LANG.get(interp) or SHEBANG_LANG.get(interp.rstrip("0123456789")) - - -def numstat_newpath(field): - """Resolve a numstat path column to the post-rename path. - - Renames render as `old => new`, or with a shared brace group like - `src/{old => new}/file.js`; plain paths pass through unchanged. - """ - if " => " not in field: - return field - lo = field.find("{") - hi = field.find("}", lo) if lo != -1 else -1 - if lo != -1 and hi != -1 and " => " in field[lo:hi]: - new = field[lo + 1:hi].split(" => ", 1)[1] - return field[:lo] + new + field[hi + 1:] - return field.split(" => ", 1)[1] - - -def classify_path(field, present=None, shebang=None): - """Map a numstat path column to a language name, or None to exclude it. - - `present`: when given, the set of paths at HEAD — files absent from it - (deleted since, or renamed away) are excluded so the bar reflects the repo - as it stands, not churn against files that no longer exist. - `shebang`: {path: language} for extensionless/unknown scripts a `#!` line - identified, so they land in their real language instead of "Other". - """ - path = numstat_newpath(field.strip().strip('"')).replace("\\", "/") - if present is not None and path not in present: - return None # file no longer exists at HEAD — count only survivors - if _VENDOR_RE and _VENDOR_RE.search(path): # Linguist vendored paths - return None - base = path.rsplit("/", 1)[-1].lower() - if base in NOISE_BASENAMES: - return None - if base.endswith((".min.js", ".min.css", ".map")): - return None - if base in FILENAME_LANG: # Dockerfile, Makefile, Rakefile, … - return FILENAME_LANG[base] - dot = base.rfind(".") - if dot > 0: - lang = EXT_LANG.get(base[dot + 1:]) - if lang: - return lang - if shebang and path in shebang: # extensionless/unknown but has a #! line - return shebang[path] - return OTHER_LANG - - -def top_languages(langs, limit=6): - """Build a sorted language-bar list from {name: [added, deleted, files]}. - - Ranks by lines touched (added + deleted); languages past `limit` collapse - into a single grey "Other" segment. Returns [] when nothing qualifies. - """ - items = [(name, a + d, files) for name, (a, d, files) in langs.items()] - total = sum(lines for _, lines, _ in items) - if total <= 0: - return [] - items.sort(key=lambda x: x[1], reverse=True) - out = [ - { - "name": name, - "lines": lines, - "files": files, - "pct": round(lines * 100 / total, 1), - "color": NAME_COLOR.get(name, OTHER_COLOR), - } - for name, lines, files in items[:limit] - ] - overflow = sum(lines for _, lines, _ in items[limit:]) - if overflow > 0: - existing = next((o for o in out if o["name"] == OTHER_LANG), None) - if existing: - existing["lines"] += overflow - existing["pct"] = round(existing["lines"] * 100 / total, 1) - else: - out.append({ - "name": OTHER_LANG, - "lines": overflow, - "files": 0, - "pct": round(overflow * 100 / total, 1), - "color": OTHER_COLOR, - }) - return out - - -def git(*args, cwd=None, quiet=False): - # quiet=True hides git's stderr — for best-effort probes that are expected - # to fail (e.g. work-tree-only commands run against a bare clone). - return subprocess.check_output( - ["git", *args], - text=True, - cwd=cwd, - stderr=subprocess.DEVNULL if quiet else None, - ) - - -def _git_show(path, cwd=None): - """Contents of `path` at HEAD, or "" if missing. Works on bare clones.""" - try: - return git("show", f"HEAD:{path}", cwd=cwd) - except subprocess.CalledProcessError: - return "" - - -def _head_first_line(path, cwd=None): - """First line of `path` at HEAD, decoded leniently, or "". Reads bytes so a - stray binary doesn't crash the utf-8 decode `git(text=True)` would attempt.""" - try: - out = subprocess.run( - ["git", "show", f"HEAD:{path}"], cwd=cwd, capture_output=True - ).stdout - except OSError: - return "" - nl = out.find(b"\n") - return (out if nl < 0 else out[:nl]).decode("utf-8", "replace") - - -def detect_frameworks(paths, cwd=None): - """Detect frameworks at HEAD from a local repo / bare clone. - - `paths`: the HEAD tree (repo-relative), already listed by the caller. - Returns a list grouped by language, ordered by framework count: - [{"language": "TypeScript", "color": "#3178c6", "names": [...]}, ...] - Best-effort and local-only — the GraphQL remote path skips this. - """ - return _frameworks_from_files(paths, lambda p: _git_show(p, cwd)) - - -def _frameworks_from_files(paths, read_file): - """Core framework detection over a file list, driven by techdata maps. - - `paths`: repo-relative paths that exist. `read_file(path)` -> contents - ("" if unavailable; only called for manifests worth parsing). Decoupled - from git so the remote path can supply GraphQL-fetched blobs. - """ - if not FW_DEPS: - return [] - paths = set(paths) - by_base = defaultdict(list) - for p in paths: - by_base[p.rsplit("/", 1)[-1].lower()].append(p) - - found = defaultdict(list) - seen = defaultdict(set) - - def add(language, name): - if name and name not in seen[language]: - seen[language].add(name) - found[language].append(name) - - def present(dep, text): - return re.search(r"(?= 3: - field = cols[2] - if field in lang_cache: - lang = lang_cache[field] - else: - lang = classify_path(field, present=present, shebang=shebang) - lang_cache[field] = lang - if lang: - rec = lang_stats.setdefault(cur, {}).setdefault(lang, [0, 0, 0]) - rec[0] += added - rec[1] += deleted - rec[2] += 1 - - default_branch = detect_default_branch(cwd=cwd) - extras = {"lang_stats": lang_stats, "frameworks": detect_frameworks(present, cwd=cwd)} - return ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - {}, - {}, - default_branch, - repo_disk_kb(cwd=cwd), - collect_local_tags(cwd=cwd), - extras, - ) - - -def gh_graphql(query, variables, token): - """POST a GraphQL query to api.github.com. Returns the parsed JSON body.""" - payload = json.dumps({"query": query, "variables": variables}).encode() - req = urllib.request.Request( - "https://api.github.com/graphql", - data=payload, - headers={ - "Authorization": f"bearer {token}", - "Content-Type": "application/json", - "User-Agent": "repo-intel", - }, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - - -# GitHub returns these transient statuses when its GraphQL backend is -# overloaded or times out; they're worth retrying. -RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504}) - -# Plan for a single Commit.history page: (page_size, seconds_to_wait_first). -# Resolving Commit.history makes GitHub compute per-commit diff stats -# (additions/deletions), so a page holding a few large commits can blow past -# its backend timeout and return 502 — deterministically, at the same cursor. -# Shrinking `first` cuts the per-request work; the backoff rides out flakiness. -HISTORY_FETCH_PLAN = ( - (100, 0), - (100, 2), - (25, 4), - (25, 8), - (10, 15), -) - - -def fetch_history_page(query, variables, token, label): - """gh_graphql for a Commit.history page, retrying transient 5xx with - backoff and a shrinking page size. Raises the last error if all attempts - fail. `variables` must omit `pageSize` — it is injected per attempt.""" - last_exc = None - for page_size, sleep_s in HISTORY_FETCH_PLAN: - if sleep_s: - time.sleep(sleep_s) - try: - return gh_graphql(query, {**variables, "pageSize": page_size}, token) - except urllib.error.HTTPError as exc: - if exc.code not in RETRYABLE_STATUS: - raise - last_exc, detail = exc, f"HTTP {exc.code}" - except urllib.error.URLError as exc: - last_exc, detail = exc, str(exc.reason) - print( - f" warning: {label} page (size {page_size}) failed: {detail}", - file=sys.stderr, - ) - raise last_exc - - -def gh_repository(body): - """Extract data.repository defensively — GraphQL returns null on errors.""" - return (body.get("data") or {}).get("repository") or {} - - -def probe_remote_total(owner, repo, token): - """Total commits on the default branch via GraphQL; None on error.""" - query = """ -query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - defaultBranchRef { - target { ... on Commit { history(first: 1) { totalCount } } } - } - } -} -""".strip() - try: - body = gh_graphql(query, {"owner": owner, "repo": repo}, token) - except urllib.error.URLError: - return None - if "errors" in body: - return None - repo_node = gh_repository(body) - branch = repo_node.get("defaultBranchRef") or {} - target = branch.get("target") or {} - history = target.get("history") or {} - total = history.get("totalCount") - return total if isinstance(total, int) else None - - -def get_github_token(): - try: - token = subprocess.check_output( - ["gh", "auth", "token", "-h", "github.com"], - text=True, - stderr=subprocess.DEVNULL, - ).strip() - except (subprocess.CalledProcessError, FileNotFoundError): - token = "" - return token or os.environ.get("GITHUB_TOKEN") or None - - -def fetch_logins_for_commits(owner, repo, oids_by_email, token): - """Look up GitHub author login for each email using a few sample oids. - - Used in the local-repo path where the commit walk doesn't know logins. - Local commits not yet pushed return null, so multiple oids per email are - queried in one batched GraphQL call; the first that resolves wins. - Returns {email: login}. - """ - if not oids_by_email or not token: - return {} - aliases = [] - oid_values = [] - for email, oids in oids_by_email.items(): - for oid in oids: - aliases.append((len(oid_values), email)) - oid_values.append(oid) - if not aliases: - return {} - var_decls = ", ".join(f"$oid{i}: GitObjectID!" for i in range(len(oid_values))) - fragments = " ".join( - f"c{i}: object(oid: $oid{i}) {{ ... on Commit {{ author {{ user {{ login }} }} }} }}" - for i in range(len(oid_values)) - ) - query = ( - f"query($owner: String!, $repo: String!, {var_decls}) " - f"{{ repository(owner: $owner, name: $repo) {{ {fragments} }} }}" - ) - variables = {"owner": owner, "repo": repo} - for i, oid in enumerate(oid_values): - variables[f"oid{i}"] = oid - - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: login lookup failed: {exc}", file=sys.stderr) - return {} - if "errors" in body: - print(f" warning: login lookup errors: {body['errors']}", file=sys.stderr) - repo_node = gh_repository(body) - out = {} - for i, email in aliases: - if email in out: - continue - node = repo_node.get(f"c{i}") or {} - user = ((node.get("author") or {}).get("user")) or {} - login = user.get("login") - if login: - out[email] = login - return out - - -def fetch_user_profiles(logins, token): - """Fetch GitHub profile fields for `logins` in one aliased GraphQL query. - - Returns {login: {login,name,bio,location,websiteUrl,followers,following,publicRepos}}. - Missing/renamed users are silently skipped. - """ - if not logins or not token: - return {} - unique = [] - seen = set() - for login in logins: - if login and login not in seen: - seen.add(login) - unique.append(login) - if not unique: - return {} - - fields = ( - "login name bio location websiteUrl " - "followers { totalCount } following { totalCount } " - "repositories(privacy: PUBLIC, ownerAffiliations: OWNER) { totalCount }" - ) - var_decls = ", ".join(f"$l{i}: String!" for i in range(len(unique))) - fragments = " ".join( - f"u{i}: user(login: $l{i}) {{ {fields} }}" for i in range(len(unique)) - ) - query = f"query({var_decls}) {{ {fragments} }}" - variables = {f"l{i}": login for i, login in enumerate(unique)} - - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: profile fetch failed: {exc}", file=sys.stderr) - return {} - if "errors" in body: - print(f" warning: profile fetch errors: {body['errors']}", file=sys.stderr) - data = body.get("data") or {} - out = {} - for i, login in enumerate(unique): - node = data.get(f"u{i}") - if not node: - continue - out[login] = { - "login": node.get("login") or login, - "name": node.get("name") or "", - "bio": node.get("bio") or "", - "location": node.get("location") or "", - "websiteUrl": node.get("websiteUrl") or "", - "followers": (node.get("followers") or {}).get("totalCount") or 0, - "following": (node.get("following") or {}).get("totalCount") or 0, - "publicRepos": (node.get("repositories") or {}).get("totalCount") or 0, - } - return out - - -def fetch_remote_tags(owner, repo, token): - """Fetch all tag refs via GraphQL. Returns list of {name, oid, date, message}.""" - query = """ -query($owner: String!, $repo: String!, $cursor: String) { - repository(owner: $owner, name: $repo) { - refs(refPrefix: "refs/tags/", first: 100, after: $cursor, orderBy: {field: TAG_COMMIT_DATE, direction: ASC}) { - pageInfo { hasNextPage endCursor } - nodes { - name - target { - __typename - ... on Tag { - message - target { ... on Commit { oid committedDate } } - } - ... on Commit { oid committedDate } - } - } - } - } -} -""".strip() - cursor = None - tags = [] - while True: - try: - body = gh_graphql(query, {"owner": owner, "repo": repo, "cursor": cursor}, token) - except urllib.error.URLError as exc: - print(f" warning: tag fetch failed: {exc}", file=sys.stderr) - return tags - if "errors" in body: - print(f" warning: tag fetch GraphQL error: {body['errors']}", file=sys.stderr) - return tags - refs = gh_repository(body).get("refs") or {} - for node in refs.get("nodes") or []: - tgt = node.get("target") or {} - kind = tgt.get("__typename") - if kind == "Tag": - inner = tgt.get("target") or {} - oid = inner.get("oid") or "" - date = inner.get("committedDate") or "" - message = tgt.get("message") or "" - elif kind == "Commit": - oid = tgt.get("oid") or "" - date = tgt.get("committedDate") or "" - message = "" - else: - continue - if not oid or not date: - continue - tags.append({ - "name": node.get("name") or "", - "oid": oid, - "date": date, - "message": (message.splitlines() or [""])[0], - }) - page = refs.get("pageInfo") or {} - if not page.get("hasNextPage"): - break - cursor = page.get("endCursor") - tags.sort(key=lambda t: parse_iso_instant(t.get("date"))) - return tags - - -def gh_rest_get(path, token): - """GET an api.github.com REST endpoint; returns the parsed JSON body.""" - req = urllib.request.Request( - f"https://api.github.com{path}", - headers={ - "Authorization": f"bearer {token}", - "User-Agent": "repo-intel", - "Accept": "application/vnd.github+json", - }, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - - -# Manifests _frameworks_from_files actually parses (so we only fetch those -# blobs). tsconfig.json / sentinels are presence-only — covered by the tree. -_REMOTE_MANIFEST_BASES = frozenset({ - "package.json", "composer.json", "pyproject.toml", "pipfile", - "setup.py", "setup.cfg", "gemfile", "go.mod", "cargo.toml", -}) - - -def _remote_manifest_paths(paths): - out = [] - for p in paths: - base = p.rsplit("/", 1)[-1].lower() - if base in _REMOTE_MANIFEST_BASES or ( - base.startswith("requirements") and base.endswith(".txt") - ): - out.append(p) - return out - - -def fetch_blob_texts(owner, repo, paths, token): - """HEAD blob text for each path via aliased GraphQL. Returns {path: text}.""" - out = {} - paths = list(paths) - for start in range(0, len(paths), 50): - chunk = paths[start:start + 50] - var_decls = ", ".join(f"$p{i}: String!" for i in range(len(chunk))) - frags = " ".join( - f"b{i}: object(expression: $p{i}) {{ ... on Blob {{ text }} }}" - for i in range(len(chunk)) - ) - query = ( - f"query($owner: String!, $repo: String!, {var_decls}) " - f"{{ repository(owner: $owner, name: $repo) {{ {frags} }} }}" - ) - variables = {"owner": owner, "repo": repo} - for i, p in enumerate(chunk): - variables[f"p{i}"] = f"HEAD:{p}" - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: manifest fetch failed: {exc}", file=sys.stderr) - continue - node = gh_repository(body) - for i, p in enumerate(chunk): - blob = node.get(f"b{i}") - if blob and blob.get("text") is not None: - out[p] = blob["text"] - return out - - -def fetch_frameworks_remote(owner, repo, token): - """Detect frameworks on the GraphQL path without a clone. - - Lists the repo tree (REST, recursive — manifests can be nested) and fetches - just the manifest blobs (GraphQL), then runs the shared detection core. - Per-file *languages* stay local-only (too expensive over the network), but - manifests are cheap, so frameworks work here too. - """ - if not token: - return [] - try: - tree = gh_rest_get(f"/repos/{owner}/{repo}/git/trees/HEAD?recursive=1", token) - except urllib.error.URLError as exc: - print(f" warning: framework tree fetch failed: {exc}", file=sys.stderr) - return [] - if tree.get("truncated"): - # GitHub caps the recursive tree at ~100k entries / 7MB; deep manifests - # past the cap are dropped, so detection may miss frameworks silently. - print( - " warning: repo tree truncated by GitHub — framework detection " - "may be incomplete", - file=sys.stderr, - ) - paths = [e["path"] for e in (tree.get("tree") or []) if e.get("type") == "blob"] - if not paths: - return [] - contents = fetch_blob_texts(owner, repo, _remote_manifest_paths(paths), token) - return _frameworks_from_files(paths, lambda p: contents.get(p, "")) - - -def fetch_languages_remote(owner, repo, token): - """Repo-wide language breakdown on the GraphQL path, no clone needed. - - GitHub runs Linguist itself and exposes the result as bytes-per-language at - HEAD. That's a composition snapshot, not the per-commit line churn the local - path tracks — so it can only fill the repo-wide bar, never per-author or - per-commit language stats. Reuses `top_languages` (ranking by the first - slot, here byte size) so colors and overflow collapsing match local runs. - Returns [] on error or when the repo has no detected languages. - """ - if not token: - return [] - query = """ -query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - languages(first: 50, orderBy: {field: SIZE, direction: DESC}) { - edges { size node { name } } - } - } -} -""".strip() - try: - body = gh_graphql(query, {"owner": owner, "repo": repo}, token) - except urllib.error.URLError as exc: - print(f" warning: language fetch failed: {exc}", file=sys.stderr) - return [] - if "errors" in body: - return [] - edges = ((gh_repository(body).get("languages") or {}).get("edges")) or [] - langs = {} - for e in edges: - name = ((e.get("node") or {}).get("name") or "").strip() - size = e.get("size") or 0 - if name and size > 0: - langs[name] = [size, 0, 0] - return top_languages(langs) - - -def _paginate_history(fetch_page, cached_oids, last_n, since, - have_count_baseline, label, skip_first=False): - """Walk a Commit.history connection page by page. - - fetch_page(cursor) -> history dict, or None when the anchor object is gone. - Returns (nodes, reason) where reason ∈ - "hit_cache" | "short_circuit" | "page_end" | "anchor_null" | "fetch_failed" - On "fetch_failed" the returned nodes are still a contiguous run from the - walk's start, so the caller can persist them and resume on a re-run. - """ - nodes = [] - cursor = None - dropped_anchor = not skip_first - while True: - try: - history = fetch_page(cursor) - except urllib.error.URLError as exc: - # A non-retryable HTTP status (401/403/404) is a hard failure, not - # a resumable one — propagate it rather than persisting a partial - # cache and telling the user to re-run. - if isinstance(exc, urllib.error.HTTPError) and exc.code not in RETRYABLE_STATUS: - raise - print(f" error: {label} fetch aborted: {exc}", file=sys.stderr) - return nodes, "fetch_failed" - if history is None: - return nodes, "anchor_null" - for n in history.get("nodes") or []: - if not dropped_anchor: - dropped_anchor = True - continue - if n["oid"] in cached_oids: - return nodes, "hit_cache" - nodes.append(n) - if last_n is not None and len(nodes) + have_count_baseline >= last_n: - return nodes, "short_circuit" - if since: - d = ((n.get("author") or {}).get("date") or "")[:10] - if d and d < since: - return nodes, "short_circuit" - page = history.get("pageInfo") or {} - if not page.get("hasNextPage"): - return nodes, "page_end" - cursor = page.get("endCursor") - print(f" fetched {len(nodes)} {label} commits…", file=sys.stderr) - - -def collect_remote(slug, token, no_cache=False, commits_filter=None, since=None, until=None): - owner, repo = slug.split("/", 1) - - if not token: - clone_dir = ensure_bare_clone(owner, repo, no_cache) - ( - repo_name, - github_base, - _, - commits_meta, - line_stats, - _, - _, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_local(cwd=clone_dir, suppress_current_user=True) - if not github_base: - github_base = f"https://github.com/{owner}/{repo}" - return ( - repo_name, - github_base, - "", - commits_meta, - line_stats, - {}, - {}, - default_branch, - repo_size_kb, - tags, - extras, - ) - - history_block = """ -history(first: $pageSize, after: $cursor) { - pageInfo { hasNextPage endCursor } - nodes { - oid messageHeadline - author { name email date user { avatarUrl(size: 64) login } } - additions deletions - } -}""".strip() - - top_query = f""" -query($owner: String!, $repo: String!, $cursor: String, $pageSize: Int!) {{ - repository(owner: $owner, name: $repo) {{ - name url diskUsage - defaultBranchRef {{ - name - target {{ ... on Commit {{ {history_block} }} }} - }} - }} -}}""".strip() - - bottom_query = f""" -query($owner: String!, $repo: String!, $oid: GitObjectID!, $cursor: String, $pageSize: Int!) {{ - repository(owner: $owner, name: $repo) {{ - object(oid: $oid) {{ ... on Commit {{ {history_block} }} }} - }} -}}""".strip() - - loaded_nodes, loaded_complete = ([], False) if no_cache else load_cache(slug) - cached_nodes = loaded_nodes - cached_oids = {n["oid"] for n in cached_nodes} - if cached_nodes: - label = "complete" if loaded_complete else "partial" - print(f" cache: {len(cached_nodes)} commits ({label})", file=sys.stderr) - - last_n = commits_filter[1] if commits_filter and commits_filter[0] == "last" else None - - repo_meta = { - "name": repo, - "url": f"https://github.com/{owner}/{repo}", - "branch": "main", - "disk_kb": 0, - } - - def top_fetch_page(cursor): - body = fetch_history_page( - top_query, {"owner": owner, "repo": repo, "cursor": cursor}, token, "new" - ) - if "errors" in body: - sys.exit(f"GraphQL error: {body['errors']}") - repo_node = gh_repository(body) - if not repo_node: - sys.exit(f"Repository not found or inaccessible: {slug}") - repo_meta["name"] = repo_node["name"] - repo_meta["url"] = repo_node["url"] - repo_meta["disk_kb"] = repo_node.get("diskUsage") or 0 - branch_ref = repo_node.get("defaultBranchRef") - if not branch_ref or not branch_ref.get("target"): - sys.exit(f"error: {slug} has no commits on its default branch") - repo_meta["branch"] = branch_ref.get("name") or repo_meta["branch"] - return branch_ref["target"]["history"] - - def bail_partial(nodes): - """Persist a contiguous partial run after a fetch failure, then exit so - the next run resumes from its tail. Saved as incomplete on purpose.""" - if not no_cache and nodes: - save_cache(slug, nodes, False) - print( - f" cached {len(nodes)} commits so far — re-run to resume", - file=sys.stderr, - ) - sys.exit("error: GitHub fetch failed after repeated retries; aborting.") - - new_nodes, top_reason = _paginate_history( - top_fetch_page, cached_oids, last_n, since, - have_count_baseline=len(cached_nodes), label="new", - ) - - if top_reason == "fetch_failed": - # new_nodes is a contiguous run from HEAD. We never reached the old - # cache, so merging would leave a gap — persist just the fresh prefix - # (the next run resumes its tail via the older-fetch) and bail out. - bail_partial(new_nodes) - - if top_reason == "page_end" and cached_oids: - print( - f" cache: orphaned by force-push/rewrite, discarded ({len(cached_nodes)} commits)", - file=sys.stderr, - ) - cached_nodes = [] - cached_oids = set() - loaded_complete = False - if new_nodes: - print(f" fetched {len(new_nodes)} new commits", file=sys.stderr) - - older_nodes = [] - bottom_reason = None - have_count = len(new_nodes) + len(cached_nodes) - cached_oldest_date = ( - ((cached_nodes[-1].get("author") or {}).get("date") or "")[:10] - if cached_nodes else "" - ) - if needs_older_fetch( - have_count, cached_oldest_date, loaded_complete, - commits_filter, since, until, - ): - anchor_oid = cached_nodes[-1]["oid"] - - def bottom_fetch_page(cursor): - body = fetch_history_page( - bottom_query, - {"owner": owner, "repo": repo, "oid": anchor_oid, "cursor": cursor}, - token, - "older", - ) - if "errors" in body: - sys.exit(f"GraphQL error: {body['errors']}") - obj = gh_repository(body).get("object") - if not obj: - return None - return obj.get("history") or {"nodes": [], "pageInfo": {}} - - older_nodes, bottom_reason = _paginate_history( - bottom_fetch_page, cached_oids, last_n, since, - have_count_baseline=have_count, label="older", skip_first=True, - ) - if bottom_reason == "anchor_null": - print( - " warning: cache anchor commit no longer exists; keeping what we have", - file=sys.stderr, - ) - elif older_nodes: - print(f" fetched {len(older_nodes)} older commits", file=sys.stderr) - - repo_name = repo_meta["name"] - repo_url = repo_meta["url"] - default_branch = repo_meta["branch"] - repo_size_kb = repo_meta["disk_kb"] - - nodes = new_nodes + cached_nodes + older_nodes - if bottom_reason == "fetch_failed": - # new + cached + older are contiguous, so the partial run is a valid - # prefix to persist; the next run extends from its tail. - bail_partial(nodes) - if bottom_reason is None: - new_complete = top_reason == "page_end" or loaded_complete - else: - new_complete = bottom_reason == "page_end" - if not no_cache and nodes: - save_cache(slug, nodes, new_complete) - - commits_meta, line_stats, avatars, logins = {}, {}, {}, {} - for n in nodes: - author = n.get("author") or {} - email = (author.get("email") or "").lower() - commits_meta[n["oid"]] = { - "subject": n.get("messageHeadline") or "", - "email": email, - "name": author.get("name") or email or "unknown", - "iso": author.get("date"), - } - line_stats[n["oid"]] = [n.get("additions") or 0, n.get("deletions") or 0] - user = author.get("user") - if user and email: - if user.get("avatarUrl") and email not in avatars: - avatars[email] = user["avatarUrl"] - if user.get("login") and email not in logins: - logins[email] = user["login"] - - tags = fetch_remote_tags(owner, repo, token) - # Per-commit/per-author language churn needs a clone, so `lang_stats` stays - # empty here. But the repo-wide bar and frameworks both come straight from - # the API: GitHub runs Linguist for `repo_languages`, and manifests are - # cheap to fetch for frameworks. - frameworks = fetch_frameworks_remote(owner, repo, token) - repo_languages = fetch_languages_remote(owner, repo, token) - return ( - repo_name, - repo_url, - "", - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - {"lang_stats": {}, "frameworks": frameworks, "repo_languages": repo_languages}, - ) - - -def apply_filters(commits_meta, line_stats, commits_filter, since, until): - if since or until: - def in_range(m): - d = (m.get("iso") or "")[:10] - return bool(d) and (not since or d >= since) and (not until or d <= until) - commits_meta = {h: m for h, m in commits_meta.items() if in_range(m)} - if commits_filter: - epoch = datetime(1970, 1, 1, tzinfo=timezone.utc) - def _ts(h): - iso = commits_meta[h].get("iso") or "" - if not iso: - return epoch - try: - return datetime.fromisoformat(iso.replace("Z", "+00:00")) - except ValueError: - return epoch - ordered = sorted(commits_meta, key=_ts) - if commits_filter[0] == "last": - keep = set(ordered[-commits_filter[1]:]) - else: - keep = set(ordered[commits_filter[1]:commits_filter[2]]) - commits_meta = {h: m for h, m in commits_meta.items() if h in keep} - line_stats = {h: line_stats[h] for h in commits_meta if h in line_stats} - return commits_meta, line_stats - - -def filter_tags_to_range(tags, commits_meta): - if not tags or not commits_meta: - return [] - dates = [(m.get("iso") or "")[:10] for m in commits_meta.values()] - dates = [d for d in dates if d] - if not dates: - return list(tags) - lo, hi = min(dates), max(dates) - return [t for t in tags if lo <= (t.get("date") or "")[:10] <= hi] - - -def build_data( - top_n, - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, -): - lang_stats = (extras or {}).get("lang_stats", {}) - frameworks = (extras or {}).get("frameworks", []) - # Remote runs ship a precomputed repo-wide bar (bytes at HEAD); local/bare - # runs build it below from per-commit line churn. `repo_languages` being a - # non-empty list signals the former. - repo_languages = (extras or {}).get("repo_languages") or [] - repo_langs = {} - authors = {} - daily_by_author = defaultdict(lambda: defaultdict(int)) - hourly_by_author = defaultdict(lambda: [0] * 24) - dow_by_author = defaultdict(lambda: [0] * 7) - weekly_by_author = defaultdict(lambda: defaultdict(int)) - all_dates, all_weeks = set(), set() - total_added = total_deleted = total_commits = 0 - - for h, meta in commits_meta.items(): - iso = meta.get("iso") - if not iso: - continue - try: - dt = datetime.fromisoformat(iso.replace("Z", "+00:00")) - except ValueError: - continue - total_commits += 1 - d_key = dt.strftime("%Y-%m-%d") - wk, hr, dow = iso_week_label(dt), dt.hour, dt.weekday() - email, name = meta["email"], meta["name"] - a, d = line_stats.get(h, [0, 0]) - total_added += a - total_deleted += d - - rec = authors.setdefault( - email, - { - "name": name, - "email": email, - "commits": 0, - "added": 0, - "deleted": 0, - "dates": set(), - "daily_counts": defaultdict(int), - "langs": {}, - "first": d_key, - "last": d_key, - }, - ) - rec["commits"] += 1 - rec["added"] += a - rec["deleted"] += d - rec["dates"].add(d_key) - rec["daily_counts"][d_key] += 1 - for lang, (la, ld, lf) in lang_stats.get(h, {}).items(): - agg = rec["langs"].setdefault(lang, [0, 0, 0]) - agg[0] += la - agg[1] += ld - agg[2] += lf - repo = repo_langs.setdefault(lang, [0, 0, 0]) - repo[0] += la - repo[1] += ld - repo[2] += lf - if d_key < rec["first"]: - rec["first"] = d_key - if d_key > rec["last"]: - rec["last"] = d_key - - daily_by_author[email][d_key] += 1 - hourly_by_author[email][hr] += 1 - dow_by_author[email][dow] += 1 - weekly_by_author[email][wk] += 1 - all_dates.add(d_key) - all_weeks.add(wk) - - total_contributors = len(authors) - ranked = sorted(authors.values(), key=lambda r: r["commits"], reverse=True) - top = ranked[:top_n] - top_emails = {r["email"] for r in top} - - contributors = [] - for r in top: - busiest_day, busiest_count = "", 0 - for k, v in r["daily_counts"].items(): - if v > busiest_count: - busiest_day, busiest_count = k, v - login = logins.get(r["email"]) or login_from_email(r["email"]) - contributors.append( - { - "name": r["name"], - "email": r["email"], - "login": login, - "commits": r["commits"], - "added": r["added"], - "deleted": r["deleted"], - "activeDays": len(r["dates"]), - "first": r["first"], - "last": r["last"], - "busiestDay": busiest_day, - "busiestCount": busiest_count, - "avatarUrl": avatar_url(r["email"], override=avatars.get(r["email"])), - "highlight": bool(current_email) and r["email"] == current_email, - "languages": top_languages(r["langs"]), - } - ) - - weeks_sorted = sorted(all_weeks) - weekly_data = { - r["email"]: [weekly_by_author[r["email"]].get(w, 0) for w in weeks_sorted] - for r in top - } - daily_data = {r["email"]: dict(daily_by_author[r["email"]]) for r in top} - hourly_data = {r["email"]: hourly_by_author[r["email"]] for r in top} - dow_data = {r["email"]: dow_by_author[r["email"]] for r in top} - - commits_list = [] - for h, meta in commits_meta.items(): - if meta["email"] not in top_emails: - continue - a, d = line_stats.get(h, [0, 0]) - entry = { - "h": h[:7], - "s": (meta["subject"] or "")[:120], - "e": meta["email"], - "d": meta.get("iso") or "", - "a": a, - "l": d, - } - cl = lang_stats.get(h) - if cl: - ftypes = sorted( - ([name, NAME_COLOR.get(name, OTHER_COLOR), files] - for name, (_, _, files) in cl.items()), - key=lambda x: x[2], reverse=True, - ) - entry["f"] = ftypes[:4] - commits_list.append(entry) - - date_range = ( - {"start": min(all_dates), "end": max(all_dates)} - if all_dates - else {"start": "", "end": ""} - ) - return { - "repoName": repo_name, - "githubBaseUrl": github_base, - "defaultBranch": default_branch, - "repoSizeKb": repo_size_kb, - "dateRange": date_range, - "totals": { - "commits": total_commits, - "added": total_added, - "deleted": total_deleted, - "contributors": total_contributors, - }, - "contributors": contributors, - "weeks": weeks_sorted, - "weeklyData": weekly_data, - "dailyData": daily_data, - "hourlyData": hourly_data, - "dowData": dow_data, - "commits": commits_list, - "tags": tags or [], - "repoLanguages": repo_languages or top_languages(repo_langs), - "repoLanguagesBasis": "size" if repo_languages else "churn", - "frameworks": frameworks or [], - } - - -def _sample_oids_per_email(commits_meta, target_emails, per_email=3): - """Up to `per_email` oldest oids per email. Unknown-date commits sort last.""" - by_email = defaultdict(list) - for h, meta in commits_meta.items(): - if meta["email"] in target_emails: - by_email[meta["email"]].append((meta.get("iso") or "", h)) - sample = {} - for email, items in by_email.items(): - items.sort(key=lambda x: (not x[0], x[0])) - sample[email] = [h for _, h in items[:per_email]] - return sample - - -def enrich_contributor_profiles(contributors, commits_meta, github_base, token=None): - """In-place: attach `profile` dict to contributors using GitHub GraphQL.""" - if not github_base: - return - if token is None: - token = get_github_token() - if not token: - return - origin = ORIGIN_RE.match(github_base) - if not origin: - return - # gh_graphql is hardcoded to api.github.com; skip Enterprise hosts so we - # don't issue lookups against the wrong API. - if (origin.group("https_host") or "").lower() != "github.com": - return - - missing = [c for c in contributors if not c.get("login")] - if missing: - sample = _sample_oids_per_email( - commits_meta, {c["email"] for c in missing} - ) - resolved = fetch_logins_for_commits( - origin.group("owner"), origin.group("repo"), sample, token - ) - for c in missing: - login = resolved.get(c["email"]) - if login: - c["login"] = login - - top_logins = [c["login"] for c in contributors if c.get("login")] - profiles = fetch_user_profiles(top_logins, token) - for c in contributors: - p = profiles.get(c.get("login") or "") - if p: - c["profile"] = p - - -def main(): - top_n, remote, output, no_open, no_cache, clone, commits_filter, since, until = parse_args( - sys.argv[1:] - ) - - token = None - if remote: - owner, repo = remote.split("/", 1) - token = get_github_token() - # --clone forces the bare-clone path even when a token is present: a - # local clone unlocks per-author language churn the GraphQL history API - # can't provide. The token (if any) is still used below for hovercard - # enrichment, so `use_graphql` — not `token` — gates the API path. - use_graphql = bool(token) and not clone - - if not use_graphql and not clone: - print("No GitHub token — falling back to bare clone.", file=sys.stderr) - - # Subset prompt only in the GraphQL path: probing total via the API is - # cheap, and skipping `--commits N` actually saves network. In the - # bare-clone path the full clone runs regardless, so the prompt would - # only trim local display — pass `--commits` / `--since` for that. - if ( - use_graphql - and not (commits_filter or since or until) - and sys.stdin.isatty() - and sys.stderr.isatty() - ): - has_any_cache = not no_cache and cache_path(remote).exists() - total = None if has_any_cache else probe_remote_total(owner, repo, token) - if total and total > 1000: - commits_filter, since, until = prompt_subset(total) - - if use_graphql: - print(f"Fetching {remote} via GitHub GraphQL…", file=sys.stderr) - else: - print(f"Cloning {remote} (bare) for local analysis…", file=sys.stderr) - ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_remote( - remote, - token if use_graphql else None, - no_cache=no_cache, - commits_filter=commits_filter, - since=since, - until=until, - ) - else: - try: - subprocess.check_output( - ["git", "rev-parse", "--git-dir"], stderr=subprocess.DEVNULL - ) - except subprocess.CalledProcessError: - sys.exit( - "error: not in a git repository (and no owner/repo argument given)" - ) - ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_local() - - if not commits_meta: - sys.exit("error: no commits found") - - if commits_filter or since or until: - total_before = len(commits_meta) - commits_meta, line_stats = apply_filters( - commits_meta, line_stats, commits_filter, since, until - ) - print( - f" filtered: {len(commits_meta)}/{total_before} commits", file=sys.stderr - ) - if not commits_meta: - sys.exit("error: no commits match the given filters") - - tags = filter_tags_to_range(tags, commits_meta) - - data = build_data( - top_n, - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) - - enrich_contributor_profiles(data["contributors"], commits_meta, github_base, token=token) - - payload = f"window.__DATA__ = {json.dumps(data, ensure_ascii=False, separators=(',', ':'))};" - template = TEMPLATE - if template == "__TEMPLATE_PLACEHOLDER__": - sibling = Path(__file__).resolve().parent / "template.html" - if not sibling.exists(): - sys.exit(f"error: unbuilt script and template.html not found at {sibling}") - template = sibling.read_text() - if PLACEHOLDER not in template: - sys.exit(f"error: placeholder {PLACEHOLDER!r} not found in template") - html = template.replace(PLACEHOLDER, payload) - - if output: - out_path = Path(output).expanduser() - else: - safe_name = _slugify(data["repoName"]) or "repo" - owner = "" - if data["githubBaseUrl"]: - m = ORIGIN_RE.match(data["githubBaseUrl"]) - if m: - owner = _slugify(m.group("owner")) - stem = f"{owner}--{safe_name}" if owner else safe_name - out_path = Path("/tmp") / f"{stem}.html" - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(html) - - print(f"Wrote {out_path}") - print( - f" {data['totals']['commits']} commits · " - f"{data['dateRange']['start']} — {data['dateRange']['end']} · " - f"{data['totals']['contributors']} contributor" - f"{'' if data['totals']['contributors'] == 1 else 's'}" - ) - print(" top 3:") - for c in data["contributors"][:3]: - print(f" {c['commits']:>5} {c['name']} <{c['email']}>") - - if no_open: - return - opener = "open" if sys.platform == "darwin" else "xdg-open" - try: - subprocess.run([opener, str(out_path)], check=False) - except FileNotFoundError: - webbrowser.open(out_path.as_uri()) - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\nAborted.", file=sys.stderr) - sys.exit(130) diff --git a/src/repo-intel/techdata.json b/src/repo-intel/techdata.json deleted file mode 100644 index 5eba008..0000000 --- a/src/repo-intel/techdata.json +++ /dev/null @@ -1,2749 +0,0 @@ -{ - "_source": { - "languages": "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/languages.yml", - "vendor": "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/vendor.yml" - }, - "fw_deps": { - "Go": { - "github.com/gin-gonic/gin": "Gin", - "github.com/go-chi/chi": "chi", - "github.com/gofiber/fiber": "Fiber", - "github.com/gorilla/mux": "Gorilla", - "github.com/labstack/echo": "Echo", - "github.com/spf13/cobra": "Cobra", - "google.golang.org/grpc": "gRPC", - "gorm.io/gorm": "GORM" - }, - "PHP": { - "cakephp/cakephp": "CakePHP", - "laravel/framework": "Laravel", - "slim/slim": "Slim", - "symfony/framework-bundle": "Symfony", - "symfony/symfony": "Symfony", - "yiisoft/yii2": "Yii" - }, - "Python": { - "aiohttp": "aiohttp", - "celery": "Celery", - "click": "Click", - "django": "Django", - "djangorestframework": "Django REST", - "fastapi": "FastAPI", - "flask": "Flask", - "httpx": "HTTPX", - "keras": "Keras", - "matplotlib": "Matplotlib", - "numpy": "NumPy", - "pandas": "pandas", - "pydantic": "Pydantic", - "pyramid": "Pyramid", - "pytest": "pytest", - "requests": "Requests", - "sanic": "Sanic", - "scikit-learn": "scikit-learn", - "scipy": "SciPy", - "scrapy": "Scrapy", - "sqlalchemy": "SQLAlchemy", - "starlette": "Starlette", - "tensorflow": "TensorFlow", - "torch": "PyTorch", - "tornado": "Tornado", - "transformers": "Transformers", - "typer": "Typer" - }, - "Ruby": { - "devise": "Devise", - "hanami": "Hanami", - "puma": "Puma", - "rails": "Rails", - "rspec": "RSpec", - "sidekiq": "Sidekiq", - "sinatra": "Sinatra" - }, - "Rust": { - "actix-web": "Actix Web", - "axum": "Axum", - "bevy": "Bevy", - "clap": "clap", - "diesel": "Diesel", - "rocket": "Rocket", - "serde": "Serde", - "tauri": "Tauri", - "tokio": "Tokio", - "tonic": "Tonic", - "warp": "warp" - }, - "npm": { - "@angular/core": "Angular", - "@apollo/client": "Apollo", - "@babel/core": "Babel", - "@biomejs/biome": "Biome", - "@chakra-ui/react": "Chakra UI", - "@hapi/hapi": "hapi", - "@ionic/core": "Ionic", - "@mui/material": "MUI", - "@nestjs/core": "NestJS", - "@nx/workspace": "Nx", - "@playwright/test": "Playwright", - "@prisma/client": "Prisma", - "@reduxjs/toolkit": "Redux", - "@remix-run/react": "Remix", - "@storybook/angular": "Storybook", - "@storybook/html": "Storybook", - "@storybook/preact": "Storybook", - "@storybook/react": "Storybook", - "@storybook/svelte": "Storybook", - "@storybook/vue3": "Storybook", - "@storybook/web-components": "Storybook", - "@sveltejs/kit": "SvelteKit", - "@swc/core": "SWC", - "@testing-library/dom": "Testing Library", - "@testing-library/react": "Testing Library", - "@testing-library/vue": "Testing Library", - "@trpc/client": "tRPC", - "@trpc/server": "tRPC", - "astro": "Astro", - "bootstrap": "Bootstrap", - "chart.js": "Chart.js", - "cypress": "Cypress", - "d3": "D3", - "drizzle-orm": "Drizzle", - "electron": "Electron", - "esbuild": "esbuild", - "eslint": "ESLint", - "expo": "Expo", - "express": "Express", - "fastify": "Fastify", - "gatsby": "Gatsby", - "graphql": "GraphQL", - "jest": "Jest", - "koa": "Koa", - "mocha": "Mocha", - "mongoose": "Mongoose", - "next": "Next.js", - "nuxt": "Nuxt", - "nx": "Nx", - "parcel": "Parcel", - "playwright": "Playwright", - "preact": "Preact", - "prettier": "Prettier", - "prisma": "Prisma", - "puppeteer": "Puppeteer", - "react": "React", - "react-dom": "React", - "react-native": "React Native", - "redux": "Redux", - "rollup": "Rollup", - "sequelize": "Sequelize", - "solid-js": "SolidJS", - "storybook": "Storybook", - "styled-components": "styled-components", - "svelte": "Svelte", - "tailwindcss": "Tailwind CSS", - "testcafe": "TestCafe", - "three": "three.js", - "turbo": "Turborepo", - "typeorm": "TypeORM", - "vite": "Vite", - "vitest": "Vitest", - "vue": "Vue", - "webpack": "webpack", - "zustand": "Zustand" - } - }, - "fw_sentinels_js": [ - [ - "next.config.js", - "Next.js" - ], - [ - "next.config.ts", - "Next.js" - ], - [ - "next.config.mjs", - "Next.js" - ], - [ - "nuxt.config.js", - "Nuxt" - ], - [ - "nuxt.config.ts", - "Nuxt" - ], - [ - "svelte.config.js", - "Svelte" - ], - [ - "astro.config.mjs", - "Astro" - ], - [ - "vue.config.js", - "Vue" - ], - [ - "gatsby-config.js", - "Gatsby" - ], - [ - "angular.json", - "Angular" - ] - ], - "fw_sentinels_other": [ - [ - "manage.py", - "Django", - "Python" - ], - [ - "artisan", - "Laravel", - "PHP" - ], - [ - "config/application.rb", - "Rails", - "Ruby" - ], - [ - "Dockerfile", - "Docker", - "Tools" - ], - [ - "docker-compose.yml", - "Docker Compose", - "Tools" - ], - [ - "docker-compose.yaml", - "Docker Compose", - "Tools" - ], - [ - "compose.yml", - "Docker Compose", - "Tools" - ], - [ - "compose.yaml", - "Docker Compose", - "Tools" - ], - [ - "Makefile", - "Make", - "Tools" - ], - [ - "GNUmakefile", - "Make", - "Tools" - ], - [ - "pnpm-lock.yaml", - "pnpm", - "Tools" - ], - [ - "yarn.lock", - "Yarn", - "Tools" - ], - [ - "bun.lockb", - "Bun", - "Tools" - ], - [ - "bun.lock", - "Bun", - "Tools" - ], - [ - ".gitlab-ci.yml", - "GitLab CI", - "Tools" - ], - [ - "vercel.json", - "Vercel", - "Tools" - ], - [ - "netlify.toml", - "Netlify", - "Tools" - ], - [ - ".github/workflows/", - "GitHub Actions", - "Tools" - ] - ], - "lang": { - "color": { - "1C Enterprise": "#814CCC", - "2-Dimensional Array": "#38761D", - "4D": "#004289", - "ABAP": "#E8274B", - "ABAP CDS": "#555e25", - "AGS Script": "#B9D9FF", - "AIDL": "#34EB6B", - "AL": "#3AA2B5", - "ALGOL": "#D1E0DB", - "AMPL": "#E6EFBB", - "ANTLR": "#9DC3FF", - "API Blueprint": "#2ACCA8", - "APL": "#5A8164", - "ASP.NET": "#9400ff", - "ATS": "#1ac620", - "ActionScript": "#882B0F", - "Ada": "#02f88c", - "Adblock Filter List": "#800000", - "Adobe Font Metrics": "#fa0f00", - "Agda": "#315665", - "Aiken": "#640ff8", - "Alloy": "#64C800", - "Alpine Abuild": "#0D597F", - "Altium Designer": "#A89663", - "AngelScript": "#C7D7DC", - "Answer Set Programming": "#A9CC29", - "Ant Build System": "#A9157E", - "Antlers": "#ff269e", - "ApacheConf": "#d12127", - "Apex": "#1797c0", - "Apollo Guidance Computer": "#0B3D91", - "AppleScript": "#101F1F", - "Arc": "#aa2afe", - "AsciiDoc": "#73a0c5", - "AspectJ": "#a957b0", - "Assembly": "#6E4C13", - "Astro": "#ff5a03", - "Asymptote": "#ff0000", - "Augeas": "#9CC134", - "AutoHotkey": "#6594b9", - "AutoIt": "#1C3552", - "Avro IDL": "#0040FF", - "Awk": "#c30e9b", - "B (Formal Method)": "#8aa8c5", - "B4X": "#00e4ff", - "BASIC": "#ff0000", - "BQN": "#2b7067", - "Ballerina": "#FF5000", - "Batchfile": "#C1F12E", - "Beef": "#a52f4e", - "Berry": "#15A13C", - "BibTeX": "#778899", - "Bicep": "#519aba", - "Bikeshed": "#5562ac", - "Bison": "#6A463F", - "BitBake": "#00bce4", - "Blade": "#f7523f", - "BlitzBasic": "#00FFAE", - "BlitzMax": "#cd6400", - "Bluespec": "#12223c", - "Bluespec BH": "#12223c", - "Boo": "#d4bec1", - "Boogie": "#c80fa0", - "Brainfuck": "#2F2530", - "BrighterScript": "#66AABB", - "Brightscript": "#662D91", - "Browserslist": "#ffd539", - "Bru": "#F4AA41", - "BuildStream": "#006bff", - "C": "#555555", - "C#": "#178600", - "C++": "#f34b7d", - "C3": "#2563eb", - "CAP CDS": "#0092d1", - "CLIPS": "#00A300", - "CMake": "#DA3434", - "COLLADA": "#F1A42B", - "CQL": "#006091", - "CSON": "#244776", - "CSS": "#663399", - "CSV": "#237346", - "CUE": "#5886E1", - "CWeb": "#00007a", - "Cabal Config": "#483465", - "Caddyfile": "#22b638", - "Cadence": "#00ef8b", - "Cairo": "#ff4a48", - "Cairo Zero": "#ff4a48", - "CameLIGO": "#3be133", - "Cangjie": "#00868B", - "Cap'n Proto": "#c42727", - "Carbon": "#222222", - "Ceylon": "#dfa535", - "Chapel": "#8dc63f", - "ChucK": "#3f8000", - "Circom": "#707575", - "Cirru": "#ccccff", - "Clarion": "#db901e", - "Clarity": "#5546ff", - "Classic ASP": "#6a40fd", - "Clean": "#3F85AF", - "Click": "#E4E6F3", - "Clojure": "#db5855", - "Closure Templates": "#0d948f", - "Cloud Firestore Security Rules": "#FFA000", - "Clue": "#0009b5", - "CodeQL": "#140f46", - "CoffeeScript": "#244776", - "ColdFusion": "#ed2cd6", - "ColdFusion CFC": "#ed2cd6", - "Common Lisp": "#3fb68b", - "Common Workflow Language": "#B5314C", - "Component Pascal": "#B0CE4E", - "Cooklang": "#E15A29", - "Crystal": "#000100", - "Csound": "#1a1a1a", - "Csound Document": "#1a1a1a", - "Csound Score": "#1a1a1a", - "Cuda": "#3A4E3A", - "Curry": "#531242", - "Cylc": "#00b3fd", - "Cypher": "#34c0eb", - "Cython": "#fedf5b", - "D": "#ba595e", - "D2": "#526ee8", - "DM": "#447265", - "Dafny": "#FFEC25", - "Darcs Patch": "#8eff23", - "Dart": "#00B4AB", - "Daslang": "#d3d3d3", - "DataWeave": "#003a52", - "Debian Package Control File": "#D70751", - "DenizenScript": "#FBEE96", - "Dhall": "#dfafff", - "DirectX 3D File": "#aace60", - "Dockerfile": "#384d54", - "Dogescript": "#cca760", - "Dotenv": "#e5d559", - "Dune": "#89421e", - "Dylan": "#6c616e", - "E": "#ccce35", - "ECL": "#8a1267", - "ECLiPSe": "#001d9d", - "EJS": "#a91e50", - "EQ": "#a78649", - "Earthly": "#2af0ff", - "Easybuild": "#069406", - "Ecere Projects": "#913960", - "Ecmarkup": "#eb8131", - "Edge": "#0dffe0", - "EdgeQL": "#31A7FF", - "EditorConfig": "#fff1f2", - "Eiffel": "#4d6977", - "Elixir": "#6e4a7e", - "Elm": "#60B5CC", - "Elvish": "#55BB55", - "Elvish Transcript": "#55BB55", - "Emacs Lisp": "#c065db", - "EmberScript": "#FFF4F3", - "Erlang": "#B83998", - "Euphoria": "#FF790B", - "F#": "#b845fc", - "F*": "#572e30", - "FIGlet Font": "#FFDDBB", - "FIRRTL": "#2f632f", - "FLUX": "#88ccff", - "Factor": "#636746", - "Fancy": "#7b9db4", - "Fantom": "#14253c", - "Faust": "#c37240", - "Fennel": "#fff3d7", - "Filebench WML": "#F6B900", - "FlatBuffers": "#ed284a", - "Flix": "#d44a45", - "Fluent": "#ffcc33", - "Forth": "#341708", - "Fortran": "#4d41b1", - "Fortran Free Form": "#4d41b1", - "FreeBASIC": "#141AC9", - "FreeMarker": "#0050b2", - "Frege": "#00cafe", - "Futhark": "#5f021f", - "G-code": "#D08CF2", - "GAML": "#FFC766", - "GAMS": "#f49a22", - "GAP": "#0000cc", - "GCC Machine Description": "#FFCFAB", - "GDScript": "#355570", - "GDShader": "#478CBF", - "GEDCOM": "#003058", - "GLSL": "#5686a5", - "GSC": "#FF6800", - "Game Maker Language": "#71b417", - "Gemfile.lock": "#701516", - "Gemini": "#ff6900", - "Genero 4gl": "#63408e", - "Genero per": "#d8df39", - "Genie": "#fb855d", - "Genshi": "#951531", - "Gentoo Ebuild": "#9400ff", - "Gentoo Eclass": "#9400ff", - "Gerber Image": "#d20b00", - "Gherkin": "#5B2063", - "Git Attributes": "#F44D27", - "Git Commit": "#F44D27", - "Git Config": "#F44D27", - "Git Revision List": "#F44D27", - "Gleam": "#ffaff3", - "Glimmer JS": "#F5835F", - "Glimmer TS": "#3178c6", - "Glyph": "#c1ac7f", - "Gnuplot": "#f0a9f0", - "Go": "#00ADD8", - "Go Checksums": "#00ADD8", - "Go Module": "#00ADD8", - "Go Template": "#00ADD8", - "Go Workspace": "#00ADD8", - "Godot Resource": "#355570", - "Golo": "#88562A", - "Gosu": "#82937f", - "Grace": "#615f8b", - "Gradle": "#02303a", - "Gradle Kotlin DSL": "#02303a", - "Grammatical Framework": "#ff0000", - "GraphQL": "#e10098", - "Graphviz (DOT)": "#2596be", - "Groovy": "#4298b8", - "Groovy Server Pages": "#4298b8", - "HAProxy": "#106da9", - "HCL": "#844FBA", - "HIP": "#4F3A4F", - "HLSL": "#aace60", - "HOCON": "#9ff8ee", - "HTML": "#e34c26", - "HTML+ECR": "#2e1052", - "HTML+EEX": "#6e4a7e", - "HTML+ERB": "#701516", - "HTML+PHP": "#4f5d95", - "HTML+Razor": "#512be4", - "HTTP": "#005C9C", - "HXML": "#f68712", - "Hack": "#878787", - "Haml": "#ece2a9", - "Handlebars": "#f7931e", - "Harbour": "#0e60e3", - "Hare": "#9d7424", - "Haskell": "#5e5086", - "Haxe": "#df7900", - "HiveQL": "#dce200", - "HolyC": "#ffefaf", - "Hosts File": "#308888", - "Hurl": "#FF0288", - "Hy": "#7790B2", - "IDL": "#a3522f", - "IGOR Pro": "#0000cc", - "IL Assembly": "#512BD4", - "INI": "#d1dbe0", - "ISPC": "#2D68B1", - "Idris": "#b30000", - "Ignore List": "#000000", - "ImageJ Macro": "#99AAFF", - "Imba": "#16cec6", - "Inno Setup": "#264b99", - "Io": "#a9188d", - "Ioke": "#078193", - "Isabelle": "#FEFE00", - "Isabelle ROOT": "#FEFE00", - "J": "#9EEDFF", - "JAR Manifest": "#b07219", - "JCL": "#d90e09", - "JFlex": "#DBCA00", - "JSON": "#292929", - "JSON with Comments": "#292929", - "JSON5": "#267CB9", - "JSONLD": "#0c479c", - "JSONiq": "#40d47e", - "Jac": "#FC792D", - "Jai": "#ab8b4b", - "Janet": "#0886a5", - "Jasmin": "#d03600", - "Java": "#b07219", - "Java Properties": "#2A6277", - "Java Server Pages": "#2A6277", - "Java Template Engine": "#2A6277", - "JavaScript": "#f1e05a", - "JavaScript+ERB": "#f1e05a", - "Jest Snapshot": "#15c213", - "JetBrains MPS": "#21D789", - "Jinja": "#a52a22", - "Jison": "#56b3cb", - "Jison Lex": "#56b3cb", - "Jolie": "#843179", - "Jsonnet": "#0064bd", - "Julia": "#a270ba", - "Julia REPL": "#a270ba", - "Jupyter Notebook": "#DA5B0B", - "Just": "#384d54", - "KCL": "#7ABABF", - "KDL": "#ffb3b3", - "KFramework": "#4195c5", - "KRL": "#28430A", - "Kaitai Struct": "#773b37", - "KakouneScript": "#6f8042", - "KerboScript": "#41adf0", - "KiCad Layout": "#2f4aab", - "KiCad Legacy Layout": "#2f4aab", - "KiCad Schematic": "#2f4aab", - "KoLmafia ASH": "#B9D9B9", - "Koka": "#215166", - "Kotlin": "#A97BFF", - "LFE": "#4C3023", - "LLVM": "#185619", - "LOLCODE": "#cc9900", - "LSL": "#3d9970", - "LabVIEW": "#fede06", - "Lambdapi": "#8027a3", - "Langium": "#2c8c87", - "Lark": "#2980B9", - "Lasso": "#999999", - "Latte": "#f2a542", - "Leo": "#C4FFC2", - "Less": "#1d365d", - "Lex": "#DBCA00", - "LigoLANG": "#0e74ff", - "LilyPond": "#9ccc7c", - "Liquid": "#67b8de", - "Liquidsoap": "#990066", - "Literate Agda": "#315665", - "Literate CoffeeScript": "#244776", - "Literate Haskell": "#5e5086", - "LiveCode Script": "#0c5ba5", - "LiveScript": "#499886", - "Logtalk": "#295b9a", - "LookML": "#652B81", - "Lua": "#000080", - "Luau": "#00A2FF", - "M3U": "#179C7D", - "MATLAB": "#e16737", - "MAXScript": "#00a6a6", - "MDX": "#fcb32c", - "MLIR": "#5EC8DB", - "MQL4": "#62A8D6", - "MQL5": "#4A76B8", - "MTML": "#b7e1f4", - "Macaulay2": "#d8ffff", - "Makefile": "#427819", - "Mako": "#7e858d", - "Markdown": "#083fa1", - "Marko": "#42bff2", - "Mask": "#f97732", - "Mathematical Programming System": "#0530ad", - "Max": "#c4a79c", - "MeTTa": "#6a5acd", - "Mercury": "#ff2b2b", - "Mermaid": "#ff3670", - "Meson": "#007800", - "Metal": "#8f14e9", - "MiniYAML": "#ff1111", - "MiniZinc": "#06a9e6", - "Mint": "#02b046", - "Mirah": "#c7a938", - "Modelica": "#de1d31", - "Modula-2": "#10253f", - "Modula-3": "#223388", - "Mojo": "#ff4c1f", - "Monkey C": "#8D6747", - "MoonBit": "#b92381", - "MoonScript": "#ff4585", - "Motoko": "#fbb03b", - "Motorola 68K Assembly": "#005daa", - "Move": "#4a137a", - "Mustache": "#724b3b", - "NCL": "#28431f", - "NMODL": "#00356B", - "NPM Config": "#cb3837", - "NWScript": "#111522", - "Nasal": "#1d2c4e", - "Nearley": "#990000", - "Nemerle": "#3d3c6e", - "NetLinx": "#0aa0ff", - "NetLinx+ERB": "#747faa", - "NetLogo": "#ff6375", - "NewLisp": "#87AED7", - "Nextflow": "#3ac486", - "Nginx": "#009639", - "Nickel": "#E0C3FC", - "Nim": "#ffc200", - "Nit": "#009917", - "Nix": "#7e7eff", - "Noir": "#2f1f49", - "Nu": "#c9df40", - "NumPy": "#9C8AF9", - "Nunjucks": "#3d8137", - "Nushell": "#4E9906", - "OASv2-json": "#85ea2d", - "OASv2-yaml": "#85ea2d", - "OASv3-json": "#85ea2d", - "OASv3-yaml": "#85ea2d", - "OCaml": "#ef7a08", - "OMNeT++ MSG": "#a0e0a0", - "OMNeT++ NED": "#08607c", - "ObjectScript": "#424893", - "Objective-C": "#438eff", - "Objective-C++": "#6866fb", - "Objective-J": "#ff0c5a", - "Odin": "#60AFFE", - "Omgrofl": "#cabbff", - "Opal": "#f7ede0", - "Open Policy Agent": "#7d9199", - "OpenAPI Specification v2": "#85ea2d", - "OpenAPI Specification v3": "#85ea2d", - "OpenCL": "#ed2e2d", - "OpenEdge ABL": "#5ce600", - "OpenQASM": "#AA70FF", - "OpenSCAD": "#e5cd45", - "Option List": "#476732", - "Org": "#77aa99", - "OverpassQL": "#cce2aa", - "Oxygene": "#cdd0e3", - "Oz": "#fab738", - "P4": "#7055b5", - "PDDL": "#0d00ff", - "PEG.js": "#234d6b", - "PHP": "#4F5D95", - "PLSQL": "#dad8d8", - "PLpgSQL": "#336790", - "POV-Ray SDL": "#6bac65", - "Pact": "#F7A8B8", - "Pan": "#cc0000", - "Papyrus": "#6600cc", - "Parrot": "#f3ca0a", - "Pascal": "#E3F171", - "Pawn": "#dbb284", - "Pep8": "#C76F5B", - "Perl": "#0298c3", - "PicoLisp": "#6067af", - "PigLatin": "#fcd7de", - "Pike": "#005390", - "Pip Requirements": "#FFD343", - "Pkl": "#6b9543", - "PlantUML": "#fbbd16", - "PogoScript": "#d80074", - "Polar": "#ae81ff", - "Portugol": "#f8bd00", - "PostCSS": "#dc3a0c", - "PostScript": "#da291c", - "PowerBuilder": "#8f0f8d", - "PowerShell": "#012456", - "Praat": "#c8506d", - "Prisma": "#0c344b", - "Processing": "#0096D8", - "Procfile": "#3B2F63", - "Prolog": "#74283c", - "Promela": "#de0000", - "Propeller Spin": "#7fa2a7", - "Pug": "#a86454", - "Puppet": "#302B6D", - "PureBasic": "#5a6986", - "PureScript": "#1D222D", - "Pyret": "#ee1e10", - "Python": "#3572A5", - "Python console": "#3572A5", - "Python traceback": "#3572A5", - "Q#": "#fed659", - "QML": "#44a51c", - "Qt Script": "#00b841", - "Quake": "#882233", - "QuakeC": "#975777", - "QuickBASIC": "#008080", - "Quint": "#9d6ce5", - "R": "#198CE7", - "RAML": "#77d9fb", - "RAScript": "#2C97FA", - "RBS": "#701516", - "RDoc": "#701516", - "REXX": "#d90e09", - "RMarkdown": "#198ce7", - "RON": "#a62c00", - "ROS Interface": "#22314e", - "RPGLE": "#2BDE21", - "RUNOFF": "#665a4e", - "Racket": "#3c5caa", - "Ragel": "#9d5200", - "Raku": "#0000fb", - "Rascal": "#fffaa0", - "ReScript": "#ed5051", - "Reason": "#ff5847", - "ReasonLIGO": "#ff5847", - "Rebol": "#358a5b", - "Record Jar": "#0673ba", - "Red": "#f50000", - "Regular Expression": "#009a00", - "Ren'Py": "#ff7f7f", - "Rez": "#FFDAB3", - "Ring": "#2D54CB", - "Riot": "#A71E49", - "RobotFramework": "#00c0b5", - "Roc": "#7c38f5", - "Rocq Prover": "#d0b68c", - "Roff": "#ecdebe", - "Roff Manpage": "#ecdebe", - "Rouge": "#cc0088", - "RouterOS Script": "#DE3941", - "Ruby": "#701516", - "Rust": "#dea584", - "SAS": "#B34936", - "SCSS": "#c6538c", - "SPARQL": "#0C4597", - "SQF": "#3F3F3F", - "SQL": "#e38c00", - "SQLPL": "#e38c00", - "SRecode Template": "#348a34", - "STL": "#373b5e", - "SVG": "#ff9900", - "Sail": "#259dd5", - "SaltStack": "#646464", - "Sass": "#a53b70", - "Scala": "#c22d40", - "Scaml": "#bd181a", - "Scenic": "#fdc700", - "Scheme": "#1e4aec", - "Scilab": "#ca0f21", - "Self": "#0579aa", - "ShaderLab": "#222c37", - "Shell": "#89e051", - "ShellCheck Config": "#cecfcb", - "Shen": "#120F14", - "Simple File Verification": "#C9BFED", - "Singularity": "#64E6AD", - "Slang": "#1fbec9", - "Slash": "#007eff", - "Slice": "#003fa2", - "Slim": "#2b2b2b", - "Slint": "#2379F4", - "SmPL": "#c94949", - "Smalltalk": "#596706", - "Smarty": "#f0c040", - "Smithy": "#c44536", - "Snakemake": "#419179", - "Solidity": "#AA6746", - "SourcePawn": "#f69e1d", - "SpiceDB Schema": "#a5318a", - "Squirrel": "#800000", - "Stan": "#b2011d", - "Standard ML": "#dc566d", - "Starlark": "#76d275", - "Stata": "#1a5f91", - "StringTemplate": "#3fb34f", - "Stylus": "#ff6347", - "SubRip Text": "#9e0101", - "SugarSS": "#2fcc9f", - "SuperCollider": "#46390b", - "SurrealQL": "#ff00a0", - "Survex data": "#ffcc99", - "Svelte": "#ff3e00", - "Sway": "#00F58C", - "Sweave": "#198ce7", - "Swift": "#F05138", - "SystemVerilog": "#DAE1C2", - "TI Program": "#A0AA87", - "TL-Verilog": "#C40023", - "TLA": "#4b0079", - "TMDL": "#f0c913", - "TOML": "#9c4221", - "TSQL": "#e38c00", - "TSV": "#237346", - "TSX": "#3178c6", - "TXL": "#0178b8", - "Tact": "#48b5ff", - "Talon": "#333333", - "Tcl": "#e4cc98", - "TeX": "#3D6117", - "Teal": "#00B1BC", - "Terra": "#00004c", - "Terraform Template": "#7b42bb", - "TextGrid": "#c8506d", - "TextMate Properties": "#df66e4", - "Textile": "#ffe7ac", - "Thrift": "#D12127", - "Toit": "#c2c9fb", - "Tools": "#a371f7", - "Tor Config": "#59316b", - "Tree-sitter Query": "#8ea64c", - "Turing": "#cf142b", - "Twig": "#c1d026", - "TypeScript": "#3178c6", - "TypeSpec": "#4A3665", - "Typst": "#239dad", - "Unified Parallel C": "#4e3617", - "Unity3D Asset": "#222c37", - "Uno": "#9933cc", - "UnrealScript": "#a54c4d", - "Untyped Plutus Core": "#36adbd", - "UrWeb": "#ccccee", - "V": "#4f87c4", - "VBA": "#867db1", - "VBScript": "#15dcdc", - "VCL": "#148AA8", - "VHDL": "#adb2cb", - "Vala": "#a56de2", - "Valve Data Format": "#f26025", - "Velocity Template Language": "#507cff", - "Vento": "#ff0080", - "Verilog": "#b2b7f8", - "Vim Help File": "#199f4b", - "Vim Script": "#199f4b", - "Vim Snippet": "#199f4b", - "Visual Basic .NET": "#945db7", - "Visual Basic 6.0": "#2c6353", - "Volt": "#1F1F1F", - "Vue": "#41b883", - "Vyper": "#9F4CF2", - "WDL": "#42f1f4", - "WGSL": "#1a5e9a", - "Web Ontology Language": "#5b70bd", - "WebAssembly": "#04133b", - "WebAssembly Interface Type": "#6250e7", - "Whiley": "#d5c397", - "Wikitext": "#fc5757", - "Windows Registry Entries": "#52d5ff", - "Witcher Script": "#ff0000", - "Wolfram Language": "#dd1100", - "Wollok": "#a23738", - "World of Warcraft Addon Data": "#f7e43f", - "Wren": "#383838", - "X10": "#4B6BEF", - "XC": "#99DA07", - "XML": "#0060ac", - "XML Property List": "#0060ac", - "XQuery": "#5232e7", - "XSLT": "#EB8CEB", - "Xmake": "#22a079", - "Xojo": "#81bd41", - "Xonsh": "#285EEF", - "Xtend": "#24255d", - "YAML": "#cb171e", - "YARA": "#220000", - "YASnippet": "#32AB90", - "Yacc": "#4B6C4B", - "Yul": "#794932", - "ZAP": "#0d665e", - "ZIL": "#dc75e5", - "ZenScript": "#00BCD1", - "Zephir": "#118f9e", - "Zig": "#ec915c", - "Zimpl": "#d67711", - "Zmodel": "#ff7100", - "crontab": "#ead7ac", - "eC": "#913960", - "fish": "#4aae47", - "hoon": "#00b171", - "iCalendar": "#ec564c", - "jq": "#c7254e", - "kvlang": "#1da6e0", - "mIRC Script": "#3d57c3", - "mcfunction": "#E22837", - "mdsvex": "#5f9ea0", - "mupad": "#244963", - "nanorc": "#2d004d", - "nesC": "#94B0C7", - "ooc": "#b0b77e", - "q": "#0040cd", - "reStructuredText": "#141414", - "sed": "#64b970", - "templ": "#66D0DD", - "vCard": "#ee2647", - "wisp": "#7582D1", - "xBase": "#403a40" - }, - "ext": { - "1": "Roff", - "1in": "Roff", - "1m": "Roff", - "1x": "Roff", - "2": "Roff", - "2da": "2-Dimensional Array", - "3": "Roff", - "3in": "Roff", - "3m": "Roff", - "3p": "Roff", - "3pm": "Roff", - "3qt": "Roff", - "3x": "Roff", - "4": "Roff", - "4dform": "JSON", - "4dm": "4D", - "4dproject": "JSON", - "4gl": "Genero 4gl", - "4th": "Forth", - "5": "Roff", - "6": "Roff", - "6pl": "Raku", - "6pm": "Raku", - "7": "Roff", - "8": "Roff", - "8xp": "TI Program", - "8xp.txt": "TI Program", - "9": "Roff", - "_coffee": "CoffeeScript", - "_js": "JavaScript", - "_ls": "LiveScript", - "a51": "Assembly", - "abap": "ABAP", - "action": "ROS Interface", - "ada": "Ada", - "adb": "Ada", - "adml": "XML", - "admx": "XML", - "ado": "Stata", - "adoc": "AsciiDoc", - "adp": "Tcl", - "ads": "Ada", - "afm": "Adobe Font Metrics", - "agc": "Assembly", - "agda": "Agda", - "ahk": "AutoHotkey", - "ahkl": "AutoHotkey", - "aidl": "AIDL", - "aj": "AspectJ", - "ak": "Aiken", - "al": "AL", - "alg": "ALGOL", - "als": "Alloy", - "ampl": "AMPL", - "angelscript": "AngelScript", - "anim": "Unity3D Asset", - "ant": "XML", - "antlers.html": "Antlers", - "antlers.php": "Antlers", - "antlers.xml": "Antlers", - "apacheconf": "ApacheConf", - "apex": "Apex", - "apib": "API Blueprint", - "apl": "APL", - "app": "Erlang", - "app.src": "Erlang", - "applescript": "AppleScript", - "arc": "Arc", - "arr": "Pyret", - "as": "ActionScript", - "asax": "ASP.NET", - "asc": "AGS Script", - "asciidoc": "AsciiDoc", - "ascx": "ASP.NET", - "asd": "Common Lisp", - "asddls": "ABAP CDS", - "ash": "KoLmafia ASH", - "ashx": "ASP.NET", - "asm": "Assembly", - "asmx": "ASP.NET", - "asp": "Classic ASP", - "aspx": "ASP.NET", - "asset": "Unity3D Asset", - "astro": "Astro", - "asy": "Asymptote", - "au3": "AutoIt", - "aug": "Augeas", - "auk": "Awk", - "aux": "TeX", - "avdl": "Avro IDL", - "avsc": "JSON", - "aw": "PHP", - "awk": "Awk", - "axaml": "XML", - "axd": "ASP.NET", - "axi": "NetLinx", - "axi.erb": "NetLinx+ERB", - "axml": "XML", - "axs": "NetLinx", - "axs.erb": "NetLinx+ERB", - "b": "Brainfuck", - "bal": "Ballerina", - "bas": "B4X", - "bash": "Shell", - "bat": "Batchfile", - "bats": "Shell", - "bb": "BitBake", - "bbappend": "BitBake", - "bbclass": "BitBake", - "bbx": "TeX", - "bdy": "PLSQL", - "be": "Berry", - "bf": "Beef", - "bi": "FreeBASIC", - "bib": "TeX", - "bibtex": "TeX", - "bicep": "Bicep", - "bicepparam": "Bicep", - "bison": "Yacc", - "blade": "Blade", - "blade.php": "Blade", - "bmx": "BlitzMax", - "bones": "JavaScript", - "boo": "Boo", - "boot": "Clojure", - "bpl": "Boogie", - "bqn": "BQN", - "brd": "KiCad Legacy Layout", - "brs": "Brightscript", - "bru": "Bru", - "bs": "Bluespec", - "bsl": "1C Enterprise", - "bst": "BuildStream", - "bsv": "Bluespec", - "builder": "Ruby", - "builds": "XML", - "bzl": "Starlark", - "c": "C", - "c++": "C++", - "c3": "C3", - "cabal": "Cabal Config", - "caddyfile": "Caddyfile", - "cairo": "Cairo", - "cake": "C#", - "capnp": "Cap'n Proto", - "carbon": "Carbon", - "cats": "C", - "cbx": "TeX", - "cc": "C++", - "ccproj": "XML", - "ccxml": "XML", - "cdc": "Cadence", - "cdf": "Wolfram Language", - "cds": "CAP CDS", - "ceylon": "Ceylon", - "cfc": "ColdFusion", - "cfg": "HAProxy", - "cfm": "ColdFusion", - "cfml": "ColdFusion", - "cgi": "Perl", - "cginc": "HLSL", - "ch": "xBase", - "chpl": "Chapel", - "circom": "Circom", - "cirru": "Cirru", - "cj": "Cangjie", - "cjs": "JavaScript", - "cjsx": "CoffeeScript", - "ck": "ChucK", - "cl": "C", - "cl2": "Clojure", - "clar": "Clarity", - "click": "Click", - "clixml": "XML", - "clj": "Clojure", - "cljc": "Clojure", - "cljs": "Clojure", - "cljs.hl": "Clojure", - "cljscm": "Clojure", - "cljx": "Clojure", - "clp": "CLIPS", - "cls": "Apex", - "clue": "Clue", - "clw": "Clarion", - "cmake": "CMake", - "cmake.in": "CMake", - "cmd": "Batchfile", - "cmp": "Gerber Image", - "cnc": "G-code", - "cnf": "INI", - "cocci": "SmPL", - "code-snippets": "JSON", - "code-workspace": "JSON", - "coffee": "CoffeeScript", - "coffee.md": "CoffeeScript", - "command": "Shell", - "containerfile": "Dockerfile", - "cook": "Cooklang", - "coq": "Rocq Prover", - "cp": "Component Pascal", - "cpp": "C++", - "cppm": "C++", - "cproject": "XML", - "cps": "Component Pascal", - "cql": "CQL", - "cr": "Crystal", - "cs": "C#", - "cs.pp": "C#", - "csc": "GSC", - "cscfg": "XML", - "csd": "Csound Document", - "csdef": "XML", - "cshtml": "HTML", - "csl": "XML", - "cson": "CSON", - "csproj": "XML", - "css": "CSS", - "csv": "CSV", - "csx": "C#", - "ct": "XML", - "ctl": "Visual Basic 6.0", - "ctp": "PHP", - "cts": "TypeScript", - "cu": "Cuda", - "cue": "CUE", - "cuh": "Cuda", - "curry": "Curry", - "cwl": "Common Workflow Language", - "cxx": "C++", - "cylc": "INI", - "cyp": "Cypher", - "cypher": "Cypher", - "d": "D", - "d2": "D2", - "dae": "COLLADA", - "darcspatch": "Darcs Patch", - "dart": "Dart", - "das": "Daslang", - "dats": "ATS", - "db2": "SQLPL", - "dcl": "Clean", - "ddl": "PLSQL", - "decls": "BlitzBasic", - "depproj": "XML", - "dfm": "Pascal", - "dfy": "Dafny", - "dhall": "Dhall", - "di": "D", - "dita": "XML", - "ditamap": "XML", - "ditaval": "XML", - "djs": "Dogescript", - "dll.config": "XML", - "dlm": "IDL", - "dm": "DM", - "do": "Stata", - "dockerfile": "Dockerfile", - "dof": "INI", - "doh": "Stata", - "dot": "Graphviz (DOT)", - "dotsettings": "XML", - "dpatch": "Darcs Patch", - "dpr": "Pascal", - "druby": "Mirah", - "dsc": "DenizenScript", - "dsp": "Faust", - "dsr": "Visual Basic 6.0", - "dtx": "TeX", - "duby": "Mirah", - "dwl": "DataWeave", - "dyalog": "APL", - "dyl": "Dylan", - "dylan": "Dylan", - "e": "E", - "eb": "Python", - "ebuild": "Shell", - "ec": "eC", - "ecl": "ECL", - "eclass": "Shell", - "eclxml": "ECL", - "ecr": "HTML", - "ect": "EJS", - "edge": "Edge", - "edgeql": "EdgeQL", - "editorconfig": "INI", - "eh": "eC", - "ejs": "EJS", - "ejs.t": "EJS", - "el": "Emacs Lisp", - "eliom": "OCaml", - "eliomi": "OCaml", - "elm": "Elm", - "elv": "Elvish", - "em": "EmberScript", - "emacs": "Emacs Lisp", - "emacs.desktop": "Emacs Lisp", - "emberscript": "EmberScript", - "env": "Dotenv", - "epj": "JavaScript", - "eps": "PostScript", - "epsi": "PostScript", - "eq": "EQ", - "erb": "HTML", - "erb.deface": "HTML", - "erl": "Erlang", - "es": "Erlang", - "es6": "JavaScript", - "escript": "Erlang", - "esdl": "EdgeQL", - "ex": "Elixir", - "exs": "Elixir", - "eye": "Ruby", - "f": "Fortran", - "f03": "Fortran", - "f08": "Fortran", - "f77": "Fortran", - "f90": "Fortran", - "f95": "Fortran", - "factor": "Factor", - "fan": "Fantom", - "fancypack": "Fancy", - "fbs": "FlatBuffers", - "fcgi": "Lua", - "feature": "Gherkin", - "filters": "XML", - "fir": "FIRRTL", - "fish": "Shell", - "flex": "Lex", - "flf": "FIGlet Font", - "flix": "Flix", - "flux": "FLUX", - "fnc": "PLSQL", - "fnl": "Fennel", - "for": "Fortran", - "forth": "Forth", - "fp": "GLSL", - "fpp": "Fortran", - "fr": "Frege", - "frag": "GLSL", - "frg": "GLSL", - "frm": "VBA", - "frt": "Forth", - "fs": "F#", - "fsh": "GLSL", - "fshader": "GLSL", - "fsi": "F#", - "fsproj": "XML", - "fst": "F*", - "fsti": "F*", - "fsx": "F#", - "fth": "Forth", - "ftl": "Fluent", - "ftlh": "FreeMarker", - "fun": "Standard ML", - "fut": "Futhark", - "fx": "FLUX", - "fxh": "HLSL", - "fxml": "XML", - "fy": "Fancy", - "g": "G-code", - "g4": "ANTLR", - "gaml": "GAML", - "gap": "GAP", - "gawk": "Awk", - "gbl": "Gerber Image", - "gbo": "Gerber Image", - "gbp": "Gerber Image", - "gbr": "Gerber Image", - "gbs": "Gerber Image", - "gco": "G-code", - "gcode": "G-code", - "gd": "GDScript", - "gdnlib": "Godot Resource", - "gdns": "Godot Resource", - "gdshader": "GDShader", - "gdshaderinc": "GDShader", - "ged": "GEDCOM", - "gemspec": "Ruby", - "geo": "GLSL", - "geojson": "JSON", - "geom": "GLSL", - "gf": "Grammatical Framework", - "gi": "GAP", - "gitconfig": "INI", - "gitignore": "Ignore List", - "gjs": "JavaScript", - "gko": "Gerber Image", - "glade": "XML", - "gleam": "Gleam", - "glf": "Glyph", - "glsl": "GLSL", - "glslf": "GLSL", - "glslv": "GLSL", - "gltf": "JSON", - "gmi": "Gemini", - "gml": "Game Maker Language", - "gms": "GAMS", - "gmx": "XML", - "gnu": "Gnuplot", - "gnuplot": "Gnuplot", - "go": "Go", - "god": "Ruby", - "gohtml": "Go Template", - "golo": "Golo", - "gotmpl": "Go Template", - "gp": "Gnuplot", - "gpb": "Gerber Image", - "gpt": "Gerber Image", - "gpx": "XML", - "gql": "GraphQL", - "grace": "Grace", - "gradle": "Gradle", - "gradle.kts": "Gradle", - "graphql": "GraphQL", - "graphqls": "GraphQL", - "groovy": "Groovy", - "grt": "Groovy", - "grxml": "XML", - "gs": "Genie", - "gsc": "GSC", - "gsh": "GSC", - "gshader": "GLSL", - "gsp": "Groovy", - "gst": "Gosu", - "gsx": "Gosu", - "gtl": "Gerber Image", - "gto": "Gerber Image", - "gtp": "Gerber Image", - "gtpl": "Groovy", - "gts": "TypeScript", - "gv": "Graphviz (DOT)", - "gvy": "Groovy", - "gyp": "Python", - "gypi": "Python", - "h": "C", - "h++": "C++", - "h.in": "C", - "ha": "Hare", - "hack": "Hack", - "haml": "Haml", - "haml.deface": "Haml", - "handlebars": "Handlebars", - "har": "JSON", - "hats": "ATS", - "hb": "Harbour", - "hbs": "Handlebars", - "hc": "HolyC", - "hcl": "HCL", - "heex": "HTML", - "hh": "C++", - "hhi": "Hack", - "hic": "Clojure", - "hip": "HIP", - "hlsl": "HLSL", - "hlsli": "HLSL", - "hocon": "HOCON", - "hoon": "hoon", - "hpp": "C++", - "hqf": "SQF", - "hql": "HiveQL", - "hrl": "Erlang", - "hs": "Haskell", - "hs-boot": "Haskell", - "hsc": "Haskell", - "hta": "HTML", - "htm": "HTML", - "html": "HTML", - "html.eex": "HTML", - "html.hl": "HTML", - "html.tmpl": "Go Template", - "http": "HTTP", - "hurl": "Hurl", - "hx": "Haxe", - "hxml": "HXML", - "hxsl": "Haxe", - "hxx": "C++", - "hy": "Hy", - "hzp": "XML", - "i": "Assembly", - "i3": "Modula-3", - "ical": "iCalendar", - "ice": "Slice", - "iced": "CoffeeScript", - "icl": "Clean", - "icls": "XML", - "ics": "iCalendar", - "idc": "C", - "idr": "Idris", - "ig": "Modula-3", - "ihlp": "Stata", - "ijm": "ImageJ Macro", - "ijs": "J", - "ik": "Ioke", - "il": "IL Assembly", - "ily": "LilyPond", - "imba": "Imba", - "iml": "XML", - "inc": "Assembly", - "ini": "INI", - "inl": "C++", - "ino": "C++", - "ins": "TeX", - "intr": "Dylan", - "io": "Io", - "iol": "Jolie", - "ipf": "IGOR Pro", - "ipp": "C++", - "ipynb": "Jupyter Notebook", - "isl": "Inno Setup", - "ispc": "ISPC", - "iss": "Inno Setup", - "iuml": "PlantUML", - "ivy": "XML", - "ixx": "C++", - "j": "Jasmin", - "j2": "Jinja", - "jac": "Jac", - "jade": "Pug", - "jai": "Jai", - "jake": "JavaScript", - "janet": "Janet", - "jav": "Java", - "java": "Java", - "javascript": "JavaScript", - "jbuilder": "Ruby", - "jcl": "JCL", - "jelly": "XML", - "jflex": "Lex", - "jinja": "Jinja", - "jinja2": "Jinja", - "jison": "Yacc", - "jisonlex": "Lex", - "jl": "Julia", - "jq": "JSONiq", - "js": "JavaScript", - "js.erb": "JavaScript", - "jsb": "JavaScript", - "jscad": "JavaScript", - "jsfl": "JavaScript", - "jsh": "Java", - "jslib": "JavaScript", - "jsm": "JavaScript", - "json": "JSON", - "json-tmlanguage": "JSON", - "json.example": "JSON", - "json5": "JSON5", - "jsonc": "JSON", - "jsonl": "JSON", - "jsonld": "JSONLD", - "jsonnet": "Jsonnet", - "jsp": "Java", - "jspre": "JavaScript", - "jsproj": "XML", - "jss": "JavaScript", - "jst": "EJS", - "jsx": "JavaScript", - "jte": "Java", - "just": "Just", - "k": "KCL", - "kak": "KakouneScript", - "kdl": "KDL", - "kicad_mod": "KiCad Layout", - "kicad_pcb": "KiCad Layout", - "kicad_sch": "KiCad Schematic", - "kicad_sym": "KiCad Schematic", - "kicad_wks": "KiCad Layout", - "kid": "Genshi", - "kk": "Koka", - "kml": "XML", - "kojo": "Scala", - "krl": "KRL", - "ks": "KerboScript", - "ksh": "Shell", - "ksy": "Kaitai Struct", - "kt": "Kotlin", - "ktm": "Kotlin", - "kts": "Kotlin", - "kv": "kvlang", - "l": "Common Lisp", - "lagda": "Agda", - "langium": "Langium", - "lark": "Lark", - "las": "Lasso", - "lasso": "Lasso", - "lasso8": "Lasso", - "lasso9": "Lasso", - "latte": "Latte", - "launch": "XML", - "lbx": "TeX", - "leex": "HTML", - "lektorproject": "INI", - "leo": "Leo", - "less": "Less", - "lex": "Lex", - "lfe": "LFE", - "lgt": "Logtalk", - "lhs": "Haskell", - "libsonnet": "Jsonnet", - "lid": "Dylan", - "lidr": "Idris", - "ligo": "LigoLANG", - "linq": "C#", - "liq": "Liquidsoap", - "liquid": "Liquid", - "lisp": "Common Lisp", - "litcoffee": "CoffeeScript", - "livecodescript": "LiveCode Script", - "livemd": "Markdown", - "lkml": "LookML", - "ll": "LLVM", - "lmi": "Python", - "logtalk": "Logtalk", - "lol": "LOLCODE", - "lookml": "LookML", - "lp": "Answer Set Programming", - "lpr": "Pascal", - "ls": "LiveScript", - "lsl": "LSL", - "lslp": "LSL", - "lsp": "Common Lisp", - "ltx": "TeX", - "lua": "Lua", - "luau": "Luau", - "lvclass": "LabVIEW", - "lvlib": "LabVIEW", - "lvproj": "LabVIEW", - "ly": "LilyPond", - "m": "Objective-C", - "m2": "Macaulay2", - "m3": "Modula-3", - "m3u": "M3U", - "m3u8": "M3U", - "ma": "Wolfram Language", - "mak": "Makefile", - "make": "Makefile", - "makefile": "Makefile", - "mako": "Mako", - "man": "Roff", - "mao": "Mako", - "markdown": "Markdown", - "marko": "Marko", - "mask": "Mask", - "mat": "Unity3D Asset", - "mata": "Stata", - "matah": "Stata", - "mathematica": "Wolfram Language", - "matlab": "MATLAB", - "mawk": "Awk", - "maxhelp": "Max", - "maxpat": "Max", - "maxproj": "Max", - "mbt": "MoonBit", - "mc": "Monkey C", - "mcfunction": "mcfunction", - "mch": "B (Formal Method)", - "mcmeta": "JSON", - "mcr": "MAXScript", - "md": "Markdown", - "mdoc": "Roff", - "mdown": "Markdown", - "mdpolicy": "XML", - "mdwn": "Markdown", - "mdx": "MDX", - "me": "Roff", - "mediawiki": "Wikitext", - "mermaid": "Mermaid", - "meta": "Unity3D Asset", - "metal": "Metal", - "metta": "MeTTa", - "mg": "Modula-3", - "mint": "Mint", - "mir": "YAML", - "mirah": "Mirah", - "mjml": "XML", - "mjs": "JavaScript", - "mk": "Makefile", - "mkd": "Markdown", - "mkdn": "Markdown", - "mkdown": "Markdown", - "mkfile": "Makefile", - "mkii": "TeX", - "mkiv": "TeX", - "mkvi": "TeX", - "ml": "OCaml", - "ml4": "OCaml", - "mli": "OCaml", - "mligo": "LigoLANG", - "mlir": "MLIR", - "mll": "OCaml", - "mly": "OCaml", - "mm": "Objective-C++", - "mmd": "Mermaid", - "mo": "Modelica", - "mod": "Modula-2", - "mojo": "Mojo", - "moo": "Mercury", - "moon": "MoonScript", - "move": "Move", - "mpl": "JetBrains MPS", - "mps": "JetBrains MPS", - "mq4": "MQL4", - "mq5": "MQL5", - "mqh": "MQL4", - "mrc": "mIRC Script", - "ms": "MAXScript", - "msd": "JetBrains MPS", - "msg": "OMNeT++ MSG", - "mspec": "Ruby", - "mt": "Wolfram Language", - "mtml": "MTML", - "mts": "TypeScript", - "mu": "mupad", - "mud": "ZIL", - "mustache": "Mustache", - "mxml": "XML", - "mxt": "Max", - "mysql": "SQL", - "mzn": "MiniZinc", - "n": "Nemerle", - "nanorc": "INI", - "nas": "Nasal", - "nasm": "Assembly", - "natvis": "XML", - "nawk": "Awk", - "nb": "Wolfram Language", - "nbp": "Wolfram Language", - "nc": "nesC", - "ncl": "NCL", - "ndproj": "XML", - "ne": "Nearley", - "nearley": "Nearley", - "ned": "OMNeT++ NED", - "nf": "Nextflow", - "nginx": "Nginx", - "nginxconf": "Nginx", - "nim": "Nim", - "nim.cfg": "Nim", - "nimble": "Nim", - "nimrod": "Nim", - "nims": "Nim", - "nit": "Nit", - "nix": "Nix", - "njk": "Nunjucks", - "njs": "JavaScript", - "nl": "NewLisp", - "nlogo": "NetLogo", - "nomad": "HCL", - "nproj": "XML", - "nqp": "Raku", - "nr": "Noir", - "nse": "Lua", - "nss": "NWScript", - "nu": "Nu", - "numpy": "Python", - "numpyw": "Python", - "numsc": "Python", - "nuspec": "XML", - "nut": "Squirrel", - "ny": "Common Lisp", - "odd": "XML", - "odin": "Odin", - "ol": "Jolie", - "omgrofl": "Omgrofl", - "ooc": "ooc", - "opal": "Opal", - "opencl": "C", - "orc": "Csound", - "org": "Org", - "os": "1C Enterprise", - "osm": "XML", - "outjob": "Altium Designer", - "overpassql": "OverpassQL", - "owl": "Web Ontology Language", - "oxygene": "Oxygene", - "oz": "Oz", - "p": "OpenEdge ABL", - "p4": "P4", - "p6": "Raku", - "p6l": "Raku", - "p6m": "Raku", - "p8": "Lua", - "pac": "JavaScript", - "pact": "Pact", - "pan": "Pan", - "parrot": "Parrot", - "pas": "Pascal", - "pascal": "Pascal", - "pat": "Max", - "pb": "PureBasic", - "pbi": "PureBasic", - "pbt": "PowerBuilder", - "pcbdoc": "Altium Designer", - "pck": "PLSQL", - "pcss": "CSS", - "pd_lua": "Lua", - "pddl": "PDDL", - "pde": "Processing", - "peggy": "PEG.js", - "pegjs": "PEG.js", - "pep": "Pep8", - "per": "Genero per", - "perl": "Perl", - "pfa": "PostScript", - "pgsql": "PLpgSQL", - "ph": "Perl", - "php": "PHP", - "php3": "PHP", - "php4": "PHP", - "php5": "PHP", - "phps": "PHP", - "phpt": "PHP", - "phtml": "HTML", - "pig": "PigLatin", - "pike": "Pike", - "pkb": "PLSQL", - "pkgproj": "XML", - "pkl": "Pkl", - "pks": "PLSQL", - "pl": "Perl", - "pl6": "Raku", - "plantuml": "PlantUML", - "plb": "PLSQL", - "plist": "XML", - "plot": "Gnuplot", - "pls": "PLSQL", - "plsql": "PLSQL", - "plt": "Gnuplot", - "pluginspec": "Ruby", - "plx": "Perl", - "pm": "Perl", - "pm6": "Raku", - "pml": "Promela", - "pmod": "Pike", - "podsl": "Common Lisp", - "podspec": "Ruby", - "pogo": "PogoScript", - "polar": "Polar", - "por": "Portugol", - "postcss": "CSS", - "pov": "POV-Ray SDL", - "pp": "Puppet", - "pprx": "REXX", - "praat": "Praat", - "prawn": "Ruby", - "prc": "PLSQL", - "prefab": "Unity3D Asset", - "prefs": "INI", - "prg": "xBase", - "prisma": "Prisma", - "prjpcb": "Altium Designer", - "pro": "Prolog", - "proj": "XML", - "prolog": "Prolog", - "properties": "Java Properties", - "props": "XML", - "prw": "xBase", - "ps": "PostScript", - "ps1": "PowerShell", - "ps1xml": "XML", - "psc": "Papyrus", - "psc1": "XML", - "psd1": "PowerShell", - "psgi": "Perl", - "psm1": "PowerShell", - "pt": "XML", - "pubxml": "XML", - "pug": "Pug", - "puml": "PlantUML", - "purs": "PureScript", - "pwn": "Pawn", - "pxd": "Cython", - "pxi": "Cython", - "py": "Python", - "py3": "Python", - "pyde": "Python", - "pyi": "Python", - "pyp": "Python", - "pyt": "Python", - "pytb": "Python", - "pyw": "Python", - "pyx": "Cython", - "q": "HiveQL", - "qasm": "OpenQASM", - "qbs": "QML", - "qc": "QuakeC", - "qhelp": "XML", - "ql": "CodeQL", - "qll": "CodeQL", - "qmd": "RMarkdown", - "qml": "QML", - "qnt": "Quint", - "qs": "Q#", - "r": "R", - "r2": "Rebol", - "r3": "Rebol", - "rabl": "Ruby", - "rake": "Ruby", - "raku": "Raku", - "rakumod": "Raku", - "raml": "RAML", - "rascript": "RAScript", - "razor": "HTML", - "rb": "Ruby", - "rbi": "Ruby", - "rbs": "Ruby", - "rbuild": "Ruby", - "rbw": "Ruby", - "rbx": "Ruby", - "rbxs": "Lua", - "rchit": "GLSL", - "rd": "R", - "rdf": "XML", - "rdoc": "RDoc", - "re": "Reason", - "reb": "Rebol", - "rebol": "Rebol", - "red": "Red", - "reds": "Red", - "reek": "YAML", - "reg": "Windows Registry Entries", - "regex": "Regular Expression", - "regexp": "Regular Expression", - "rego": "Open Policy Agent", - "rei": "Reason", - "religo": "LigoLANG", - "res": "ReScript", - "resi": "ReScript", - "resource": "RobotFramework", - "rest": "reStructuredText", - "rest.txt": "reStructuredText", - "resx": "XML", - "rex": "REXX", - "rexx": "REXX", - "rg": "Rouge", - "rhtml": "HTML", - "ring": "Ring", - "riot": "Riot", - "rkt": "Racket", - "rktd": "Racket", - "rktl": "Racket", - "rl": "Ragel", - "rmd": "RMarkdown", - "rmiss": "GLSL", - "rnh": "RUNOFF", - "rno": "RUNOFF", - "rnw": "Sweave", - "robot": "RobotFramework", - "roc": "Roc", - "rockspec": "Lua", - "roff": "Roff", - "ron": "RON", - "ronn": "Markdown", - "rpgle": "RPGLE", - "rpy": "Ren'Py", - "rq": "SPARQL", - "rs": "Rust", - "rs.in": "Rust", - "rsc": "Rascal", - "rss": "XML", - "rst": "reStructuredText", - "rst.txt": "reStructuredText", - "rsx": "R", - "ru": "Ruby", - "ruby": "Ruby", - "rviz": "YAML", - "s": "Assembly", - "sail": "Sail", - "sarif": "JSON", - "sas": "SAS", - "sass": "Sass", - "sats": "ATS", - "sbatch": "Shell", - "sbt": "Scala", - "sc": "SuperCollider", - "scad": "OpenSCAD", - "scala": "Scala", - "scaml": "Scaml", - "scd": "SuperCollider", - "sce": "Scilab", - "scenic": "Scenic", - "sch": "Scheme", - "schdoc": "Altium Designer", - "sci": "Scilab", - "scm": "Scheme", - "sco": "Csound Score", - "scpt": "AppleScript", - "scrbl": "Racket", - "scss": "SCSS", - "scxml": "XML", - "sdc": "Tcl", - "sed": "sed", - "self": "Self", - "sexp": "Common Lisp", - "sfproj": "XML", - "sfv": "Simple File Verification", - "sh": "Shell", - "sh.in": "Shell", - "shader": "ShaderLab", - "shen": "Shen", - "shproj": "XML", - "sig": "Standard ML", - "sj": "Objective-J", - "sjs": "JavaScript", - "sl": "Slash", - "slang": "Slang", - "sld": "Scheme", - "slim": "Slim", - "slint": "Slint", - "slnx": "XML", - "sls": "SaltStack", - "slurm": "Shell", - "sma": "Pawn", - "smithy": "Smithy", - "smk": "Python", - "sml": "Standard ML", - "snakefile": "Python", - "snap": "Jest Snapshot", - "snip": "Vim Snippet", - "snippet": "Vim Snippet", - "snippets": "Vim Snippet", - "sol": "Solidity", - "soy": "Closure Templates", - "sp": "SourcePawn", - "sparql": "SPARQL", - "spc": "PLSQL", - "spec": "Python", - "spin": "Propeller Spin", - "sps": "Scheme", - "sqf": "SQF", - "sql": "SQL", - "sqlrpgle": "RPGLE", - "sra": "PowerBuilder", - "srdf": "XML", - "srt": "SRecode Template", - "sru": "PowerBuilder", - "srv": "ROS Interface", - "srw": "PowerBuilder", - "ss": "Scheme", - "ssjs": "JavaScript", - "sss": "SugarSS", - "st": "Smalltalk", - "stan": "Stan", - "star": "Starlark", - "sthlp": "Stata", - "stl": "STL", - "story": "Gherkin", - "storyboard": "XML", - "sttheme": "XML", - "sty": "TeX", - "styl": "Stylus", - "sublime-build": "JSON", - "sublime-color-scheme": "JSON", - "sublime-commands": "JSON", - "sublime-completions": "JSON", - "sublime-keymap": "JSON", - "sublime-macro": "JSON", - "sublime-menu": "JSON", - "sublime-mousemap": "JSON", - "sublime-project": "JSON", - "sublime-settings": "JSON", - "sublime-snippet": "XML", - "sublime-syntax": "YAML", - "sublime-theme": "JSON", - "sublime-workspace": "JSON", - "sublime_metrics": "JSON", - "sublime_session": "JSON", - "surql": "SurrealQL", - "sv": "SystemVerilog", - "svelte": "Svelte", - "svg": "SVG", - "svh": "SystemVerilog", - "svx": "mdsvex", - "sw": "Sway", - "swift": "Swift", - "syntax": "YAML", - "t": "Perl", - "tab": "SQL", - "tac": "Python", - "tact": "Tact", - "tag": "Java", - "talon": "Talon", - "targets": "XML", - "tcc": "C++", - "tcl": "Tcl", - "tcl.in": "Tcl", - "templ": "templ", - "tesc": "GLSL", - "tese": "GLSL", - "tex": "TeX", - "textgrid": "TextGrid", - "textile": "Textile", - "tf": "HCL", - "tfstate": "JSON", - "tfstate.backup": "JSON", - "tftpl": "HCL", - "tfvars": "HCL", - "thor": "Ruby", - "thrift": "Thrift", - "thy": "Isabelle", - "tl": "Teal", - "tla": "TLA", - "tlv": "TL-Verilog", - "tm": "Tcl", - "tmac": "Roff", - "tmcommand": "XML", - "tmdl": "TMDL", - "tml": "XML", - "tmlanguage": "XML", - "tmpl": "Go Template", - "tmpreferences": "XML", - "tmsnippet": "XML", - "tmtheme": "XML", - "tmux": "Shell", - "toc": "TeX", - "tofu": "HCL", - "toit": "Toit", - "toml": "TOML", - "toml.example": "TOML", - "tool": "Shell", - "topojson": "JSON", - "tpb": "PLSQL", - "tpl": "Smarty", - "tpp": "C++", - "tps": "PLSQL", - "tres": "Godot Resource", - "trg": "PLSQL", - "trigger": "Apex", - "ts": "TypeScript", - "tscn": "Godot Resource", - "tsconfig.json": "JSON", - "tsp": "TypeSpec", - "tst": "GAP", - "tsv": "TSV", - "tsx": "TypeScript", - "tu": "Turing", - "twig": "Twig", - "txl": "TXL", - "txx": "C++", - "typ": "Typst", - "uc": "UnrealScript", - "udf": "SQL", - "udo": "Csound", - "ui": "XML", - "unity": "Unity3D Asset", - "uno": "Uno", - "upc": "C", - "uplc": "Untyped Plutus Core", - "ur": "UrWeb", - "urdf": "XML", - "url": "INI", - "urs": "UrWeb", - "ux": "XML", - "v": "Verilog", - "vala": "Vala", - "vapi": "Vala", - "vark": "Gosu", - "vb": "Visual Basic .NET", - "vba": "VBA", - "vbhtml": "Visual Basic .NET", - "vbproj": "XML", - "vbs": "VBScript", - "vcf": "vCard", - "vcl": "VCL", - "vcxproj": "XML", - "vdf": "Valve Data Format", - "veo": "Verilog", - "vert": "GLSL", - "vh": "SystemVerilog", - "vhd": "VHDL", - "vhdl": "VHDL", - "vhf": "VHDL", - "vhi": "VHDL", - "vho": "VHDL", - "vhost": "ApacheConf", - "vhs": "VHDL", - "vht": "VHDL", - "vhw": "VHDL", - "vim": "Vim Script", - "vimrc": "Vim Script", - "viw": "SQL", - "vmb": "Vim Script", - "volt": "Volt", - "vrx": "GLSL", - "vs": "GLSL", - "vsh": "GLSL", - "vshader": "GLSL", - "vsixmanifest": "XML", - "vssettings": "XML", - "vstemplate": "XML", - "vtl": "Velocity Template Language", - "vto": "Vento", - "vue": "Vue", - "vw": "PLSQL", - "vxml": "XML", - "vy": "Vyper", - "w": "CWeb", - "wast": "WebAssembly", - "wat": "WebAssembly", - "watchr": "Ruby", - "wdl": "WDL", - "webapp": "JSON", - "webmanifest": "JSON", - "wgsl": "WGSL", - "whiley": "Whiley", - "wiki": "Wikitext", - "wikitext": "Wikitext", - "wisp": "wisp", - "wit": "WebAssembly Interface Type", - "wixproj": "XML", - "wl": "Wolfram Language", - "wlk": "Wollok", - "wls": "Wolfram Language", - "wlt": "Wolfram Language", - "wlua": "Lua", - "workbook": "Markdown", - "workflow": "HCL", - "wren": "Wren", - "ws": "Witcher Script", - "wsdl": "XML", - "wsf": "XML", - "wsgi": "Python", - "wxi": "XML", - "wxl": "XML", - "wxs": "XML", - "x": "DirectX 3D File", - "x10": "X10", - "x3d": "XML", - "x68": "Assembly", - "xacro": "XML", - "xaml": "XML", - "xc": "XC", - "xdc": "Tcl", - "xht": "HTML", - "xhtml": "HTML", - "xib": "XML", - "xlf": "XML", - "xliff": "XML", - "xmi": "XML", - "xml": "XML", - "xml.dist": "XML", - "xmp": "XML", - "xojo_code": "Xojo", - "xojo_menu": "Xojo", - "xojo_report": "Xojo", - "xojo_script": "Xojo", - "xojo_toolbar": "Xojo", - "xojo_window": "Xojo", - "xproj": "XML", - "xpy": "Python", - "xq": "XQuery", - "xql": "XQuery", - "xqm": "XQuery", - "xquery": "XQuery", - "xqy": "XQuery", - "xrl": "Erlang", - "xsd": "XML", - "xsh": "Xonsh", - "xsjs": "JavaScript", - "xsjslib": "JavaScript", - "xsl": "XSLT", - "xslt": "XSLT", - "xspec": "XML", - "xtend": "Xtend", - "xul": "XML", - "xzap": "ZAP", - "y": "Yacc", - "yacc": "Yacc", - "yaml": "MiniYAML", - "yaml-tmlanguage": "YAML", - "yaml.sed": "YAML", - "yap": "Prolog", - "yar": "YARA", - "yara": "YARA", - "yasnippet": "YASnippet", - "yml": "YAML", - "yml.mysql": "YAML", - "yrl": "Erlang", - "yul": "Yul", - "yy": "Yacc", - "yyp": "JSON", - "zap": "ZAP", - "zcml": "XML", - "zed": "SpiceDB Schema", - "zep": "Zephir", - "zig": "Zig", - "zig.zon": "Zig", - "zil": "ZIL", - "zimpl": "Zimpl", - "zmodel": "Zmodel", - "zmpl": "Zimpl", - "zpl": "Zimpl", - "zs": "ZenScript", - "zsh": "Shell", - "zsh-theme": "Shell" - }, - "filename": { - ".abbrev_defs": "Emacs Lisp", - ".ackrc": "Option List", - ".all-contributorsrc": "JSON", - ".arcconfig": "JSON", - ".atomignore": "Ignore List", - ".auto-changelog": "JSON", - ".babelignore": "Ignore List", - ".babelrc": "JSON", - ".bash_aliases": "Shell", - ".bash_functions": "Shell", - ".bash_history": "Shell", - ".bash_logout": "Shell", - ".bash_profile": "Shell", - ".bashrc": "Shell", - ".browserslistrc": "Browserslist", - ".buckconfig": "INI", - ".bzrignore": "Ignore List", - ".c8rc": "JSON", - ".clang-format": "YAML", - ".clang-tidy": "YAML", - ".clangd": "YAML", - ".classpath": "XML", - ".coffeelintignore": "Ignore List", - ".coveragerc": "INI", - ".cproject": "XML", - ".cshrc": "Shell", - ".cvsignore": "Ignore List", - ".devcontainer.json": "JSON", - ".dockerignore": "Ignore List", - ".easignore": "Ignore List", - ".editorconfig": "INI", - ".eleventyignore": "Ignore List", - ".emacs": "Emacs Lisp", - ".emacs.desktop": "Emacs Lisp", - ".env": "Dotenv", - ".env.ci": "Dotenv", - ".env.dev": "Dotenv", - ".env.development": "Dotenv", - ".env.development.local": "Dotenv", - ".env.example": "Dotenv", - ".env.local": "Dotenv", - ".env.prod": "Dotenv", - ".env.production": "Dotenv", - ".env.sample": "Dotenv", - ".env.staging": "Dotenv", - ".env.template": "Dotenv", - ".env.test": "Dotenv", - ".env.testing": "Dotenv", - ".envrc": "Shell", - ".eslintignore": "Ignore List", - ".eslintrc.json": "JSON", - ".exrc": "Vim Script", - ".factor-boot-rc": "Factor", - ".factor-rc": "Factor", - ".flake8": "INI", - ".flaskenv": "Shell", - ".gclient": "Python", - ".gemrc": "YAML", - ".git-blame-ignore-revs": "Git Revision List", - ".gitattributes": "Git Attributes", - ".gitconfig": "INI", - ".gitignore": "Ignore List", - ".gitmodules": "INI", - ".gnus": "Emacs Lisp", - ".gvimrc": "Vim Script", - ".htaccess": "ApacheConf", - ".htmlhintrc": "JSON", - ".ignore": "Ignore List", - ".imgbotconfig": "JSON", - ".irbrc": "Ruby", - ".jscsrc": "JSON", - ".jshintrc": "JSON", - ".jslintrc": "JSON", - ".justfile": "Just", - ".kshrc": "Shell", - ".latexmkrc": "Perl", - ".login": "Shell", - ".luacheckrc": "Lua", - ".markdownlintignore": "Ignore List", - ".nanorc": "INI", - ".nodemonignore": "Ignore List", - ".npmignore": "Ignore List", - ".npmrc": "INI", - ".nvimrc": "Vim Script", - ".nycrc": "JSON", - ".oxlintrc.json": "JSON", - ".php": "PHP", - ".php_cs": "PHP", - ".php_cs.dist": "PHP", - ".prettierignore": "Ignore List", - ".profile": "Shell", - ".project": "XML", - ".pryrc": "Ruby", - ".pylintrc": "INI", - ".rprofile": "R", - ".rspec": "Option List", - ".scalafix.conf": "HOCON", - ".scalafmt.conf": "HOCON", - ".shellcheckrc": "ShellCheck Config", - ".simplecov": "Ruby", - ".spacemacs": "Emacs Lisp", - ".stylelintignore": "Ignore List", - ".swcrc": "JSON", - ".tern-config": "JSON", - ".tern-project": "JSON", - ".tm_properties": "TextMate Properties", - ".tmux.conf": "Shell", - ".vercelignore": "Ignore List", - ".vimrc": "Vim Script", - ".viper": "Emacs Lisp", - ".vscodeignore": "Ignore List", - ".watchmanconfig": "JSON", - ".xinitrc": "Shell", - ".xsession": "Shell", - ".yardopts": "Option List", - ".zlogin": "Shell", - ".zlogout": "Shell", - ".zprofile": "Shell", - ".zshenv": "Shell", - ".zshrc": "Shell", - "9fs": "Shell", - "_emacs": "Emacs Lisp", - "_helpers.tpl": "Go Template", - "_vimrc": "Vim Script", - "abbrev_defs": "Emacs Lisp", - "ack": "Perl", - "ackrc": "Option List", - "ant.xml": "Ant Build System", - "apache2.conf": "ApacheConf", - "api-extractor.json": "JSON", - "apkbuild": "Shell", - "app.config": "XML", - "appraisals": "Ruby", - "bash_aliases": "Shell", - "bash_logout": "Shell", - "bash_profile": "Shell", - "bashrc": "Shell", - "berksfile": "Ruby", - "brewfile": "Ruby", - "browserslist": "Browserslist", - "bsdmakefile": "Makefile", - "buck": "Starlark", - "build": "Starlark", - "build.bazel": "Starlark", - "build.xml": "Ant Build System", - "buildfile": "Ruby", - "buildozer.spec": "INI", - "bun.lock": "JSON", - "cabal.config": "Cabal Config", - "cabal.project": "Cabal Config", - "caddyfile": "Caddyfile", - "cakefile": "CoffeeScript", - "capfile": "Ruby", - "cargo.lock": "TOML", - "cargo.toml.orig": "TOML", - "cask": "Emacs Lisp", - "citation.cff": "YAML", - "cmakelists.txt": "CMake", - "commit_editmsg": "Git Commit", - "composer.lock": "JSON", - "containerfile": "Dockerfile", - "contents.lr": "Markdown", - "cpanfile": "Perl", - "crontab": "crontab", - "cshrc": "Shell", - "dangerfile": "Ruby", - "deliverfile": "Ruby", - "deno.lock": "JSON", - "deps": "Python", - "dev-requirements.txt": "Pip Requirements", - "devcontainer.json": "JSON", - "dockerfile": "Dockerfile", - "dune-project": "Dune", - "earthfile": "Earthly", - "eask": "Emacs Lisp", - "emakefile": "Erlang", - "eqnrc": "Roff", - "expr-dist": "R", - "fakefile": "Fancy", - "fastfile": "Ruby", - "firestore.rules": "Cloud Firestore Security Rules", - "flake.lock": "JSON", - "fp-lib-table": "KiCad Layout", - "gemfile": "Ruby", - "gemfile.lock": "Gemfile.lock", - "gitignore-global": "Ignore List", - "gitignore_global": "Ignore List", - "glide.lock": "YAML", - "gnumakefile": "Makefile", - "go.mod": "Go Module", - "go.sum": "Go Checksums", - "go.work": "Go Workspace", - "go.work.sum": "Go Checksums", - "gopkg.lock": "TOML", - "gradlew": "Shell", - "gradlew.bat": "Batchfile", - "guardfile": "Ruby", - "gvimrc": "Vim Script", - "haproxy.cfg": "HAProxy", - "hosts": "Hosts File", - "hosts.txt": "Hosts File", - "httpd.conf": "ApacheConf", - "installscript.qs": "Qt Script", - "jakefile": "JavaScript", - "jarfile": "Ruby", - "jenkinsfile": "Groovy", - "jsconfig.json": "JSON", - "justfile": "Just", - "kakrc": "KakouneScript", - "kbuild": "Makefile", - "kcl.mod": "KCL", - "kcl.mod.lock": "KCL", - "kshrc": "Shell", - "language-configuration.json": "JSON", - "language-subtag-registry.txt": "Record Jar", - "latexmkrc": "Perl", - "lexer.x": "Lex", - "login": "Shell", - "m3makefile": "Quake", - "m3overrides": "Quake", - "makefile": "Makefile", - "makefile.am": "Makefile", - "makefile.boot": "Makefile", - "makefile.frag": "Makefile", - "makefile.in": "Makefile", - "makefile.inc": "Makefile", - "makefile.pl": "Perl", - "makefile.sco": "Makefile", - "makefile.wat": "Makefile", - "man": "Shell", - "manifest.mf": "JAR Manifest", - "mavenfile": "Ruby", - "mcmod.info": "JSON", - "meson.build": "Meson", - "meson_options.txt": "Meson", - "mise.local.lock": "TOML", - "mise.lock": "TOML", - "mix.lock": "Elixir", - "mkfile": "Makefile", - "mmn": "Roff", - "mmt": "Roff", - "mocha.opts": "Option List", - "module.bazel": "Starlark", - "module.bazel.lock": "JSON", - "modulefile": "Puppet", - "mvnw": "Shell", - "mvnw.cmd": "Batchfile", - "nanorc": "INI", - "nextflow.config": "Nextflow", - "nginx.conf": "Nginx", - "nim.cfg": "Nim", - "notebook": "Jupyter Notebook", - "nuget.config": "XML", - "nukefile": "Nu", - "nvimrc": "Vim Script", - "owh": "Tcl", - "package.resolved": "JSON", - "packages.config": "XML", - "pdm.lock": "TOML", - "phakefile": "PHP", - "pipfile": "TOML", - "pipfile.lock": "JSON", - "pixi.lock": "YAML", - "pkgbuild": "Shell", - "podfile": "Ruby", - "poetry.lock": "TOML", - "procfile": "Procfile", - "profile": "Shell", - "project.ede": "Emacs Lisp", - "project.godot": "Godot Resource", - "puppetfile": "Ruby", - "pylintrc": "INI", - "rakefile": "Ruby", - "rebar.config": "Erlang", - "rebar.config.lock": "Erlang", - "rebar.lock": "Erlang", - "requirements-dev.txt": "Pip Requirements", - "requirements.lock.txt": "Pip Requirements", - "requirements.txt": "Pip Requirements", - "rexfile": "Perl", - "riemann.config": "Clojure", - "root": "Isabelle", - "sconscript": "Python", - "sconstruct": "Python", - "settings.stylecop": "XML", - "singularity": "Singularity", - "slakefile": "LiveScript", - "snakefile": "Python", - "snapfile": "Ruby", - "starfield": "Tcl", - "steepfile": "Ruby", - "suite.rc": "INI", - "thorfile": "Ruby", - "tiltfile": "Starlark", - "tmux.conf": "Shell", - "toolchain_installscript.qs": "Qt Script", - "torrc": "Tor Config", - "troffrc": "Roff", - "troffrc-end": "Roff", - "tsconfig.json": "JSON", - "tslint.json": "JSON", - "uv.lock": "TOML", - "vagrantfile": "Ruby", - "vimrc": "Vim Script", - "vlcrc": "INI", - "web.config": "XML", - "web.debug.config": "XML", - "web.release.config": "XML", - "workspace": "Starlark", - "workspace.bazel": "Starlark", - "workspace.bzlmod": "Starlark", - "wscript": "Python", - "xinitrc": "Shell", - "xmake.lua": "Xmake", - "xsession": "Shell", - "yarn.lock": "YAML", - "zlogin": "Shell", - "zlogout": "Shell", - "zprofile": "Shell", - "zshenv": "Shell", - "zshrc": "Shell" - } - }, - "vendor": [ - "(^|/)cache/", - "^[Dd]ependencies/", - "(^|/)dist/", - "^deps/", - "(^|/)configure$", - "(^|/)config\\.guess$", - "(^|/)config\\.sub$", - "(^|/)aclocal\\.m4", - "(^|/)libtool\\.m4", - "(^|/)ltoptions\\.m4", - "(^|/)ltsugar\\.m4", - "(^|/)ltversion\\.m4", - "(^|/)lt~obsolete\\.m4", - "(^|/)dotnet-install\\.(ps1|sh)$", - "(^|/)cpplint\\.py", - "(^|/)node_modules/", - "(^|/)\\.yarn/releases/", - "(^|/)\\.yarn/plugins/", - "(^|/)\\.yarn/sdks/", - "(^|/)\\.yarn/versions/", - "(^|/)\\.yarn/unplugged/", - "(^|/)_esy$", - "(^|/)bower_components/", - "^rebar$", - "(^|/)erlang\\.mk", - "(^|/)Godeps/_workspace/", - "(^|/)testdata/", - "(^|/)\\.indent\\.pro", - "(\\.|-)min\\.(js|css)$", - "([^\\s]*)import\\.(css|less|scss|styl)$", - "(^|/)bootstrap([^/.]*)(\\..*)?\\.(js|css|less|scss|styl)$", - "(^|/)custom\\.bootstrap([^\\s]*)(js|css|less|scss|styl)$", - "(^|/)font-?awesome\\.(css|less|scss|styl)$", - "(^|/)font-?awesome/.*\\.(css|less|scss|styl)$", - "(^|/)foundation\\.(css|less|scss|styl)$", - "(^|/)normalize\\.(css|less|scss|styl)$", - "(^|/)skeleton\\.(css|less|scss|styl)$", - "(^|/)[Bb]ourbon/.*\\.(css|less|scss|styl)$", - "(^|/)animate\\.(css|less|scss|styl)$", - "(^|/)materialize\\.(css|less|scss|styl|js)$", - "(^|/)select2/.*\\.(css|scss|js)$", - "(^|/)bulma\\.(css|sass|scss)$", - "(3rd|[Tt]hird)[-_]?[Pp]arty/", - "(^|/)vendors?/", - "(^|/)[Ee]xtern(als?)?/", - "(^|/)[Vv]+endor/", - "^debian/", - "(^|/)run\\.n$", - "(^|/)bootstrap-datepicker/", - "(^|/)jquery([^.]*)\\.js$", - "(^|/)jquery\\-\\d\\.\\d+(\\.\\d+)?\\.js$", - "(^|/)jquery\\-ui(\\-\\d\\.\\d+(\\.\\d+)?)?(\\.\\w+)?\\.(js|css)$", - "(^|/)jquery\\.(ui|effects)\\.([^.]*)\\.(js|css)$", - "(^|/)jquery\\.fn\\.gantt\\.js", - "(^|/)jquery\\.fancybox\\.(js|css)", - "(^|/)fuelux\\.js", - "(^|/)jquery\\.fileupload(-\\w+)?\\.js$", - "(^|/)jquery\\.dataTables\\.js", - "(^|/)bootbox\\.js", - "(^|/)pdf\\.worker\\.js", - "(^|/)slick\\.\\w+.js$", - "(^|/)Leaflet\\.Coordinates-\\d+\\.\\d+\\.\\d+\\.src\\.js$", - "(^|/)leaflet\\.draw-src\\.js", - "(^|/)leaflet\\.draw\\.css", - "(^|/)Control\\.FullScreen\\.css", - "(^|/)Control\\.FullScreen\\.js", - "(^|/)leaflet\\.spin\\.js", - "(^|/)wicket-leaflet\\.js", - "(^|/)\\.sublime-project", - "(^|/)\\.sublime-workspace", - "(^|/)\\.vscode/", - "(^|/)prototype(.*)\\.js$", - "(^|/)effects\\.js$", - "(^|/)controls\\.js$", - "(^|/)dragdrop\\.js$", - "(.*?)\\.d\\.ts$", - "(^|/)mootools([^.]*)\\d+\\.\\d+.\\d+([^.]*)\\.js$", - "(^|/)dojo\\.js$", - "(^|/)MochiKit\\.js$", - "(^|/)yahoo-([^.]*)\\.js$", - "(^|/)yui([^.]*)\\.js$", - "(^|/)ckeditor\\.js$", - "(^|/)tiny_mce([^.]*)\\.js$", - "(^|/)tiny_mce/(langs|plugins|themes|utils)", - "(^|/)ace-builds/", - "(^|/)fontello(.*?)\\.css$", - "(^|/)MathJax/", - "(^|/)Chart\\.js$", - "(^|/)[Cc]ode[Mm]irror/(\\d+\\.\\d+/)?(lib|mode|theme|addon|keymap|demo)", - "(^|/)shBrush([^.]*)\\.js$", - "(^|/)shCore\\.js$", - "(^|/)shLegacy\\.js$", - "(^|/)angular([^.]*)\\.js$", - "(^|\\/)d3(\\.v\\d+)?([^.]*)\\.js$", - "(^|/)react(-[^.]*)?\\.js$", - "(^|/)flow-typed/.*\\.js$", - "(^|/)modernizr\\-\\d\\.\\d+(\\.\\d+)?\\.js$", - "(^|/)modernizr\\.custom\\.\\d+\\.js$", - "(^|/)knockout-(\\d+\\.){3}(debug\\.)?js$", - "(^|/)docs?/_?(build|themes?|templates?|static)/", - "(^|/)admin_media/", - "(^|/)env/", - "(^|/)fabfile\\.py$", - "(^|/)waf$", - "(^|/)\\.osx$", - "\\.xctemplate/", - "\\.imageset/", - "(^|/)Carthage/", - "(^|/)Sparkle/", - "(^|/)Crashlytics\\.framework/", - "(^|/)Fabric\\.framework/", - "(^|/)BuddyBuildSDK\\.framework/", - "(^|/)Realm\\.framework", - "(^|/)RealmSwift\\.framework", - "(^|/)\\.gitattributes$", - "(^|/)\\.gitignore$", - "(^|/)\\.gitmodules$", - "(^|/)gradlew$", - "(^|/)gradlew\\.bat$", - "(^|/)gradle/wrapper/", - "(^|/)mvnw$", - "(^|/)mvnw\\.cmd$", - "(^|/)\\.mvn/wrapper/", - "-vsdoc\\.js$", - "\\.intellisense\\.js$", - "(^|/)jquery([^.]*)\\.validate(\\.unobtrusive)?\\.js$", - "(^|/)jquery([^.]*)\\.unobtrusive\\-ajax\\.js$", - "(^|/)[Mm]icrosoft([Mm]vc)?([Aa]jax|[Vv]alidation)(\\.debug)?\\.js$", - "(^|/)[Pp]ackages\\/.+\\.\\d+\\/", - "(^|/)extjs/.*?\\.js$", - "(^|/)extjs/.*?\\.xml$", - "(^|/)extjs/.*?\\.txt$", - "(^|/)extjs/.*?\\.html$", - "(^|/)extjs/.*?\\.properties$", - "(^|/)extjs/\\.sencha/", - "(^|/)extjs/docs/", - "(^|/)extjs/builds/", - "(^|/)extjs/cmd/", - "(^|/)extjs/examples/", - "(^|/)extjs/locale/", - "(^|/)extjs/packages/", - "(^|/)extjs/plugins/", - "(^|/)extjs/resources/", - "(^|/)extjs/src/", - "(^|/)extjs/welcome/", - "(^|/)html5shiv\\.js$", - "(^|/)[Tt]ests?/fixtures/", - "(^|/)[Ss]pecs?/fixtures/", - "(^|/)cordova([^.]*)\\.js$", - "(^|/)cordova\\-\\d\\.\\d(\\.\\d)?\\.js$", - "(^|/)foundation(\\..*)?\\.js$", - "(^|/)Vagrantfile$", - "(^|/)\\.[Dd][Ss]_[Ss]tore$", - "(^|/)inst/extdata/", - "(^|/)octicons\\.css", - "(^|/)sprockets-octicons\\.scss", - "(^|/)activator$", - "(^|/)activator\\.bat$", - "(^|/)proguard\\.pro$", - "(^|/)proguard-rules\\.pro$", - "(^|/)puphpet/", - "(^|/)\\.google_apis/", - "(^|/)Jenkinsfile$", - "(^|/)\\.gitpod\\.Dockerfile$", - "(^|/)\\.github/", - "(^|/)\\.obsidian/", - "(^|/)\\.teamcity/", - "(^|/)xvba_modules/" - ] -} \ No newline at end of file diff --git a/src/repo-intel/template.html b/src/repo-intel/template.html deleted file mode 100644 index b2c6dd6..0000000 --- a/src/repo-intel/template.html +++ /dev/null @@ -1,2060 +0,0 @@ - - - - - - -Repo Intel - - - - - -
-

-

-
- -
-

Contributions

-

Commit timeline

-
Drag to draw a zoom window (drag to pan once zoomed in) · Drag the histogram below to jump · Shift-scroll or pinch to zoom · Hover for details · Hover tag dots to mark a moment · Click to open on GitHub
-
-
-
-
-
- -

Summary

- - - - - - - - - - - - -
#AuthorCommits%Added%Deleted%Net%L/CActive daysAvg/dayFirstLast
-

Overall

-
-
-
-
-
-

Commit frequency over time

-

Commit time patterns (hour of day)

-

Day of week patterns

-
-
-
- - - diff --git a/stow/bin/repo-intel b/stow/bin/repo-intel deleted file mode 100755 index dad1785..0000000 --- a/stow/bin/repo-intel +++ /dev/null @@ -1,1982 +0,0 @@ -#!/usr/bin/env python3 -"""repo-intel — generate a contributor stats dashboard for a git repo.""" - -HELP = """\ -repo-intel — generate a contributor stats dashboard for a git repo. - -Usage: - repo-intel [N] [REPO] [-o PATH] [--no-open] [--clone] - repo-intel -h | --help - -Arguments: - N Number of top contributors to include (default: 10) - REPO A GitHub repository, in any of these forms: - owner/repo - https://github.com/owner/repo - remote:owner/repo - When omitted, the current working directory is used as a local git repo. - -Options: - -o, --output PATH Write the dashboard to PATH instead of /tmp/--.html. - --no-open Don't open the result in a browser. - --no-cache Ignore the local cache and re-fetch all commits. - --clone For a remote REPO, analyse a bare `git clone` instead of - the GitHub GraphQL API (alias: --bare). Slower to fetch - but unlocks per-author language churn the API can't give. - --commits SPEC Filter commits by position. SPEC is either N (last N - commits, newest) or A-B (positions [A, B), 0-indexed - from oldest, half-open like Python slicing). - --since DATE Only include commits on or after DATE (YYYY-MM-DD, inclusive). - --until DATE Only include commits on or before DATE (YYYY-MM-DD, inclusive). - -h, --help Show this help message and exit. - -Examples: - repo-intel # local repo (cwd), top 10 - repo-intel 20 # local repo, top 20 - repo-intel facebook/react # remote, top 10 - repo-intel 15 facebook/react # remote, top 15 - repo-intel -o ./stats.html # write to a specific path - repo-intel --no-open # generate without launching browser - repo-intel facebook/react --clone # analyse via bare clone, not the API - repo-intel --commits 100 # only the last 100 commits - repo-intel --commits 0-100 # the first 100 commits - repo-intel --commits 400-800 # commits at positions 400..799 (oldest-first) - repo-intel --since 2024-01-01 # commits since 2024-01-01 - repo-intel --since 2024-01-01 --until 2024-06-30 # H1 2024 - -Remote auth: - The GitHub CLI (`gh`, https://cli.github.com/) is optional but - recommended — when authenticated it unlocks GraphQL remote fetching - and author hovercard enrichment (avatar, bio, follower counts). - Lookup order: `gh auth token -h github.com`, then $GITHUB_TOKEN. - Falls back to `git clone --bare` into /tmp if neither is available; - pass --clone to force that bare-clone path even when a token is present. - -Output: - /tmp/--.html (or --output PATH), opened in default browser - unless --no-open is given. Falls back to /tmp/.html for local - repos without a github.com origin. - -Cache: - Remote commit nodes are cached under - $XDG_CACHE_HOME/repo-intel (default ~/.cache/repo-intel) as one JSON - file per repo. Re-runs only fetch new commits. -""" - -import hashlib -import json -import os -import re -import subprocess -import sys -import time -import urllib.request -import webbrowser -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from pathlib import Path - -TEMPLATE = '\n\n\n\n\n\nRepo Intel\n\n\n\n\n\n
\n

\n

\n
\n \n
\n

Contributions

\n

Commit timeline

\n
Drag to draw a zoom window (drag to pan once zoomed in) · Drag the histogram below to jump · Shift-scroll or pinch to zoom · Hover for details · Hover tag dots to mark a moment · Click to open on GitHub
\n
\n
\n
\n
\n
\n \n

Summary

\n \n \n \n \n \n \n \n \n \n \n \n \n
#AuthorCommits%Added%Deleted%Net%L/CActive daysAvg/dayFirstLast
\n

Overall

\n
\n
\n
\n
\n
\n

Commit frequency over time

\n

Commit time patterns (hour of day)

\n

Day of week patterns

\n
\n
\n
\n\n\n\n' -PLACEHOLDER = "/*__DATA_INJECTION__*/" -NOREPLY_RE = re.compile(r"(?:\d+\+)?(.+)@users\.noreply\.github\.com") -ORIGIN_RE = re.compile( - r"^(?:https?://(?P[^/]+)/|git@(?P[^:]+):)" - r"(?P[^/]+)/(?P.+?)(?:\.git)?/?$" -) -CACHE_DIR = ( - Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "repo-intel" -) - - -def parse_iso_instant(s): - """Parse an ISO 8601 timestamp to a UTC-aware datetime; epoch on failure. - - Tags mix `Z`-suffixed UTC (GraphQL) with offset-suffixed local time - (`git for-each-ref iso8601-strict`), so lex-sorting can misorder them. - """ - if not s: - return datetime(1970, 1, 1, tzinfo=timezone.utc) - try: - dt = datetime.fromisoformat(s.replace("Z", "+00:00")) - except ValueError: - return datetime(1970, 1, 1, tzinfo=timezone.utc) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.astimezone(timezone.utc) - - -def _slugify(s): - return re.sub(r"[^\w.-]+", "-", s).strip("-") - - -def cache_path(slug): - safe = _slugify(slug.lower()) or "repo" - return CACHE_DIR / f"{safe}.json" - - -def load_cache(slug): - p = cache_path(slug) - if not p.exists(): - return [], False - try: - data = json.loads(p.read_text()) - return data.get("nodes", []), bool(data.get("complete", False)) - except (json.JSONDecodeError, OSError): - return [], False - - -def save_cache(slug, nodes, complete): - CACHE_DIR.mkdir(parents=True, exist_ok=True) - cache_path(slug).write_text(json.dumps({"nodes": nodes, "complete": complete})) - - -def needs_older_fetch(have_count, cached_oldest_date, prev_complete, - commits_filter, since, until): - """Should we paginate below the oldest cached commit after top-fetch? - - have_count: len(new_nodes) + len(cached_nodes) after the top-fetch. - cached_oldest_date: YYYY-MM-DD of the oldest cached commit, "" if empty. - """ - if prev_complete: - return False - if not cached_oldest_date: - return False - if until: - return True - if commits_filter: - if commits_filter[0] == "last": - return have_count < commits_filter[1] - return True # range — slice is anchored at oldest, must walk full history - if since: - return cached_oldest_date > since - return True - - -def parse_commits_spec(val): - if re.fullmatch(r"\d+", val): - n = int(val) - if n <= 0: - raise ValueError("--commits N requires a positive integer") - return ("last", n) - m = re.fullmatch(r"(\d+)-(\d+)", val) - if m: - a, b = int(m.group(1)), int(m.group(2)) - if a >= b: - raise ValueError(f"--commits A-B requires A < B (got {a}-{b})") - return ("range", a, b) - raise ValueError(f"--commits must be N or A-B (got {val!r})") - - -def parse_date(val, flag): - if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", val): - raise ValueError(f"{flag} requires YYYY-MM-DD (got {val!r})") - return val - - -def parse_args(argv): - if any(tok in ("-h", "--help") for tok in argv): - sys.stdout.write(HELP) - sys.exit(0) - top_n, remote, output, no_open, no_cache = 10, None, None, False, False - clone = False - commits_filter, since, until = None, None, None - i = 0 - - def take_value(name): - tok = argv[i] - if tok == name: - if i + 1 >= len(argv): - sys.stderr.write(f"repo-intel: {name} requires a value\n") - sys.exit(2) - return argv[i + 1], 2 - if tok.startswith(name + "="): - return tok[len(name) + 1:], 1 - return None, 0 - - while i < len(argv): - tok = argv[i] - try: - if tok == "--no-open": - no_open = True - i += 1 - continue - if tok == "--no-cache": - no_cache = True - i += 1 - continue - if tok in ("--clone", "--bare"): - clone = True - i += 1 - continue - if tok == "-o": - if i + 1 >= len(argv): - sys.stderr.write("repo-intel: -o requires a value\n") - sys.exit(2) - output = argv[i + 1] - i += 2 - continue - val, step = take_value("--output") - if step: - output = val - i += step - continue - val, step = take_value("--commits") - if step: - commits_filter = parse_commits_spec(val) - i += step - continue - val, step = take_value("--since") - if step: - since = parse_date(val, "--since") - i += step - continue - val, step = take_value("--until") - if step: - until = parse_date(val, "--until") - i += step - continue - except ValueError as exc: - sys.stderr.write(f"repo-intel: {exc}\n") - sys.exit(2) - if tok.isdigit(): - n = int(tok) - if n <= 0: - sys.stderr.write(f"repo-intel: N must be a positive integer (got {tok!r})\n") - sys.exit(2) - top_n = n - i += 1 - continue - t = tok.removeprefix("remote:") - t = t.removeprefix("https://github.com/").removeprefix("http://github.com/") - parts = t.rstrip("/").split("/") - if ( - len(parts) >= 2 - and re.fullmatch(r"[\w.-]+", parts[0]) - and re.fullmatch(r"[\w.-]+", parts[1]) - ): - remote = f"{parts[0]}/{parts[1]}" - i += 1 - continue - sys.stderr.write(f"repo-intel: unrecognized argument: {tok!r}\n") - sys.stderr.write("Try 'repo-intel --help' for usage.\n") - sys.exit(2) - if since and until and since > until: - sys.stderr.write(f"repo-intel: --since {since} is after --until {until}\n") - sys.exit(2) - return top_n, remote, output, no_open, no_cache, clone, commits_filter, since, until - - -def login_from_email(email): - m = NOREPLY_RE.fullmatch(email or "") - return m.group(1) if m else "" - - -def avatar_url(email, override=None): - if override: - return override - login = login_from_email(email) - if login: - return f"https://github.com/{login}.png?size=64" - h = hashlib.md5(email.strip().lower().encode()).hexdigest() - return f"https://www.gravatar.com/avatar/{h}?d=mp&s=64" - - -def iso_week_label(dt): - y, w, _ = dt.isocalendar() - return f"{y}-W{w:02d}" - - -# Language + framework detection data, generated from GitHub Linguist and a -# curated framework map by gen_techdata.py (see `make repo-intel-techdata`). -# build.py inlines the JSON here; when unbuilt we read the sibling file. Used -# only on the local + bare-clone paths — the GraphQL remote path lacks per-file -# data, so these maps go unused there. -TECHDATA = '{\n "_source": {\n "languages": "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/languages.yml",\n "vendor": "https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/vendor.yml"\n },\n "fw_deps": {\n "Go": {\n "github.com/gin-gonic/gin": "Gin",\n "github.com/go-chi/chi": "chi",\n "github.com/gofiber/fiber": "Fiber",\n "github.com/gorilla/mux": "Gorilla",\n "github.com/labstack/echo": "Echo",\n "github.com/spf13/cobra": "Cobra",\n "google.golang.org/grpc": "gRPC",\n "gorm.io/gorm": "GORM"\n },\n "PHP": {\n "cakephp/cakephp": "CakePHP",\n "laravel/framework": "Laravel",\n "slim/slim": "Slim",\n "symfony/framework-bundle": "Symfony",\n "symfony/symfony": "Symfony",\n "yiisoft/yii2": "Yii"\n },\n "Python": {\n "aiohttp": "aiohttp",\n "celery": "Celery",\n "click": "Click",\n "django": "Django",\n "djangorestframework": "Django REST",\n "fastapi": "FastAPI",\n "flask": "Flask",\n "httpx": "HTTPX",\n "keras": "Keras",\n "matplotlib": "Matplotlib",\n "numpy": "NumPy",\n "pandas": "pandas",\n "pydantic": "Pydantic",\n "pyramid": "Pyramid",\n "pytest": "pytest",\n "requests": "Requests",\n "sanic": "Sanic",\n "scikit-learn": "scikit-learn",\n "scipy": "SciPy",\n "scrapy": "Scrapy",\n "sqlalchemy": "SQLAlchemy",\n "starlette": "Starlette",\n "tensorflow": "TensorFlow",\n "torch": "PyTorch",\n "tornado": "Tornado",\n "transformers": "Transformers",\n "typer": "Typer"\n },\n "Ruby": {\n "devise": "Devise",\n "hanami": "Hanami",\n "puma": "Puma",\n "rails": "Rails",\n "rspec": "RSpec",\n "sidekiq": "Sidekiq",\n "sinatra": "Sinatra"\n },\n "Rust": {\n "actix-web": "Actix Web",\n "axum": "Axum",\n "bevy": "Bevy",\n "clap": "clap",\n "diesel": "Diesel",\n "rocket": "Rocket",\n "serde": "Serde",\n "tauri": "Tauri",\n "tokio": "Tokio",\n "tonic": "Tonic",\n "warp": "warp"\n },\n "npm": {\n "@angular/core": "Angular",\n "@apollo/client": "Apollo",\n "@babel/core": "Babel",\n "@biomejs/biome": "Biome",\n "@chakra-ui/react": "Chakra UI",\n "@hapi/hapi": "hapi",\n "@ionic/core": "Ionic",\n "@mui/material": "MUI",\n "@nestjs/core": "NestJS",\n "@nx/workspace": "Nx",\n "@playwright/test": "Playwright",\n "@prisma/client": "Prisma",\n "@reduxjs/toolkit": "Redux",\n "@remix-run/react": "Remix",\n "@storybook/angular": "Storybook",\n "@storybook/html": "Storybook",\n "@storybook/preact": "Storybook",\n "@storybook/react": "Storybook",\n "@storybook/svelte": "Storybook",\n "@storybook/vue3": "Storybook",\n "@storybook/web-components": "Storybook",\n "@sveltejs/kit": "SvelteKit",\n "@swc/core": "SWC",\n "@testing-library/dom": "Testing Library",\n "@testing-library/react": "Testing Library",\n "@testing-library/vue": "Testing Library",\n "@trpc/client": "tRPC",\n "@trpc/server": "tRPC",\n "astro": "Astro",\n "bootstrap": "Bootstrap",\n "chart.js": "Chart.js",\n "cypress": "Cypress",\n "d3": "D3",\n "drizzle-orm": "Drizzle",\n "electron": "Electron",\n "esbuild": "esbuild",\n "eslint": "ESLint",\n "expo": "Expo",\n "express": "Express",\n "fastify": "Fastify",\n "gatsby": "Gatsby",\n "graphql": "GraphQL",\n "jest": "Jest",\n "koa": "Koa",\n "mocha": "Mocha",\n "mongoose": "Mongoose",\n "next": "Next.js",\n "nuxt": "Nuxt",\n "nx": "Nx",\n "parcel": "Parcel",\n "playwright": "Playwright",\n "preact": "Preact",\n "prettier": "Prettier",\n "prisma": "Prisma",\n "puppeteer": "Puppeteer",\n "react": "React",\n "react-dom": "React",\n "react-native": "React Native",\n "redux": "Redux",\n "rollup": "Rollup",\n "sequelize": "Sequelize",\n "solid-js": "SolidJS",\n "storybook": "Storybook",\n "styled-components": "styled-components",\n "svelte": "Svelte",\n "tailwindcss": "Tailwind CSS",\n "testcafe": "TestCafe",\n "three": "three.js",\n "turbo": "Turborepo",\n "typeorm": "TypeORM",\n "vite": "Vite",\n "vitest": "Vitest",\n "vue": "Vue",\n "webpack": "webpack",\n "zustand": "Zustand"\n }\n },\n "fw_sentinels_js": [\n [\n "next.config.js",\n "Next.js"\n ],\n [\n "next.config.ts",\n "Next.js"\n ],\n [\n "next.config.mjs",\n "Next.js"\n ],\n [\n "nuxt.config.js",\n "Nuxt"\n ],\n [\n "nuxt.config.ts",\n "Nuxt"\n ],\n [\n "svelte.config.js",\n "Svelte"\n ],\n [\n "astro.config.mjs",\n "Astro"\n ],\n [\n "vue.config.js",\n "Vue"\n ],\n [\n "gatsby-config.js",\n "Gatsby"\n ],\n [\n "angular.json",\n "Angular"\n ]\n ],\n "fw_sentinels_other": [\n [\n "manage.py",\n "Django",\n "Python"\n ],\n [\n "artisan",\n "Laravel",\n "PHP"\n ],\n [\n "config/application.rb",\n "Rails",\n "Ruby"\n ],\n [\n "Dockerfile",\n "Docker",\n "Tools"\n ],\n [\n "docker-compose.yml",\n "Docker Compose",\n "Tools"\n ],\n [\n "docker-compose.yaml",\n "Docker Compose",\n "Tools"\n ],\n [\n "compose.yml",\n "Docker Compose",\n "Tools"\n ],\n [\n "compose.yaml",\n "Docker Compose",\n "Tools"\n ],\n [\n "Makefile",\n "Make",\n "Tools"\n ],\n [\n "GNUmakefile",\n "Make",\n "Tools"\n ],\n [\n "pnpm-lock.yaml",\n "pnpm",\n "Tools"\n ],\n [\n "yarn.lock",\n "Yarn",\n "Tools"\n ],\n [\n "bun.lockb",\n "Bun",\n "Tools"\n ],\n [\n "bun.lock",\n "Bun",\n "Tools"\n ],\n [\n ".gitlab-ci.yml",\n "GitLab CI",\n "Tools"\n ],\n [\n "vercel.json",\n "Vercel",\n "Tools"\n ],\n [\n "netlify.toml",\n "Netlify",\n "Tools"\n ],\n [\n ".github/workflows/",\n "GitHub Actions",\n "Tools"\n ]\n ],\n "lang": {\n "color": {\n "1C Enterprise": "#814CCC",\n "2-Dimensional Array": "#38761D",\n "4D": "#004289",\n "ABAP": "#E8274B",\n "ABAP CDS": "#555e25",\n "AGS Script": "#B9D9FF",\n "AIDL": "#34EB6B",\n "AL": "#3AA2B5",\n "ALGOL": "#D1E0DB",\n "AMPL": "#E6EFBB",\n "ANTLR": "#9DC3FF",\n "API Blueprint": "#2ACCA8",\n "APL": "#5A8164",\n "ASP.NET": "#9400ff",\n "ATS": "#1ac620",\n "ActionScript": "#882B0F",\n "Ada": "#02f88c",\n "Adblock Filter List": "#800000",\n "Adobe Font Metrics": "#fa0f00",\n "Agda": "#315665",\n "Aiken": "#640ff8",\n "Alloy": "#64C800",\n "Alpine Abuild": "#0D597F",\n "Altium Designer": "#A89663",\n "AngelScript": "#C7D7DC",\n "Answer Set Programming": "#A9CC29",\n "Ant Build System": "#A9157E",\n "Antlers": "#ff269e",\n "ApacheConf": "#d12127",\n "Apex": "#1797c0",\n "Apollo Guidance Computer": "#0B3D91",\n "AppleScript": "#101F1F",\n "Arc": "#aa2afe",\n "AsciiDoc": "#73a0c5",\n "AspectJ": "#a957b0",\n "Assembly": "#6E4C13",\n "Astro": "#ff5a03",\n "Asymptote": "#ff0000",\n "Augeas": "#9CC134",\n "AutoHotkey": "#6594b9",\n "AutoIt": "#1C3552",\n "Avro IDL": "#0040FF",\n "Awk": "#c30e9b",\n "B (Formal Method)": "#8aa8c5",\n "B4X": "#00e4ff",\n "BASIC": "#ff0000",\n "BQN": "#2b7067",\n "Ballerina": "#FF5000",\n "Batchfile": "#C1F12E",\n "Beef": "#a52f4e",\n "Berry": "#15A13C",\n "BibTeX": "#778899",\n "Bicep": "#519aba",\n "Bikeshed": "#5562ac",\n "Bison": "#6A463F",\n "BitBake": "#00bce4",\n "Blade": "#f7523f",\n "BlitzBasic": "#00FFAE",\n "BlitzMax": "#cd6400",\n "Bluespec": "#12223c",\n "Bluespec BH": "#12223c",\n "Boo": "#d4bec1",\n "Boogie": "#c80fa0",\n "Brainfuck": "#2F2530",\n "BrighterScript": "#66AABB",\n "Brightscript": "#662D91",\n "Browserslist": "#ffd539",\n "Bru": "#F4AA41",\n "BuildStream": "#006bff",\n "C": "#555555",\n "C#": "#178600",\n "C++": "#f34b7d",\n "C3": "#2563eb",\n "CAP CDS": "#0092d1",\n "CLIPS": "#00A300",\n "CMake": "#DA3434",\n "COLLADA": "#F1A42B",\n "CQL": "#006091",\n "CSON": "#244776",\n "CSS": "#663399",\n "CSV": "#237346",\n "CUE": "#5886E1",\n "CWeb": "#00007a",\n "Cabal Config": "#483465",\n "Caddyfile": "#22b638",\n "Cadence": "#00ef8b",\n "Cairo": "#ff4a48",\n "Cairo Zero": "#ff4a48",\n "CameLIGO": "#3be133",\n "Cangjie": "#00868B",\n "Cap\'n Proto": "#c42727",\n "Carbon": "#222222",\n "Ceylon": "#dfa535",\n "Chapel": "#8dc63f",\n "ChucK": "#3f8000",\n "Circom": "#707575",\n "Cirru": "#ccccff",\n "Clarion": "#db901e",\n "Clarity": "#5546ff",\n "Classic ASP": "#6a40fd",\n "Clean": "#3F85AF",\n "Click": "#E4E6F3",\n "Clojure": "#db5855",\n "Closure Templates": "#0d948f",\n "Cloud Firestore Security Rules": "#FFA000",\n "Clue": "#0009b5",\n "CodeQL": "#140f46",\n "CoffeeScript": "#244776",\n "ColdFusion": "#ed2cd6",\n "ColdFusion CFC": "#ed2cd6",\n "Common Lisp": "#3fb68b",\n "Common Workflow Language": "#B5314C",\n "Component Pascal": "#B0CE4E",\n "Cooklang": "#E15A29",\n "Crystal": "#000100",\n "Csound": "#1a1a1a",\n "Csound Document": "#1a1a1a",\n "Csound Score": "#1a1a1a",\n "Cuda": "#3A4E3A",\n "Curry": "#531242",\n "Cylc": "#00b3fd",\n "Cypher": "#34c0eb",\n "Cython": "#fedf5b",\n "D": "#ba595e",\n "D2": "#526ee8",\n "DM": "#447265",\n "Dafny": "#FFEC25",\n "Darcs Patch": "#8eff23",\n "Dart": "#00B4AB",\n "Daslang": "#d3d3d3",\n "DataWeave": "#003a52",\n "Debian Package Control File": "#D70751",\n "DenizenScript": "#FBEE96",\n "Dhall": "#dfafff",\n "DirectX 3D File": "#aace60",\n "Dockerfile": "#384d54",\n "Dogescript": "#cca760",\n "Dotenv": "#e5d559",\n "Dune": "#89421e",\n "Dylan": "#6c616e",\n "E": "#ccce35",\n "ECL": "#8a1267",\n "ECLiPSe": "#001d9d",\n "EJS": "#a91e50",\n "EQ": "#a78649",\n "Earthly": "#2af0ff",\n "Easybuild": "#069406",\n "Ecere Projects": "#913960",\n "Ecmarkup": "#eb8131",\n "Edge": "#0dffe0",\n "EdgeQL": "#31A7FF",\n "EditorConfig": "#fff1f2",\n "Eiffel": "#4d6977",\n "Elixir": "#6e4a7e",\n "Elm": "#60B5CC",\n "Elvish": "#55BB55",\n "Elvish Transcript": "#55BB55",\n "Emacs Lisp": "#c065db",\n "EmberScript": "#FFF4F3",\n "Erlang": "#B83998",\n "Euphoria": "#FF790B",\n "F#": "#b845fc",\n "F*": "#572e30",\n "FIGlet Font": "#FFDDBB",\n "FIRRTL": "#2f632f",\n "FLUX": "#88ccff",\n "Factor": "#636746",\n "Fancy": "#7b9db4",\n "Fantom": "#14253c",\n "Faust": "#c37240",\n "Fennel": "#fff3d7",\n "Filebench WML": "#F6B900",\n "FlatBuffers": "#ed284a",\n "Flix": "#d44a45",\n "Fluent": "#ffcc33",\n "Forth": "#341708",\n "Fortran": "#4d41b1",\n "Fortran Free Form": "#4d41b1",\n "FreeBASIC": "#141AC9",\n "FreeMarker": "#0050b2",\n "Frege": "#00cafe",\n "Futhark": "#5f021f",\n "G-code": "#D08CF2",\n "GAML": "#FFC766",\n "GAMS": "#f49a22",\n "GAP": "#0000cc",\n "GCC Machine Description": "#FFCFAB",\n "GDScript": "#355570",\n "GDShader": "#478CBF",\n "GEDCOM": "#003058",\n "GLSL": "#5686a5",\n "GSC": "#FF6800",\n "Game Maker Language": "#71b417",\n "Gemfile.lock": "#701516",\n "Gemini": "#ff6900",\n "Genero 4gl": "#63408e",\n "Genero per": "#d8df39",\n "Genie": "#fb855d",\n "Genshi": "#951531",\n "Gentoo Ebuild": "#9400ff",\n "Gentoo Eclass": "#9400ff",\n "Gerber Image": "#d20b00",\n "Gherkin": "#5B2063",\n "Git Attributes": "#F44D27",\n "Git Commit": "#F44D27",\n "Git Config": "#F44D27",\n "Git Revision List": "#F44D27",\n "Gleam": "#ffaff3",\n "Glimmer JS": "#F5835F",\n "Glimmer TS": "#3178c6",\n "Glyph": "#c1ac7f",\n "Gnuplot": "#f0a9f0",\n "Go": "#00ADD8",\n "Go Checksums": "#00ADD8",\n "Go Module": "#00ADD8",\n "Go Template": "#00ADD8",\n "Go Workspace": "#00ADD8",\n "Godot Resource": "#355570",\n "Golo": "#88562A",\n "Gosu": "#82937f",\n "Grace": "#615f8b",\n "Gradle": "#02303a",\n "Gradle Kotlin DSL": "#02303a",\n "Grammatical Framework": "#ff0000",\n "GraphQL": "#e10098",\n "Graphviz (DOT)": "#2596be",\n "Groovy": "#4298b8",\n "Groovy Server Pages": "#4298b8",\n "HAProxy": "#106da9",\n "HCL": "#844FBA",\n "HIP": "#4F3A4F",\n "HLSL": "#aace60",\n "HOCON": "#9ff8ee",\n "HTML": "#e34c26",\n "HTML+ECR": "#2e1052",\n "HTML+EEX": "#6e4a7e",\n "HTML+ERB": "#701516",\n "HTML+PHP": "#4f5d95",\n "HTML+Razor": "#512be4",\n "HTTP": "#005C9C",\n "HXML": "#f68712",\n "Hack": "#878787",\n "Haml": "#ece2a9",\n "Handlebars": "#f7931e",\n "Harbour": "#0e60e3",\n "Hare": "#9d7424",\n "Haskell": "#5e5086",\n "Haxe": "#df7900",\n "HiveQL": "#dce200",\n "HolyC": "#ffefaf",\n "Hosts File": "#308888",\n "Hurl": "#FF0288",\n "Hy": "#7790B2",\n "IDL": "#a3522f",\n "IGOR Pro": "#0000cc",\n "IL Assembly": "#512BD4",\n "INI": "#d1dbe0",\n "ISPC": "#2D68B1",\n "Idris": "#b30000",\n "Ignore List": "#000000",\n "ImageJ Macro": "#99AAFF",\n "Imba": "#16cec6",\n "Inno Setup": "#264b99",\n "Io": "#a9188d",\n "Ioke": "#078193",\n "Isabelle": "#FEFE00",\n "Isabelle ROOT": "#FEFE00",\n "J": "#9EEDFF",\n "JAR Manifest": "#b07219",\n "JCL": "#d90e09",\n "JFlex": "#DBCA00",\n "JSON": "#292929",\n "JSON with Comments": "#292929",\n "JSON5": "#267CB9",\n "JSONLD": "#0c479c",\n "JSONiq": "#40d47e",\n "Jac": "#FC792D",\n "Jai": "#ab8b4b",\n "Janet": "#0886a5",\n "Jasmin": "#d03600",\n "Java": "#b07219",\n "Java Properties": "#2A6277",\n "Java Server Pages": "#2A6277",\n "Java Template Engine": "#2A6277",\n "JavaScript": "#f1e05a",\n "JavaScript+ERB": "#f1e05a",\n "Jest Snapshot": "#15c213",\n "JetBrains MPS": "#21D789",\n "Jinja": "#a52a22",\n "Jison": "#56b3cb",\n "Jison Lex": "#56b3cb",\n "Jolie": "#843179",\n "Jsonnet": "#0064bd",\n "Julia": "#a270ba",\n "Julia REPL": "#a270ba",\n "Jupyter Notebook": "#DA5B0B",\n "Just": "#384d54",\n "KCL": "#7ABABF",\n "KDL": "#ffb3b3",\n "KFramework": "#4195c5",\n "KRL": "#28430A",\n "Kaitai Struct": "#773b37",\n "KakouneScript": "#6f8042",\n "KerboScript": "#41adf0",\n "KiCad Layout": "#2f4aab",\n "KiCad Legacy Layout": "#2f4aab",\n "KiCad Schematic": "#2f4aab",\n "KoLmafia ASH": "#B9D9B9",\n "Koka": "#215166",\n "Kotlin": "#A97BFF",\n "LFE": "#4C3023",\n "LLVM": "#185619",\n "LOLCODE": "#cc9900",\n "LSL": "#3d9970",\n "LabVIEW": "#fede06",\n "Lambdapi": "#8027a3",\n "Langium": "#2c8c87",\n "Lark": "#2980B9",\n "Lasso": "#999999",\n "Latte": "#f2a542",\n "Leo": "#C4FFC2",\n "Less": "#1d365d",\n "Lex": "#DBCA00",\n "LigoLANG": "#0e74ff",\n "LilyPond": "#9ccc7c",\n "Liquid": "#67b8de",\n "Liquidsoap": "#990066",\n "Literate Agda": "#315665",\n "Literate CoffeeScript": "#244776",\n "Literate Haskell": "#5e5086",\n "LiveCode Script": "#0c5ba5",\n "LiveScript": "#499886",\n "Logtalk": "#295b9a",\n "LookML": "#652B81",\n "Lua": "#000080",\n "Luau": "#00A2FF",\n "M3U": "#179C7D",\n "MATLAB": "#e16737",\n "MAXScript": "#00a6a6",\n "MDX": "#fcb32c",\n "MLIR": "#5EC8DB",\n "MQL4": "#62A8D6",\n "MQL5": "#4A76B8",\n "MTML": "#b7e1f4",\n "Macaulay2": "#d8ffff",\n "Makefile": "#427819",\n "Mako": "#7e858d",\n "Markdown": "#083fa1",\n "Marko": "#42bff2",\n "Mask": "#f97732",\n "Mathematical Programming System": "#0530ad",\n "Max": "#c4a79c",\n "MeTTa": "#6a5acd",\n "Mercury": "#ff2b2b",\n "Mermaid": "#ff3670",\n "Meson": "#007800",\n "Metal": "#8f14e9",\n "MiniYAML": "#ff1111",\n "MiniZinc": "#06a9e6",\n "Mint": "#02b046",\n "Mirah": "#c7a938",\n "Modelica": "#de1d31",\n "Modula-2": "#10253f",\n "Modula-3": "#223388",\n "Mojo": "#ff4c1f",\n "Monkey C": "#8D6747",\n "MoonBit": "#b92381",\n "MoonScript": "#ff4585",\n "Motoko": "#fbb03b",\n "Motorola 68K Assembly": "#005daa",\n "Move": "#4a137a",\n "Mustache": "#724b3b",\n "NCL": "#28431f",\n "NMODL": "#00356B",\n "NPM Config": "#cb3837",\n "NWScript": "#111522",\n "Nasal": "#1d2c4e",\n "Nearley": "#990000",\n "Nemerle": "#3d3c6e",\n "NetLinx": "#0aa0ff",\n "NetLinx+ERB": "#747faa",\n "NetLogo": "#ff6375",\n "NewLisp": "#87AED7",\n "Nextflow": "#3ac486",\n "Nginx": "#009639",\n "Nickel": "#E0C3FC",\n "Nim": "#ffc200",\n "Nit": "#009917",\n "Nix": "#7e7eff",\n "Noir": "#2f1f49",\n "Nu": "#c9df40",\n "NumPy": "#9C8AF9",\n "Nunjucks": "#3d8137",\n "Nushell": "#4E9906",\n "OASv2-json": "#85ea2d",\n "OASv2-yaml": "#85ea2d",\n "OASv3-json": "#85ea2d",\n "OASv3-yaml": "#85ea2d",\n "OCaml": "#ef7a08",\n "OMNeT++ MSG": "#a0e0a0",\n "OMNeT++ NED": "#08607c",\n "ObjectScript": "#424893",\n "Objective-C": "#438eff",\n "Objective-C++": "#6866fb",\n "Objective-J": "#ff0c5a",\n "Odin": "#60AFFE",\n "Omgrofl": "#cabbff",\n "Opal": "#f7ede0",\n "Open Policy Agent": "#7d9199",\n "OpenAPI Specification v2": "#85ea2d",\n "OpenAPI Specification v3": "#85ea2d",\n "OpenCL": "#ed2e2d",\n "OpenEdge ABL": "#5ce600",\n "OpenQASM": "#AA70FF",\n "OpenSCAD": "#e5cd45",\n "Option List": "#476732",\n "Org": "#77aa99",\n "OverpassQL": "#cce2aa",\n "Oxygene": "#cdd0e3",\n "Oz": "#fab738",\n "P4": "#7055b5",\n "PDDL": "#0d00ff",\n "PEG.js": "#234d6b",\n "PHP": "#4F5D95",\n "PLSQL": "#dad8d8",\n "PLpgSQL": "#336790",\n "POV-Ray SDL": "#6bac65",\n "Pact": "#F7A8B8",\n "Pan": "#cc0000",\n "Papyrus": "#6600cc",\n "Parrot": "#f3ca0a",\n "Pascal": "#E3F171",\n "Pawn": "#dbb284",\n "Pep8": "#C76F5B",\n "Perl": "#0298c3",\n "PicoLisp": "#6067af",\n "PigLatin": "#fcd7de",\n "Pike": "#005390",\n "Pip Requirements": "#FFD343",\n "Pkl": "#6b9543",\n "PlantUML": "#fbbd16",\n "PogoScript": "#d80074",\n "Polar": "#ae81ff",\n "Portugol": "#f8bd00",\n "PostCSS": "#dc3a0c",\n "PostScript": "#da291c",\n "PowerBuilder": "#8f0f8d",\n "PowerShell": "#012456",\n "Praat": "#c8506d",\n "Prisma": "#0c344b",\n "Processing": "#0096D8",\n "Procfile": "#3B2F63",\n "Prolog": "#74283c",\n "Promela": "#de0000",\n "Propeller Spin": "#7fa2a7",\n "Pug": "#a86454",\n "Puppet": "#302B6D",\n "PureBasic": "#5a6986",\n "PureScript": "#1D222D",\n "Pyret": "#ee1e10",\n "Python": "#3572A5",\n "Python console": "#3572A5",\n "Python traceback": "#3572A5",\n "Q#": "#fed659",\n "QML": "#44a51c",\n "Qt Script": "#00b841",\n "Quake": "#882233",\n "QuakeC": "#975777",\n "QuickBASIC": "#008080",\n "Quint": "#9d6ce5",\n "R": "#198CE7",\n "RAML": "#77d9fb",\n "RAScript": "#2C97FA",\n "RBS": "#701516",\n "RDoc": "#701516",\n "REXX": "#d90e09",\n "RMarkdown": "#198ce7",\n "RON": "#a62c00",\n "ROS Interface": "#22314e",\n "RPGLE": "#2BDE21",\n "RUNOFF": "#665a4e",\n "Racket": "#3c5caa",\n "Ragel": "#9d5200",\n "Raku": "#0000fb",\n "Rascal": "#fffaa0",\n "ReScript": "#ed5051",\n "Reason": "#ff5847",\n "ReasonLIGO": "#ff5847",\n "Rebol": "#358a5b",\n "Record Jar": "#0673ba",\n "Red": "#f50000",\n "Regular Expression": "#009a00",\n "Ren\'Py": "#ff7f7f",\n "Rez": "#FFDAB3",\n "Ring": "#2D54CB",\n "Riot": "#A71E49",\n "RobotFramework": "#00c0b5",\n "Roc": "#7c38f5",\n "Rocq Prover": "#d0b68c",\n "Roff": "#ecdebe",\n "Roff Manpage": "#ecdebe",\n "Rouge": "#cc0088",\n "RouterOS Script": "#DE3941",\n "Ruby": "#701516",\n "Rust": "#dea584",\n "SAS": "#B34936",\n "SCSS": "#c6538c",\n "SPARQL": "#0C4597",\n "SQF": "#3F3F3F",\n "SQL": "#e38c00",\n "SQLPL": "#e38c00",\n "SRecode Template": "#348a34",\n "STL": "#373b5e",\n "SVG": "#ff9900",\n "Sail": "#259dd5",\n "SaltStack": "#646464",\n "Sass": "#a53b70",\n "Scala": "#c22d40",\n "Scaml": "#bd181a",\n "Scenic": "#fdc700",\n "Scheme": "#1e4aec",\n "Scilab": "#ca0f21",\n "Self": "#0579aa",\n "ShaderLab": "#222c37",\n "Shell": "#89e051",\n "ShellCheck Config": "#cecfcb",\n "Shen": "#120F14",\n "Simple File Verification": "#C9BFED",\n "Singularity": "#64E6AD",\n "Slang": "#1fbec9",\n "Slash": "#007eff",\n "Slice": "#003fa2",\n "Slim": "#2b2b2b",\n "Slint": "#2379F4",\n "SmPL": "#c94949",\n "Smalltalk": "#596706",\n "Smarty": "#f0c040",\n "Smithy": "#c44536",\n "Snakemake": "#419179",\n "Solidity": "#AA6746",\n "SourcePawn": "#f69e1d",\n "SpiceDB Schema": "#a5318a",\n "Squirrel": "#800000",\n "Stan": "#b2011d",\n "Standard ML": "#dc566d",\n "Starlark": "#76d275",\n "Stata": "#1a5f91",\n "StringTemplate": "#3fb34f",\n "Stylus": "#ff6347",\n "SubRip Text": "#9e0101",\n "SugarSS": "#2fcc9f",\n "SuperCollider": "#46390b",\n "SurrealQL": "#ff00a0",\n "Survex data": "#ffcc99",\n "Svelte": "#ff3e00",\n "Sway": "#00F58C",\n "Sweave": "#198ce7",\n "Swift": "#F05138",\n "SystemVerilog": "#DAE1C2",\n "TI Program": "#A0AA87",\n "TL-Verilog": "#C40023",\n "TLA": "#4b0079",\n "TMDL": "#f0c913",\n "TOML": "#9c4221",\n "TSQL": "#e38c00",\n "TSV": "#237346",\n "TSX": "#3178c6",\n "TXL": "#0178b8",\n "Tact": "#48b5ff",\n "Talon": "#333333",\n "Tcl": "#e4cc98",\n "TeX": "#3D6117",\n "Teal": "#00B1BC",\n "Terra": "#00004c",\n "Terraform Template": "#7b42bb",\n "TextGrid": "#c8506d",\n "TextMate Properties": "#df66e4",\n "Textile": "#ffe7ac",\n "Thrift": "#D12127",\n "Toit": "#c2c9fb",\n "Tools": "#a371f7",\n "Tor Config": "#59316b",\n "Tree-sitter Query": "#8ea64c",\n "Turing": "#cf142b",\n "Twig": "#c1d026",\n "TypeScript": "#3178c6",\n "TypeSpec": "#4A3665",\n "Typst": "#239dad",\n "Unified Parallel C": "#4e3617",\n "Unity3D Asset": "#222c37",\n "Uno": "#9933cc",\n "UnrealScript": "#a54c4d",\n "Untyped Plutus Core": "#36adbd",\n "UrWeb": "#ccccee",\n "V": "#4f87c4",\n "VBA": "#867db1",\n "VBScript": "#15dcdc",\n "VCL": "#148AA8",\n "VHDL": "#adb2cb",\n "Vala": "#a56de2",\n "Valve Data Format": "#f26025",\n "Velocity Template Language": "#507cff",\n "Vento": "#ff0080",\n "Verilog": "#b2b7f8",\n "Vim Help File": "#199f4b",\n "Vim Script": "#199f4b",\n "Vim Snippet": "#199f4b",\n "Visual Basic .NET": "#945db7",\n "Visual Basic 6.0": "#2c6353",\n "Volt": "#1F1F1F",\n "Vue": "#41b883",\n "Vyper": "#9F4CF2",\n "WDL": "#42f1f4",\n "WGSL": "#1a5e9a",\n "Web Ontology Language": "#5b70bd",\n "WebAssembly": "#04133b",\n "WebAssembly Interface Type": "#6250e7",\n "Whiley": "#d5c397",\n "Wikitext": "#fc5757",\n "Windows Registry Entries": "#52d5ff",\n "Witcher Script": "#ff0000",\n "Wolfram Language": "#dd1100",\n "Wollok": "#a23738",\n "World of Warcraft Addon Data": "#f7e43f",\n "Wren": "#383838",\n "X10": "#4B6BEF",\n "XC": "#99DA07",\n "XML": "#0060ac",\n "XML Property List": "#0060ac",\n "XQuery": "#5232e7",\n "XSLT": "#EB8CEB",\n "Xmake": "#22a079",\n "Xojo": "#81bd41",\n "Xonsh": "#285EEF",\n "Xtend": "#24255d",\n "YAML": "#cb171e",\n "YARA": "#220000",\n "YASnippet": "#32AB90",\n "Yacc": "#4B6C4B",\n "Yul": "#794932",\n "ZAP": "#0d665e",\n "ZIL": "#dc75e5",\n "ZenScript": "#00BCD1",\n "Zephir": "#118f9e",\n "Zig": "#ec915c",\n "Zimpl": "#d67711",\n "Zmodel": "#ff7100",\n "crontab": "#ead7ac",\n "eC": "#913960",\n "fish": "#4aae47",\n "hoon": "#00b171",\n "iCalendar": "#ec564c",\n "jq": "#c7254e",\n "kvlang": "#1da6e0",\n "mIRC Script": "#3d57c3",\n "mcfunction": "#E22837",\n "mdsvex": "#5f9ea0",\n "mupad": "#244963",\n "nanorc": "#2d004d",\n "nesC": "#94B0C7",\n "ooc": "#b0b77e",\n "q": "#0040cd",\n "reStructuredText": "#141414",\n "sed": "#64b970",\n "templ": "#66D0DD",\n "vCard": "#ee2647",\n "wisp": "#7582D1",\n "xBase": "#403a40"\n },\n "ext": {\n "1": "Roff",\n "1in": "Roff",\n "1m": "Roff",\n "1x": "Roff",\n "2": "Roff",\n "2da": "2-Dimensional Array",\n "3": "Roff",\n "3in": "Roff",\n "3m": "Roff",\n "3p": "Roff",\n "3pm": "Roff",\n "3qt": "Roff",\n "3x": "Roff",\n "4": "Roff",\n "4dform": "JSON",\n "4dm": "4D",\n "4dproject": "JSON",\n "4gl": "Genero 4gl",\n "4th": "Forth",\n "5": "Roff",\n "6": "Roff",\n "6pl": "Raku",\n "6pm": "Raku",\n "7": "Roff",\n "8": "Roff",\n "8xp": "TI Program",\n "8xp.txt": "TI Program",\n "9": "Roff",\n "_coffee": "CoffeeScript",\n "_js": "JavaScript",\n "_ls": "LiveScript",\n "a51": "Assembly",\n "abap": "ABAP",\n "action": "ROS Interface",\n "ada": "Ada",\n "adb": "Ada",\n "adml": "XML",\n "admx": "XML",\n "ado": "Stata",\n "adoc": "AsciiDoc",\n "adp": "Tcl",\n "ads": "Ada",\n "afm": "Adobe Font Metrics",\n "agc": "Assembly",\n "agda": "Agda",\n "ahk": "AutoHotkey",\n "ahkl": "AutoHotkey",\n "aidl": "AIDL",\n "aj": "AspectJ",\n "ak": "Aiken",\n "al": "AL",\n "alg": "ALGOL",\n "als": "Alloy",\n "ampl": "AMPL",\n "angelscript": "AngelScript",\n "anim": "Unity3D Asset",\n "ant": "XML",\n "antlers.html": "Antlers",\n "antlers.php": "Antlers",\n "antlers.xml": "Antlers",\n "apacheconf": "ApacheConf",\n "apex": "Apex",\n "apib": "API Blueprint",\n "apl": "APL",\n "app": "Erlang",\n "app.src": "Erlang",\n "applescript": "AppleScript",\n "arc": "Arc",\n "arr": "Pyret",\n "as": "ActionScript",\n "asax": "ASP.NET",\n "asc": "AGS Script",\n "asciidoc": "AsciiDoc",\n "ascx": "ASP.NET",\n "asd": "Common Lisp",\n "asddls": "ABAP CDS",\n "ash": "KoLmafia ASH",\n "ashx": "ASP.NET",\n "asm": "Assembly",\n "asmx": "ASP.NET",\n "asp": "Classic ASP",\n "aspx": "ASP.NET",\n "asset": "Unity3D Asset",\n "astro": "Astro",\n "asy": "Asymptote",\n "au3": "AutoIt",\n "aug": "Augeas",\n "auk": "Awk",\n "aux": "TeX",\n "avdl": "Avro IDL",\n "avsc": "JSON",\n "aw": "PHP",\n "awk": "Awk",\n "axaml": "XML",\n "axd": "ASP.NET",\n "axi": "NetLinx",\n "axi.erb": "NetLinx+ERB",\n "axml": "XML",\n "axs": "NetLinx",\n "axs.erb": "NetLinx+ERB",\n "b": "Brainfuck",\n "bal": "Ballerina",\n "bas": "B4X",\n "bash": "Shell",\n "bat": "Batchfile",\n "bats": "Shell",\n "bb": "BitBake",\n "bbappend": "BitBake",\n "bbclass": "BitBake",\n "bbx": "TeX",\n "bdy": "PLSQL",\n "be": "Berry",\n "bf": "Beef",\n "bi": "FreeBASIC",\n "bib": "TeX",\n "bibtex": "TeX",\n "bicep": "Bicep",\n "bicepparam": "Bicep",\n "bison": "Yacc",\n "blade": "Blade",\n "blade.php": "Blade",\n "bmx": "BlitzMax",\n "bones": "JavaScript",\n "boo": "Boo",\n "boot": "Clojure",\n "bpl": "Boogie",\n "bqn": "BQN",\n "brd": "KiCad Legacy Layout",\n "brs": "Brightscript",\n "bru": "Bru",\n "bs": "Bluespec",\n "bsl": "1C Enterprise",\n "bst": "BuildStream",\n "bsv": "Bluespec",\n "builder": "Ruby",\n "builds": "XML",\n "bzl": "Starlark",\n "c": "C",\n "c++": "C++",\n "c3": "C3",\n "cabal": "Cabal Config",\n "caddyfile": "Caddyfile",\n "cairo": "Cairo",\n "cake": "C#",\n "capnp": "Cap\'n Proto",\n "carbon": "Carbon",\n "cats": "C",\n "cbx": "TeX",\n "cc": "C++",\n "ccproj": "XML",\n "ccxml": "XML",\n "cdc": "Cadence",\n "cdf": "Wolfram Language",\n "cds": "CAP CDS",\n "ceylon": "Ceylon",\n "cfc": "ColdFusion",\n "cfg": "HAProxy",\n "cfm": "ColdFusion",\n "cfml": "ColdFusion",\n "cgi": "Perl",\n "cginc": "HLSL",\n "ch": "xBase",\n "chpl": "Chapel",\n "circom": "Circom",\n "cirru": "Cirru",\n "cj": "Cangjie",\n "cjs": "JavaScript",\n "cjsx": "CoffeeScript",\n "ck": "ChucK",\n "cl": "C",\n "cl2": "Clojure",\n "clar": "Clarity",\n "click": "Click",\n "clixml": "XML",\n "clj": "Clojure",\n "cljc": "Clojure",\n "cljs": "Clojure",\n "cljs.hl": "Clojure",\n "cljscm": "Clojure",\n "cljx": "Clojure",\n "clp": "CLIPS",\n "cls": "Apex",\n "clue": "Clue",\n "clw": "Clarion",\n "cmake": "CMake",\n "cmake.in": "CMake",\n "cmd": "Batchfile",\n "cmp": "Gerber Image",\n "cnc": "G-code",\n "cnf": "INI",\n "cocci": "SmPL",\n "code-snippets": "JSON",\n "code-workspace": "JSON",\n "coffee": "CoffeeScript",\n "coffee.md": "CoffeeScript",\n "command": "Shell",\n "containerfile": "Dockerfile",\n "cook": "Cooklang",\n "coq": "Rocq Prover",\n "cp": "Component Pascal",\n "cpp": "C++",\n "cppm": "C++",\n "cproject": "XML",\n "cps": "Component Pascal",\n "cql": "CQL",\n "cr": "Crystal",\n "cs": "C#",\n "cs.pp": "C#",\n "csc": "GSC",\n "cscfg": "XML",\n "csd": "Csound Document",\n "csdef": "XML",\n "cshtml": "HTML",\n "csl": "XML",\n "cson": "CSON",\n "csproj": "XML",\n "css": "CSS",\n "csv": "CSV",\n "csx": "C#",\n "ct": "XML",\n "ctl": "Visual Basic 6.0",\n "ctp": "PHP",\n "cts": "TypeScript",\n "cu": "Cuda",\n "cue": "CUE",\n "cuh": "Cuda",\n "curry": "Curry",\n "cwl": "Common Workflow Language",\n "cxx": "C++",\n "cylc": "INI",\n "cyp": "Cypher",\n "cypher": "Cypher",\n "d": "D",\n "d2": "D2",\n "dae": "COLLADA",\n "darcspatch": "Darcs Patch",\n "dart": "Dart",\n "das": "Daslang",\n "dats": "ATS",\n "db2": "SQLPL",\n "dcl": "Clean",\n "ddl": "PLSQL",\n "decls": "BlitzBasic",\n "depproj": "XML",\n "dfm": "Pascal",\n "dfy": "Dafny",\n "dhall": "Dhall",\n "di": "D",\n "dita": "XML",\n "ditamap": "XML",\n "ditaval": "XML",\n "djs": "Dogescript",\n "dll.config": "XML",\n "dlm": "IDL",\n "dm": "DM",\n "do": "Stata",\n "dockerfile": "Dockerfile",\n "dof": "INI",\n "doh": "Stata",\n "dot": "Graphviz (DOT)",\n "dotsettings": "XML",\n "dpatch": "Darcs Patch",\n "dpr": "Pascal",\n "druby": "Mirah",\n "dsc": "DenizenScript",\n "dsp": "Faust",\n "dsr": "Visual Basic 6.0",\n "dtx": "TeX",\n "duby": "Mirah",\n "dwl": "DataWeave",\n "dyalog": "APL",\n "dyl": "Dylan",\n "dylan": "Dylan",\n "e": "E",\n "eb": "Python",\n "ebuild": "Shell",\n "ec": "eC",\n "ecl": "ECL",\n "eclass": "Shell",\n "eclxml": "ECL",\n "ecr": "HTML",\n "ect": "EJS",\n "edge": "Edge",\n "edgeql": "EdgeQL",\n "editorconfig": "INI",\n "eh": "eC",\n "ejs": "EJS",\n "ejs.t": "EJS",\n "el": "Emacs Lisp",\n "eliom": "OCaml",\n "eliomi": "OCaml",\n "elm": "Elm",\n "elv": "Elvish",\n "em": "EmberScript",\n "emacs": "Emacs Lisp",\n "emacs.desktop": "Emacs Lisp",\n "emberscript": "EmberScript",\n "env": "Dotenv",\n "epj": "JavaScript",\n "eps": "PostScript",\n "epsi": "PostScript",\n "eq": "EQ",\n "erb": "HTML",\n "erb.deface": "HTML",\n "erl": "Erlang",\n "es": "Erlang",\n "es6": "JavaScript",\n "escript": "Erlang",\n "esdl": "EdgeQL",\n "ex": "Elixir",\n "exs": "Elixir",\n "eye": "Ruby",\n "f": "Fortran",\n "f03": "Fortran",\n "f08": "Fortran",\n "f77": "Fortran",\n "f90": "Fortran",\n "f95": "Fortran",\n "factor": "Factor",\n "fan": "Fantom",\n "fancypack": "Fancy",\n "fbs": "FlatBuffers",\n "fcgi": "Lua",\n "feature": "Gherkin",\n "filters": "XML",\n "fir": "FIRRTL",\n "fish": "Shell",\n "flex": "Lex",\n "flf": "FIGlet Font",\n "flix": "Flix",\n "flux": "FLUX",\n "fnc": "PLSQL",\n "fnl": "Fennel",\n "for": "Fortran",\n "forth": "Forth",\n "fp": "GLSL",\n "fpp": "Fortran",\n "fr": "Frege",\n "frag": "GLSL",\n "frg": "GLSL",\n "frm": "VBA",\n "frt": "Forth",\n "fs": "F#",\n "fsh": "GLSL",\n "fshader": "GLSL",\n "fsi": "F#",\n "fsproj": "XML",\n "fst": "F*",\n "fsti": "F*",\n "fsx": "F#",\n "fth": "Forth",\n "ftl": "Fluent",\n "ftlh": "FreeMarker",\n "fun": "Standard ML",\n "fut": "Futhark",\n "fx": "FLUX",\n "fxh": "HLSL",\n "fxml": "XML",\n "fy": "Fancy",\n "g": "G-code",\n "g4": "ANTLR",\n "gaml": "GAML",\n "gap": "GAP",\n "gawk": "Awk",\n "gbl": "Gerber Image",\n "gbo": "Gerber Image",\n "gbp": "Gerber Image",\n "gbr": "Gerber Image",\n "gbs": "Gerber Image",\n "gco": "G-code",\n "gcode": "G-code",\n "gd": "GDScript",\n "gdnlib": "Godot Resource",\n "gdns": "Godot Resource",\n "gdshader": "GDShader",\n "gdshaderinc": "GDShader",\n "ged": "GEDCOM",\n "gemspec": "Ruby",\n "geo": "GLSL",\n "geojson": "JSON",\n "geom": "GLSL",\n "gf": "Grammatical Framework",\n "gi": "GAP",\n "gitconfig": "INI",\n "gitignore": "Ignore List",\n "gjs": "JavaScript",\n "gko": "Gerber Image",\n "glade": "XML",\n "gleam": "Gleam",\n "glf": "Glyph",\n "glsl": "GLSL",\n "glslf": "GLSL",\n "glslv": "GLSL",\n "gltf": "JSON",\n "gmi": "Gemini",\n "gml": "Game Maker Language",\n "gms": "GAMS",\n "gmx": "XML",\n "gnu": "Gnuplot",\n "gnuplot": "Gnuplot",\n "go": "Go",\n "god": "Ruby",\n "gohtml": "Go Template",\n "golo": "Golo",\n "gotmpl": "Go Template",\n "gp": "Gnuplot",\n "gpb": "Gerber Image",\n "gpt": "Gerber Image",\n "gpx": "XML",\n "gql": "GraphQL",\n "grace": "Grace",\n "gradle": "Gradle",\n "gradle.kts": "Gradle",\n "graphql": "GraphQL",\n "graphqls": "GraphQL",\n "groovy": "Groovy",\n "grt": "Groovy",\n "grxml": "XML",\n "gs": "Genie",\n "gsc": "GSC",\n "gsh": "GSC",\n "gshader": "GLSL",\n "gsp": "Groovy",\n "gst": "Gosu",\n "gsx": "Gosu",\n "gtl": "Gerber Image",\n "gto": "Gerber Image",\n "gtp": "Gerber Image",\n "gtpl": "Groovy",\n "gts": "TypeScript",\n "gv": "Graphviz (DOT)",\n "gvy": "Groovy",\n "gyp": "Python",\n "gypi": "Python",\n "h": "C",\n "h++": "C++",\n "h.in": "C",\n "ha": "Hare",\n "hack": "Hack",\n "haml": "Haml",\n "haml.deface": "Haml",\n "handlebars": "Handlebars",\n "har": "JSON",\n "hats": "ATS",\n "hb": "Harbour",\n "hbs": "Handlebars",\n "hc": "HolyC",\n "hcl": "HCL",\n "heex": "HTML",\n "hh": "C++",\n "hhi": "Hack",\n "hic": "Clojure",\n "hip": "HIP",\n "hlsl": "HLSL",\n "hlsli": "HLSL",\n "hocon": "HOCON",\n "hoon": "hoon",\n "hpp": "C++",\n "hqf": "SQF",\n "hql": "HiveQL",\n "hrl": "Erlang",\n "hs": "Haskell",\n "hs-boot": "Haskell",\n "hsc": "Haskell",\n "hta": "HTML",\n "htm": "HTML",\n "html": "HTML",\n "html.eex": "HTML",\n "html.hl": "HTML",\n "html.tmpl": "Go Template",\n "http": "HTTP",\n "hurl": "Hurl",\n "hx": "Haxe",\n "hxml": "HXML",\n "hxsl": "Haxe",\n "hxx": "C++",\n "hy": "Hy",\n "hzp": "XML",\n "i": "Assembly",\n "i3": "Modula-3",\n "ical": "iCalendar",\n "ice": "Slice",\n "iced": "CoffeeScript",\n "icl": "Clean",\n "icls": "XML",\n "ics": "iCalendar",\n "idc": "C",\n "idr": "Idris",\n "ig": "Modula-3",\n "ihlp": "Stata",\n "ijm": "ImageJ Macro",\n "ijs": "J",\n "ik": "Ioke",\n "il": "IL Assembly",\n "ily": "LilyPond",\n "imba": "Imba",\n "iml": "XML",\n "inc": "Assembly",\n "ini": "INI",\n "inl": "C++",\n "ino": "C++",\n "ins": "TeX",\n "intr": "Dylan",\n "io": "Io",\n "iol": "Jolie",\n "ipf": "IGOR Pro",\n "ipp": "C++",\n "ipynb": "Jupyter Notebook",\n "isl": "Inno Setup",\n "ispc": "ISPC",\n "iss": "Inno Setup",\n "iuml": "PlantUML",\n "ivy": "XML",\n "ixx": "C++",\n "j": "Jasmin",\n "j2": "Jinja",\n "jac": "Jac",\n "jade": "Pug",\n "jai": "Jai",\n "jake": "JavaScript",\n "janet": "Janet",\n "jav": "Java",\n "java": "Java",\n "javascript": "JavaScript",\n "jbuilder": "Ruby",\n "jcl": "JCL",\n "jelly": "XML",\n "jflex": "Lex",\n "jinja": "Jinja",\n "jinja2": "Jinja",\n "jison": "Yacc",\n "jisonlex": "Lex",\n "jl": "Julia",\n "jq": "JSONiq",\n "js": "JavaScript",\n "js.erb": "JavaScript",\n "jsb": "JavaScript",\n "jscad": "JavaScript",\n "jsfl": "JavaScript",\n "jsh": "Java",\n "jslib": "JavaScript",\n "jsm": "JavaScript",\n "json": "JSON",\n "json-tmlanguage": "JSON",\n "json.example": "JSON",\n "json5": "JSON5",\n "jsonc": "JSON",\n "jsonl": "JSON",\n "jsonld": "JSONLD",\n "jsonnet": "Jsonnet",\n "jsp": "Java",\n "jspre": "JavaScript",\n "jsproj": "XML",\n "jss": "JavaScript",\n "jst": "EJS",\n "jsx": "JavaScript",\n "jte": "Java",\n "just": "Just",\n "k": "KCL",\n "kak": "KakouneScript",\n "kdl": "KDL",\n "kicad_mod": "KiCad Layout",\n "kicad_pcb": "KiCad Layout",\n "kicad_sch": "KiCad Schematic",\n "kicad_sym": "KiCad Schematic",\n "kicad_wks": "KiCad Layout",\n "kid": "Genshi",\n "kk": "Koka",\n "kml": "XML",\n "kojo": "Scala",\n "krl": "KRL",\n "ks": "KerboScript",\n "ksh": "Shell",\n "ksy": "Kaitai Struct",\n "kt": "Kotlin",\n "ktm": "Kotlin",\n "kts": "Kotlin",\n "kv": "kvlang",\n "l": "Common Lisp",\n "lagda": "Agda",\n "langium": "Langium",\n "lark": "Lark",\n "las": "Lasso",\n "lasso": "Lasso",\n "lasso8": "Lasso",\n "lasso9": "Lasso",\n "latte": "Latte",\n "launch": "XML",\n "lbx": "TeX",\n "leex": "HTML",\n "lektorproject": "INI",\n "leo": "Leo",\n "less": "Less",\n "lex": "Lex",\n "lfe": "LFE",\n "lgt": "Logtalk",\n "lhs": "Haskell",\n "libsonnet": "Jsonnet",\n "lid": "Dylan",\n "lidr": "Idris",\n "ligo": "LigoLANG",\n "linq": "C#",\n "liq": "Liquidsoap",\n "liquid": "Liquid",\n "lisp": "Common Lisp",\n "litcoffee": "CoffeeScript",\n "livecodescript": "LiveCode Script",\n "livemd": "Markdown",\n "lkml": "LookML",\n "ll": "LLVM",\n "lmi": "Python",\n "logtalk": "Logtalk",\n "lol": "LOLCODE",\n "lookml": "LookML",\n "lp": "Answer Set Programming",\n "lpr": "Pascal",\n "ls": "LiveScript",\n "lsl": "LSL",\n "lslp": "LSL",\n "lsp": "Common Lisp",\n "ltx": "TeX",\n "lua": "Lua",\n "luau": "Luau",\n "lvclass": "LabVIEW",\n "lvlib": "LabVIEW",\n "lvproj": "LabVIEW",\n "ly": "LilyPond",\n "m": "Objective-C",\n "m2": "Macaulay2",\n "m3": "Modula-3",\n "m3u": "M3U",\n "m3u8": "M3U",\n "ma": "Wolfram Language",\n "mak": "Makefile",\n "make": "Makefile",\n "makefile": "Makefile",\n "mako": "Mako",\n "man": "Roff",\n "mao": "Mako",\n "markdown": "Markdown",\n "marko": "Marko",\n "mask": "Mask",\n "mat": "Unity3D Asset",\n "mata": "Stata",\n "matah": "Stata",\n "mathematica": "Wolfram Language",\n "matlab": "MATLAB",\n "mawk": "Awk",\n "maxhelp": "Max",\n "maxpat": "Max",\n "maxproj": "Max",\n "mbt": "MoonBit",\n "mc": "Monkey C",\n "mcfunction": "mcfunction",\n "mch": "B (Formal Method)",\n "mcmeta": "JSON",\n "mcr": "MAXScript",\n "md": "Markdown",\n "mdoc": "Roff",\n "mdown": "Markdown",\n "mdpolicy": "XML",\n "mdwn": "Markdown",\n "mdx": "MDX",\n "me": "Roff",\n "mediawiki": "Wikitext",\n "mermaid": "Mermaid",\n "meta": "Unity3D Asset",\n "metal": "Metal",\n "metta": "MeTTa",\n "mg": "Modula-3",\n "mint": "Mint",\n "mir": "YAML",\n "mirah": "Mirah",\n "mjml": "XML",\n "mjs": "JavaScript",\n "mk": "Makefile",\n "mkd": "Markdown",\n "mkdn": "Markdown",\n "mkdown": "Markdown",\n "mkfile": "Makefile",\n "mkii": "TeX",\n "mkiv": "TeX",\n "mkvi": "TeX",\n "ml": "OCaml",\n "ml4": "OCaml",\n "mli": "OCaml",\n "mligo": "LigoLANG",\n "mlir": "MLIR",\n "mll": "OCaml",\n "mly": "OCaml",\n "mm": "Objective-C++",\n "mmd": "Mermaid",\n "mo": "Modelica",\n "mod": "Modula-2",\n "mojo": "Mojo",\n "moo": "Mercury",\n "moon": "MoonScript",\n "move": "Move",\n "mpl": "JetBrains MPS",\n "mps": "JetBrains MPS",\n "mq4": "MQL4",\n "mq5": "MQL5",\n "mqh": "MQL4",\n "mrc": "mIRC Script",\n "ms": "MAXScript",\n "msd": "JetBrains MPS",\n "msg": "OMNeT++ MSG",\n "mspec": "Ruby",\n "mt": "Wolfram Language",\n "mtml": "MTML",\n "mts": "TypeScript",\n "mu": "mupad",\n "mud": "ZIL",\n "mustache": "Mustache",\n "mxml": "XML",\n "mxt": "Max",\n "mysql": "SQL",\n "mzn": "MiniZinc",\n "n": "Nemerle",\n "nanorc": "INI",\n "nas": "Nasal",\n "nasm": "Assembly",\n "natvis": "XML",\n "nawk": "Awk",\n "nb": "Wolfram Language",\n "nbp": "Wolfram Language",\n "nc": "nesC",\n "ncl": "NCL",\n "ndproj": "XML",\n "ne": "Nearley",\n "nearley": "Nearley",\n "ned": "OMNeT++ NED",\n "nf": "Nextflow",\n "nginx": "Nginx",\n "nginxconf": "Nginx",\n "nim": "Nim",\n "nim.cfg": "Nim",\n "nimble": "Nim",\n "nimrod": "Nim",\n "nims": "Nim",\n "nit": "Nit",\n "nix": "Nix",\n "njk": "Nunjucks",\n "njs": "JavaScript",\n "nl": "NewLisp",\n "nlogo": "NetLogo",\n "nomad": "HCL",\n "nproj": "XML",\n "nqp": "Raku",\n "nr": "Noir",\n "nse": "Lua",\n "nss": "NWScript",\n "nu": "Nu",\n "numpy": "Python",\n "numpyw": "Python",\n "numsc": "Python",\n "nuspec": "XML",\n "nut": "Squirrel",\n "ny": "Common Lisp",\n "odd": "XML",\n "odin": "Odin",\n "ol": "Jolie",\n "omgrofl": "Omgrofl",\n "ooc": "ooc",\n "opal": "Opal",\n "opencl": "C",\n "orc": "Csound",\n "org": "Org",\n "os": "1C Enterprise",\n "osm": "XML",\n "outjob": "Altium Designer",\n "overpassql": "OverpassQL",\n "owl": "Web Ontology Language",\n "oxygene": "Oxygene",\n "oz": "Oz",\n "p": "OpenEdge ABL",\n "p4": "P4",\n "p6": "Raku",\n "p6l": "Raku",\n "p6m": "Raku",\n "p8": "Lua",\n "pac": "JavaScript",\n "pact": "Pact",\n "pan": "Pan",\n "parrot": "Parrot",\n "pas": "Pascal",\n "pascal": "Pascal",\n "pat": "Max",\n "pb": "PureBasic",\n "pbi": "PureBasic",\n "pbt": "PowerBuilder",\n "pcbdoc": "Altium Designer",\n "pck": "PLSQL",\n "pcss": "CSS",\n "pd_lua": "Lua",\n "pddl": "PDDL",\n "pde": "Processing",\n "peggy": "PEG.js",\n "pegjs": "PEG.js",\n "pep": "Pep8",\n "per": "Genero per",\n "perl": "Perl",\n "pfa": "PostScript",\n "pgsql": "PLpgSQL",\n "ph": "Perl",\n "php": "PHP",\n "php3": "PHP",\n "php4": "PHP",\n "php5": "PHP",\n "phps": "PHP",\n "phpt": "PHP",\n "phtml": "HTML",\n "pig": "PigLatin",\n "pike": "Pike",\n "pkb": "PLSQL",\n "pkgproj": "XML",\n "pkl": "Pkl",\n "pks": "PLSQL",\n "pl": "Perl",\n "pl6": "Raku",\n "plantuml": "PlantUML",\n "plb": "PLSQL",\n "plist": "XML",\n "plot": "Gnuplot",\n "pls": "PLSQL",\n "plsql": "PLSQL",\n "plt": "Gnuplot",\n "pluginspec": "Ruby",\n "plx": "Perl",\n "pm": "Perl",\n "pm6": "Raku",\n "pml": "Promela",\n "pmod": "Pike",\n "podsl": "Common Lisp",\n "podspec": "Ruby",\n "pogo": "PogoScript",\n "polar": "Polar",\n "por": "Portugol",\n "postcss": "CSS",\n "pov": "POV-Ray SDL",\n "pp": "Puppet",\n "pprx": "REXX",\n "praat": "Praat",\n "prawn": "Ruby",\n "prc": "PLSQL",\n "prefab": "Unity3D Asset",\n "prefs": "INI",\n "prg": "xBase",\n "prisma": "Prisma",\n "prjpcb": "Altium Designer",\n "pro": "Prolog",\n "proj": "XML",\n "prolog": "Prolog",\n "properties": "Java Properties",\n "props": "XML",\n "prw": "xBase",\n "ps": "PostScript",\n "ps1": "PowerShell",\n "ps1xml": "XML",\n "psc": "Papyrus",\n "psc1": "XML",\n "psd1": "PowerShell",\n "psgi": "Perl",\n "psm1": "PowerShell",\n "pt": "XML",\n "pubxml": "XML",\n "pug": "Pug",\n "puml": "PlantUML",\n "purs": "PureScript",\n "pwn": "Pawn",\n "pxd": "Cython",\n "pxi": "Cython",\n "py": "Python",\n "py3": "Python",\n "pyde": "Python",\n "pyi": "Python",\n "pyp": "Python",\n "pyt": "Python",\n "pytb": "Python",\n "pyw": "Python",\n "pyx": "Cython",\n "q": "HiveQL",\n "qasm": "OpenQASM",\n "qbs": "QML",\n "qc": "QuakeC",\n "qhelp": "XML",\n "ql": "CodeQL",\n "qll": "CodeQL",\n "qmd": "RMarkdown",\n "qml": "QML",\n "qnt": "Quint",\n "qs": "Q#",\n "r": "R",\n "r2": "Rebol",\n "r3": "Rebol",\n "rabl": "Ruby",\n "rake": "Ruby",\n "raku": "Raku",\n "rakumod": "Raku",\n "raml": "RAML",\n "rascript": "RAScript",\n "razor": "HTML",\n "rb": "Ruby",\n "rbi": "Ruby",\n "rbs": "Ruby",\n "rbuild": "Ruby",\n "rbw": "Ruby",\n "rbx": "Ruby",\n "rbxs": "Lua",\n "rchit": "GLSL",\n "rd": "R",\n "rdf": "XML",\n "rdoc": "RDoc",\n "re": "Reason",\n "reb": "Rebol",\n "rebol": "Rebol",\n "red": "Red",\n "reds": "Red",\n "reek": "YAML",\n "reg": "Windows Registry Entries",\n "regex": "Regular Expression",\n "regexp": "Regular Expression",\n "rego": "Open Policy Agent",\n "rei": "Reason",\n "religo": "LigoLANG",\n "res": "ReScript",\n "resi": "ReScript",\n "resource": "RobotFramework",\n "rest": "reStructuredText",\n "rest.txt": "reStructuredText",\n "resx": "XML",\n "rex": "REXX",\n "rexx": "REXX",\n "rg": "Rouge",\n "rhtml": "HTML",\n "ring": "Ring",\n "riot": "Riot",\n "rkt": "Racket",\n "rktd": "Racket",\n "rktl": "Racket",\n "rl": "Ragel",\n "rmd": "RMarkdown",\n "rmiss": "GLSL",\n "rnh": "RUNOFF",\n "rno": "RUNOFF",\n "rnw": "Sweave",\n "robot": "RobotFramework",\n "roc": "Roc",\n "rockspec": "Lua",\n "roff": "Roff",\n "ron": "RON",\n "ronn": "Markdown",\n "rpgle": "RPGLE",\n "rpy": "Ren\'Py",\n "rq": "SPARQL",\n "rs": "Rust",\n "rs.in": "Rust",\n "rsc": "Rascal",\n "rss": "XML",\n "rst": "reStructuredText",\n "rst.txt": "reStructuredText",\n "rsx": "R",\n "ru": "Ruby",\n "ruby": "Ruby",\n "rviz": "YAML",\n "s": "Assembly",\n "sail": "Sail",\n "sarif": "JSON",\n "sas": "SAS",\n "sass": "Sass",\n "sats": "ATS",\n "sbatch": "Shell",\n "sbt": "Scala",\n "sc": "SuperCollider",\n "scad": "OpenSCAD",\n "scala": "Scala",\n "scaml": "Scaml",\n "scd": "SuperCollider",\n "sce": "Scilab",\n "scenic": "Scenic",\n "sch": "Scheme",\n "schdoc": "Altium Designer",\n "sci": "Scilab",\n "scm": "Scheme",\n "sco": "Csound Score",\n "scpt": "AppleScript",\n "scrbl": "Racket",\n "scss": "SCSS",\n "scxml": "XML",\n "sdc": "Tcl",\n "sed": "sed",\n "self": "Self",\n "sexp": "Common Lisp",\n "sfproj": "XML",\n "sfv": "Simple File Verification",\n "sh": "Shell",\n "sh.in": "Shell",\n "shader": "ShaderLab",\n "shen": "Shen",\n "shproj": "XML",\n "sig": "Standard ML",\n "sj": "Objective-J",\n "sjs": "JavaScript",\n "sl": "Slash",\n "slang": "Slang",\n "sld": "Scheme",\n "slim": "Slim",\n "slint": "Slint",\n "slnx": "XML",\n "sls": "SaltStack",\n "slurm": "Shell",\n "sma": "Pawn",\n "smithy": "Smithy",\n "smk": "Python",\n "sml": "Standard ML",\n "snakefile": "Python",\n "snap": "Jest Snapshot",\n "snip": "Vim Snippet",\n "snippet": "Vim Snippet",\n "snippets": "Vim Snippet",\n "sol": "Solidity",\n "soy": "Closure Templates",\n "sp": "SourcePawn",\n "sparql": "SPARQL",\n "spc": "PLSQL",\n "spec": "Python",\n "spin": "Propeller Spin",\n "sps": "Scheme",\n "sqf": "SQF",\n "sql": "SQL",\n "sqlrpgle": "RPGLE",\n "sra": "PowerBuilder",\n "srdf": "XML",\n "srt": "SRecode Template",\n "sru": "PowerBuilder",\n "srv": "ROS Interface",\n "srw": "PowerBuilder",\n "ss": "Scheme",\n "ssjs": "JavaScript",\n "sss": "SugarSS",\n "st": "Smalltalk",\n "stan": "Stan",\n "star": "Starlark",\n "sthlp": "Stata",\n "stl": "STL",\n "story": "Gherkin",\n "storyboard": "XML",\n "sttheme": "XML",\n "sty": "TeX",\n "styl": "Stylus",\n "sublime-build": "JSON",\n "sublime-color-scheme": "JSON",\n "sublime-commands": "JSON",\n "sublime-completions": "JSON",\n "sublime-keymap": "JSON",\n "sublime-macro": "JSON",\n "sublime-menu": "JSON",\n "sublime-mousemap": "JSON",\n "sublime-project": "JSON",\n "sublime-settings": "JSON",\n "sublime-snippet": "XML",\n "sublime-syntax": "YAML",\n "sublime-theme": "JSON",\n "sublime-workspace": "JSON",\n "sublime_metrics": "JSON",\n "sublime_session": "JSON",\n "surql": "SurrealQL",\n "sv": "SystemVerilog",\n "svelte": "Svelte",\n "svg": "SVG",\n "svh": "SystemVerilog",\n "svx": "mdsvex",\n "sw": "Sway",\n "swift": "Swift",\n "syntax": "YAML",\n "t": "Perl",\n "tab": "SQL",\n "tac": "Python",\n "tact": "Tact",\n "tag": "Java",\n "talon": "Talon",\n "targets": "XML",\n "tcc": "C++",\n "tcl": "Tcl",\n "tcl.in": "Tcl",\n "templ": "templ",\n "tesc": "GLSL",\n "tese": "GLSL",\n "tex": "TeX",\n "textgrid": "TextGrid",\n "textile": "Textile",\n "tf": "HCL",\n "tfstate": "JSON",\n "tfstate.backup": "JSON",\n "tftpl": "HCL",\n "tfvars": "HCL",\n "thor": "Ruby",\n "thrift": "Thrift",\n "thy": "Isabelle",\n "tl": "Teal",\n "tla": "TLA",\n "tlv": "TL-Verilog",\n "tm": "Tcl",\n "tmac": "Roff",\n "tmcommand": "XML",\n "tmdl": "TMDL",\n "tml": "XML",\n "tmlanguage": "XML",\n "tmpl": "Go Template",\n "tmpreferences": "XML",\n "tmsnippet": "XML",\n "tmtheme": "XML",\n "tmux": "Shell",\n "toc": "TeX",\n "tofu": "HCL",\n "toit": "Toit",\n "toml": "TOML",\n "toml.example": "TOML",\n "tool": "Shell",\n "topojson": "JSON",\n "tpb": "PLSQL",\n "tpl": "Smarty",\n "tpp": "C++",\n "tps": "PLSQL",\n "tres": "Godot Resource",\n "trg": "PLSQL",\n "trigger": "Apex",\n "ts": "TypeScript",\n "tscn": "Godot Resource",\n "tsconfig.json": "JSON",\n "tsp": "TypeSpec",\n "tst": "GAP",\n "tsv": "TSV",\n "tsx": "TypeScript",\n "tu": "Turing",\n "twig": "Twig",\n "txl": "TXL",\n "txx": "C++",\n "typ": "Typst",\n "uc": "UnrealScript",\n "udf": "SQL",\n "udo": "Csound",\n "ui": "XML",\n "unity": "Unity3D Asset",\n "uno": "Uno",\n "upc": "C",\n "uplc": "Untyped Plutus Core",\n "ur": "UrWeb",\n "urdf": "XML",\n "url": "INI",\n "urs": "UrWeb",\n "ux": "XML",\n "v": "Verilog",\n "vala": "Vala",\n "vapi": "Vala",\n "vark": "Gosu",\n "vb": "Visual Basic .NET",\n "vba": "VBA",\n "vbhtml": "Visual Basic .NET",\n "vbproj": "XML",\n "vbs": "VBScript",\n "vcf": "vCard",\n "vcl": "VCL",\n "vcxproj": "XML",\n "vdf": "Valve Data Format",\n "veo": "Verilog",\n "vert": "GLSL",\n "vh": "SystemVerilog",\n "vhd": "VHDL",\n "vhdl": "VHDL",\n "vhf": "VHDL",\n "vhi": "VHDL",\n "vho": "VHDL",\n "vhost": "ApacheConf",\n "vhs": "VHDL",\n "vht": "VHDL",\n "vhw": "VHDL",\n "vim": "Vim Script",\n "vimrc": "Vim Script",\n "viw": "SQL",\n "vmb": "Vim Script",\n "volt": "Volt",\n "vrx": "GLSL",\n "vs": "GLSL",\n "vsh": "GLSL",\n "vshader": "GLSL",\n "vsixmanifest": "XML",\n "vssettings": "XML",\n "vstemplate": "XML",\n "vtl": "Velocity Template Language",\n "vto": "Vento",\n "vue": "Vue",\n "vw": "PLSQL",\n "vxml": "XML",\n "vy": "Vyper",\n "w": "CWeb",\n "wast": "WebAssembly",\n "wat": "WebAssembly",\n "watchr": "Ruby",\n "wdl": "WDL",\n "webapp": "JSON",\n "webmanifest": "JSON",\n "wgsl": "WGSL",\n "whiley": "Whiley",\n "wiki": "Wikitext",\n "wikitext": "Wikitext",\n "wisp": "wisp",\n "wit": "WebAssembly Interface Type",\n "wixproj": "XML",\n "wl": "Wolfram Language",\n "wlk": "Wollok",\n "wls": "Wolfram Language",\n "wlt": "Wolfram Language",\n "wlua": "Lua",\n "workbook": "Markdown",\n "workflow": "HCL",\n "wren": "Wren",\n "ws": "Witcher Script",\n "wsdl": "XML",\n "wsf": "XML",\n "wsgi": "Python",\n "wxi": "XML",\n "wxl": "XML",\n "wxs": "XML",\n "x": "DirectX 3D File",\n "x10": "X10",\n "x3d": "XML",\n "x68": "Assembly",\n "xacro": "XML",\n "xaml": "XML",\n "xc": "XC",\n "xdc": "Tcl",\n "xht": "HTML",\n "xhtml": "HTML",\n "xib": "XML",\n "xlf": "XML",\n "xliff": "XML",\n "xmi": "XML",\n "xml": "XML",\n "xml.dist": "XML",\n "xmp": "XML",\n "xojo_code": "Xojo",\n "xojo_menu": "Xojo",\n "xojo_report": "Xojo",\n "xojo_script": "Xojo",\n "xojo_toolbar": "Xojo",\n "xojo_window": "Xojo",\n "xproj": "XML",\n "xpy": "Python",\n "xq": "XQuery",\n "xql": "XQuery",\n "xqm": "XQuery",\n "xquery": "XQuery",\n "xqy": "XQuery",\n "xrl": "Erlang",\n "xsd": "XML",\n "xsh": "Xonsh",\n "xsjs": "JavaScript",\n "xsjslib": "JavaScript",\n "xsl": "XSLT",\n "xslt": "XSLT",\n "xspec": "XML",\n "xtend": "Xtend",\n "xul": "XML",\n "xzap": "ZAP",\n "y": "Yacc",\n "yacc": "Yacc",\n "yaml": "MiniYAML",\n "yaml-tmlanguage": "YAML",\n "yaml.sed": "YAML",\n "yap": "Prolog",\n "yar": "YARA",\n "yara": "YARA",\n "yasnippet": "YASnippet",\n "yml": "YAML",\n "yml.mysql": "YAML",\n "yrl": "Erlang",\n "yul": "Yul",\n "yy": "Yacc",\n "yyp": "JSON",\n "zap": "ZAP",\n "zcml": "XML",\n "zed": "SpiceDB Schema",\n "zep": "Zephir",\n "zig": "Zig",\n "zig.zon": "Zig",\n "zil": "ZIL",\n "zimpl": "Zimpl",\n "zmodel": "Zmodel",\n "zmpl": "Zimpl",\n "zpl": "Zimpl",\n "zs": "ZenScript",\n "zsh": "Shell",\n "zsh-theme": "Shell"\n },\n "filename": {\n ".abbrev_defs": "Emacs Lisp",\n ".ackrc": "Option List",\n ".all-contributorsrc": "JSON",\n ".arcconfig": "JSON",\n ".atomignore": "Ignore List",\n ".auto-changelog": "JSON",\n ".babelignore": "Ignore List",\n ".babelrc": "JSON",\n ".bash_aliases": "Shell",\n ".bash_functions": "Shell",\n ".bash_history": "Shell",\n ".bash_logout": "Shell",\n ".bash_profile": "Shell",\n ".bashrc": "Shell",\n ".browserslistrc": "Browserslist",\n ".buckconfig": "INI",\n ".bzrignore": "Ignore List",\n ".c8rc": "JSON",\n ".clang-format": "YAML",\n ".clang-tidy": "YAML",\n ".clangd": "YAML",\n ".classpath": "XML",\n ".coffeelintignore": "Ignore List",\n ".coveragerc": "INI",\n ".cproject": "XML",\n ".cshrc": "Shell",\n ".cvsignore": "Ignore List",\n ".devcontainer.json": "JSON",\n ".dockerignore": "Ignore List",\n ".easignore": "Ignore List",\n ".editorconfig": "INI",\n ".eleventyignore": "Ignore List",\n ".emacs": "Emacs Lisp",\n ".emacs.desktop": "Emacs Lisp",\n ".env": "Dotenv",\n ".env.ci": "Dotenv",\n ".env.dev": "Dotenv",\n ".env.development": "Dotenv",\n ".env.development.local": "Dotenv",\n ".env.example": "Dotenv",\n ".env.local": "Dotenv",\n ".env.prod": "Dotenv",\n ".env.production": "Dotenv",\n ".env.sample": "Dotenv",\n ".env.staging": "Dotenv",\n ".env.template": "Dotenv",\n ".env.test": "Dotenv",\n ".env.testing": "Dotenv",\n ".envrc": "Shell",\n ".eslintignore": "Ignore List",\n ".eslintrc.json": "JSON",\n ".exrc": "Vim Script",\n ".factor-boot-rc": "Factor",\n ".factor-rc": "Factor",\n ".flake8": "INI",\n ".flaskenv": "Shell",\n ".gclient": "Python",\n ".gemrc": "YAML",\n ".git-blame-ignore-revs": "Git Revision List",\n ".gitattributes": "Git Attributes",\n ".gitconfig": "INI",\n ".gitignore": "Ignore List",\n ".gitmodules": "INI",\n ".gnus": "Emacs Lisp",\n ".gvimrc": "Vim Script",\n ".htaccess": "ApacheConf",\n ".htmlhintrc": "JSON",\n ".ignore": "Ignore List",\n ".imgbotconfig": "JSON",\n ".irbrc": "Ruby",\n ".jscsrc": "JSON",\n ".jshintrc": "JSON",\n ".jslintrc": "JSON",\n ".justfile": "Just",\n ".kshrc": "Shell",\n ".latexmkrc": "Perl",\n ".login": "Shell",\n ".luacheckrc": "Lua",\n ".markdownlintignore": "Ignore List",\n ".nanorc": "INI",\n ".nodemonignore": "Ignore List",\n ".npmignore": "Ignore List",\n ".npmrc": "INI",\n ".nvimrc": "Vim Script",\n ".nycrc": "JSON",\n ".oxlintrc.json": "JSON",\n ".php": "PHP",\n ".php_cs": "PHP",\n ".php_cs.dist": "PHP",\n ".prettierignore": "Ignore List",\n ".profile": "Shell",\n ".project": "XML",\n ".pryrc": "Ruby",\n ".pylintrc": "INI",\n ".rprofile": "R",\n ".rspec": "Option List",\n ".scalafix.conf": "HOCON",\n ".scalafmt.conf": "HOCON",\n ".shellcheckrc": "ShellCheck Config",\n ".simplecov": "Ruby",\n ".spacemacs": "Emacs Lisp",\n ".stylelintignore": "Ignore List",\n ".swcrc": "JSON",\n ".tern-config": "JSON",\n ".tern-project": "JSON",\n ".tm_properties": "TextMate Properties",\n ".tmux.conf": "Shell",\n ".vercelignore": "Ignore List",\n ".vimrc": "Vim Script",\n ".viper": "Emacs Lisp",\n ".vscodeignore": "Ignore List",\n ".watchmanconfig": "JSON",\n ".xinitrc": "Shell",\n ".xsession": "Shell",\n ".yardopts": "Option List",\n ".zlogin": "Shell",\n ".zlogout": "Shell",\n ".zprofile": "Shell",\n ".zshenv": "Shell",\n ".zshrc": "Shell",\n "9fs": "Shell",\n "_emacs": "Emacs Lisp",\n "_helpers.tpl": "Go Template",\n "_vimrc": "Vim Script",\n "abbrev_defs": "Emacs Lisp",\n "ack": "Perl",\n "ackrc": "Option List",\n "ant.xml": "Ant Build System",\n "apache2.conf": "ApacheConf",\n "api-extractor.json": "JSON",\n "apkbuild": "Shell",\n "app.config": "XML",\n "appraisals": "Ruby",\n "bash_aliases": "Shell",\n "bash_logout": "Shell",\n "bash_profile": "Shell",\n "bashrc": "Shell",\n "berksfile": "Ruby",\n "brewfile": "Ruby",\n "browserslist": "Browserslist",\n "bsdmakefile": "Makefile",\n "buck": "Starlark",\n "build": "Starlark",\n "build.bazel": "Starlark",\n "build.xml": "Ant Build System",\n "buildfile": "Ruby",\n "buildozer.spec": "INI",\n "bun.lock": "JSON",\n "cabal.config": "Cabal Config",\n "cabal.project": "Cabal Config",\n "caddyfile": "Caddyfile",\n "cakefile": "CoffeeScript",\n "capfile": "Ruby",\n "cargo.lock": "TOML",\n "cargo.toml.orig": "TOML",\n "cask": "Emacs Lisp",\n "citation.cff": "YAML",\n "cmakelists.txt": "CMake",\n "commit_editmsg": "Git Commit",\n "composer.lock": "JSON",\n "containerfile": "Dockerfile",\n "contents.lr": "Markdown",\n "cpanfile": "Perl",\n "crontab": "crontab",\n "cshrc": "Shell",\n "dangerfile": "Ruby",\n "deliverfile": "Ruby",\n "deno.lock": "JSON",\n "deps": "Python",\n "dev-requirements.txt": "Pip Requirements",\n "devcontainer.json": "JSON",\n "dockerfile": "Dockerfile",\n "dune-project": "Dune",\n "earthfile": "Earthly",\n "eask": "Emacs Lisp",\n "emakefile": "Erlang",\n "eqnrc": "Roff",\n "expr-dist": "R",\n "fakefile": "Fancy",\n "fastfile": "Ruby",\n "firestore.rules": "Cloud Firestore Security Rules",\n "flake.lock": "JSON",\n "fp-lib-table": "KiCad Layout",\n "gemfile": "Ruby",\n "gemfile.lock": "Gemfile.lock",\n "gitignore-global": "Ignore List",\n "gitignore_global": "Ignore List",\n "glide.lock": "YAML",\n "gnumakefile": "Makefile",\n "go.mod": "Go Module",\n "go.sum": "Go Checksums",\n "go.work": "Go Workspace",\n "go.work.sum": "Go Checksums",\n "gopkg.lock": "TOML",\n "gradlew": "Shell",\n "gradlew.bat": "Batchfile",\n "guardfile": "Ruby",\n "gvimrc": "Vim Script",\n "haproxy.cfg": "HAProxy",\n "hosts": "Hosts File",\n "hosts.txt": "Hosts File",\n "httpd.conf": "ApacheConf",\n "installscript.qs": "Qt Script",\n "jakefile": "JavaScript",\n "jarfile": "Ruby",\n "jenkinsfile": "Groovy",\n "jsconfig.json": "JSON",\n "justfile": "Just",\n "kakrc": "KakouneScript",\n "kbuild": "Makefile",\n "kcl.mod": "KCL",\n "kcl.mod.lock": "KCL",\n "kshrc": "Shell",\n "language-configuration.json": "JSON",\n "language-subtag-registry.txt": "Record Jar",\n "latexmkrc": "Perl",\n "lexer.x": "Lex",\n "login": "Shell",\n "m3makefile": "Quake",\n "m3overrides": "Quake",\n "makefile": "Makefile",\n "makefile.am": "Makefile",\n "makefile.boot": "Makefile",\n "makefile.frag": "Makefile",\n "makefile.in": "Makefile",\n "makefile.inc": "Makefile",\n "makefile.pl": "Perl",\n "makefile.sco": "Makefile",\n "makefile.wat": "Makefile",\n "man": "Shell",\n "manifest.mf": "JAR Manifest",\n "mavenfile": "Ruby",\n "mcmod.info": "JSON",\n "meson.build": "Meson",\n "meson_options.txt": "Meson",\n "mise.local.lock": "TOML",\n "mise.lock": "TOML",\n "mix.lock": "Elixir",\n "mkfile": "Makefile",\n "mmn": "Roff",\n "mmt": "Roff",\n "mocha.opts": "Option List",\n "module.bazel": "Starlark",\n "module.bazel.lock": "JSON",\n "modulefile": "Puppet",\n "mvnw": "Shell",\n "mvnw.cmd": "Batchfile",\n "nanorc": "INI",\n "nextflow.config": "Nextflow",\n "nginx.conf": "Nginx",\n "nim.cfg": "Nim",\n "notebook": "Jupyter Notebook",\n "nuget.config": "XML",\n "nukefile": "Nu",\n "nvimrc": "Vim Script",\n "owh": "Tcl",\n "package.resolved": "JSON",\n "packages.config": "XML",\n "pdm.lock": "TOML",\n "phakefile": "PHP",\n "pipfile": "TOML",\n "pipfile.lock": "JSON",\n "pixi.lock": "YAML",\n "pkgbuild": "Shell",\n "podfile": "Ruby",\n "poetry.lock": "TOML",\n "procfile": "Procfile",\n "profile": "Shell",\n "project.ede": "Emacs Lisp",\n "project.godot": "Godot Resource",\n "puppetfile": "Ruby",\n "pylintrc": "INI",\n "rakefile": "Ruby",\n "rebar.config": "Erlang",\n "rebar.config.lock": "Erlang",\n "rebar.lock": "Erlang",\n "requirements-dev.txt": "Pip Requirements",\n "requirements.lock.txt": "Pip Requirements",\n "requirements.txt": "Pip Requirements",\n "rexfile": "Perl",\n "riemann.config": "Clojure",\n "root": "Isabelle",\n "sconscript": "Python",\n "sconstruct": "Python",\n "settings.stylecop": "XML",\n "singularity": "Singularity",\n "slakefile": "LiveScript",\n "snakefile": "Python",\n "snapfile": "Ruby",\n "starfield": "Tcl",\n "steepfile": "Ruby",\n "suite.rc": "INI",\n "thorfile": "Ruby",\n "tiltfile": "Starlark",\n "tmux.conf": "Shell",\n "toolchain_installscript.qs": "Qt Script",\n "torrc": "Tor Config",\n "troffrc": "Roff",\n "troffrc-end": "Roff",\n "tsconfig.json": "JSON",\n "tslint.json": "JSON",\n "uv.lock": "TOML",\n "vagrantfile": "Ruby",\n "vimrc": "Vim Script",\n "vlcrc": "INI",\n "web.config": "XML",\n "web.debug.config": "XML",\n "web.release.config": "XML",\n "workspace": "Starlark",\n "workspace.bazel": "Starlark",\n "workspace.bzlmod": "Starlark",\n "wscript": "Python",\n "xinitrc": "Shell",\n "xmake.lua": "Xmake",\n "xsession": "Shell",\n "yarn.lock": "YAML",\n "zlogin": "Shell",\n "zlogout": "Shell",\n "zprofile": "Shell",\n "zshenv": "Shell",\n "zshrc": "Shell"\n }\n },\n "vendor": [\n "(^|/)cache/",\n "^[Dd]ependencies/",\n "(^|/)dist/",\n "^deps/",\n "(^|/)configure$",\n "(^|/)config\\\\.guess$",\n "(^|/)config\\\\.sub$",\n "(^|/)aclocal\\\\.m4",\n "(^|/)libtool\\\\.m4",\n "(^|/)ltoptions\\\\.m4",\n "(^|/)ltsugar\\\\.m4",\n "(^|/)ltversion\\\\.m4",\n "(^|/)lt~obsolete\\\\.m4",\n "(^|/)dotnet-install\\\\.(ps1|sh)$",\n "(^|/)cpplint\\\\.py",\n "(^|/)node_modules/",\n "(^|/)\\\\.yarn/releases/",\n "(^|/)\\\\.yarn/plugins/",\n "(^|/)\\\\.yarn/sdks/",\n "(^|/)\\\\.yarn/versions/",\n "(^|/)\\\\.yarn/unplugged/",\n "(^|/)_esy$",\n "(^|/)bower_components/",\n "^rebar$",\n "(^|/)erlang\\\\.mk",\n "(^|/)Godeps/_workspace/",\n "(^|/)testdata/",\n "(^|/)\\\\.indent\\\\.pro",\n "(\\\\.|-)min\\\\.(js|css)$",\n "([^\\\\s]*)import\\\\.(css|less|scss|styl)$",\n "(^|/)bootstrap([^/.]*)(\\\\..*)?\\\\.(js|css|less|scss|styl)$",\n "(^|/)custom\\\\.bootstrap([^\\\\s]*)(js|css|less|scss|styl)$",\n "(^|/)font-?awesome\\\\.(css|less|scss|styl)$",\n "(^|/)font-?awesome/.*\\\\.(css|less|scss|styl)$",\n "(^|/)foundation\\\\.(css|less|scss|styl)$",\n "(^|/)normalize\\\\.(css|less|scss|styl)$",\n "(^|/)skeleton\\\\.(css|less|scss|styl)$",\n "(^|/)[Bb]ourbon/.*\\\\.(css|less|scss|styl)$",\n "(^|/)animate\\\\.(css|less|scss|styl)$",\n "(^|/)materialize\\\\.(css|less|scss|styl|js)$",\n "(^|/)select2/.*\\\\.(css|scss|js)$",\n "(^|/)bulma\\\\.(css|sass|scss)$",\n "(3rd|[Tt]hird)[-_]?[Pp]arty/",\n "(^|/)vendors?/",\n "(^|/)[Ee]xtern(als?)?/",\n "(^|/)[Vv]+endor/",\n "^debian/",\n "(^|/)run\\\\.n$",\n "(^|/)bootstrap-datepicker/",\n "(^|/)jquery([^.]*)\\\\.js$",\n "(^|/)jquery\\\\-\\\\d\\\\.\\\\d+(\\\\.\\\\d+)?\\\\.js$",\n "(^|/)jquery\\\\-ui(\\\\-\\\\d\\\\.\\\\d+(\\\\.\\\\d+)?)?(\\\\.\\\\w+)?\\\\.(js|css)$",\n "(^|/)jquery\\\\.(ui|effects)\\\\.([^.]*)\\\\.(js|css)$",\n "(^|/)jquery\\\\.fn\\\\.gantt\\\\.js",\n "(^|/)jquery\\\\.fancybox\\\\.(js|css)",\n "(^|/)fuelux\\\\.js",\n "(^|/)jquery\\\\.fileupload(-\\\\w+)?\\\\.js$",\n "(^|/)jquery\\\\.dataTables\\\\.js",\n "(^|/)bootbox\\\\.js",\n "(^|/)pdf\\\\.worker\\\\.js",\n "(^|/)slick\\\\.\\\\w+.js$",\n "(^|/)Leaflet\\\\.Coordinates-\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.src\\\\.js$",\n "(^|/)leaflet\\\\.draw-src\\\\.js",\n "(^|/)leaflet\\\\.draw\\\\.css",\n "(^|/)Control\\\\.FullScreen\\\\.css",\n "(^|/)Control\\\\.FullScreen\\\\.js",\n "(^|/)leaflet\\\\.spin\\\\.js",\n "(^|/)wicket-leaflet\\\\.js",\n "(^|/)\\\\.sublime-project",\n "(^|/)\\\\.sublime-workspace",\n "(^|/)\\\\.vscode/",\n "(^|/)prototype(.*)\\\\.js$",\n "(^|/)effects\\\\.js$",\n "(^|/)controls\\\\.js$",\n "(^|/)dragdrop\\\\.js$",\n "(.*?)\\\\.d\\\\.ts$",\n "(^|/)mootools([^.]*)\\\\d+\\\\.\\\\d+.\\\\d+([^.]*)\\\\.js$",\n "(^|/)dojo\\\\.js$",\n "(^|/)MochiKit\\\\.js$",\n "(^|/)yahoo-([^.]*)\\\\.js$",\n "(^|/)yui([^.]*)\\\\.js$",\n "(^|/)ckeditor\\\\.js$",\n "(^|/)tiny_mce([^.]*)\\\\.js$",\n "(^|/)tiny_mce/(langs|plugins|themes|utils)",\n "(^|/)ace-builds/",\n "(^|/)fontello(.*?)\\\\.css$",\n "(^|/)MathJax/",\n "(^|/)Chart\\\\.js$",\n "(^|/)[Cc]ode[Mm]irror/(\\\\d+\\\\.\\\\d+/)?(lib|mode|theme|addon|keymap|demo)",\n "(^|/)shBrush([^.]*)\\\\.js$",\n "(^|/)shCore\\\\.js$",\n "(^|/)shLegacy\\\\.js$",\n "(^|/)angular([^.]*)\\\\.js$",\n "(^|\\\\/)d3(\\\\.v\\\\d+)?([^.]*)\\\\.js$",\n "(^|/)react(-[^.]*)?\\\\.js$",\n "(^|/)flow-typed/.*\\\\.js$",\n "(^|/)modernizr\\\\-\\\\d\\\\.\\\\d+(\\\\.\\\\d+)?\\\\.js$",\n "(^|/)modernizr\\\\.custom\\\\.\\\\d+\\\\.js$",\n "(^|/)knockout-(\\\\d+\\\\.){3}(debug\\\\.)?js$",\n "(^|/)docs?/_?(build|themes?|templates?|static)/",\n "(^|/)admin_media/",\n "(^|/)env/",\n "(^|/)fabfile\\\\.py$",\n "(^|/)waf$",\n "(^|/)\\\\.osx$",\n "\\\\.xctemplate/",\n "\\\\.imageset/",\n "(^|/)Carthage/",\n "(^|/)Sparkle/",\n "(^|/)Crashlytics\\\\.framework/",\n "(^|/)Fabric\\\\.framework/",\n "(^|/)BuddyBuildSDK\\\\.framework/",\n "(^|/)Realm\\\\.framework",\n "(^|/)RealmSwift\\\\.framework",\n "(^|/)\\\\.gitattributes$",\n "(^|/)\\\\.gitignore$",\n "(^|/)\\\\.gitmodules$",\n "(^|/)gradlew$",\n "(^|/)gradlew\\\\.bat$",\n "(^|/)gradle/wrapper/",\n "(^|/)mvnw$",\n "(^|/)mvnw\\\\.cmd$",\n "(^|/)\\\\.mvn/wrapper/",\n "-vsdoc\\\\.js$",\n "\\\\.intellisense\\\\.js$",\n "(^|/)jquery([^.]*)\\\\.validate(\\\\.unobtrusive)?\\\\.js$",\n "(^|/)jquery([^.]*)\\\\.unobtrusive\\\\-ajax\\\\.js$",\n "(^|/)[Mm]icrosoft([Mm]vc)?([Aa]jax|[Vv]alidation)(\\\\.debug)?\\\\.js$",\n "(^|/)[Pp]ackages\\\\/.+\\\\.\\\\d+\\\\/",\n "(^|/)extjs/.*?\\\\.js$",\n "(^|/)extjs/.*?\\\\.xml$",\n "(^|/)extjs/.*?\\\\.txt$",\n "(^|/)extjs/.*?\\\\.html$",\n "(^|/)extjs/.*?\\\\.properties$",\n "(^|/)extjs/\\\\.sencha/",\n "(^|/)extjs/docs/",\n "(^|/)extjs/builds/",\n "(^|/)extjs/cmd/",\n "(^|/)extjs/examples/",\n "(^|/)extjs/locale/",\n "(^|/)extjs/packages/",\n "(^|/)extjs/plugins/",\n "(^|/)extjs/resources/",\n "(^|/)extjs/src/",\n "(^|/)extjs/welcome/",\n "(^|/)html5shiv\\\\.js$",\n "(^|/)[Tt]ests?/fixtures/",\n "(^|/)[Ss]pecs?/fixtures/",\n "(^|/)cordova([^.]*)\\\\.js$",\n "(^|/)cordova\\\\-\\\\d\\\\.\\\\d(\\\\.\\\\d)?\\\\.js$",\n "(^|/)foundation(\\\\..*)?\\\\.js$",\n "(^|/)Vagrantfile$",\n "(^|/)\\\\.[Dd][Ss]_[Ss]tore$",\n "(^|/)inst/extdata/",\n "(^|/)octicons\\\\.css",\n "(^|/)sprockets-octicons\\\\.scss",\n "(^|/)activator$",\n "(^|/)activator\\\\.bat$",\n "(^|/)proguard\\\\.pro$",\n "(^|/)proguard-rules\\\\.pro$",\n "(^|/)puphpet/",\n "(^|/)\\\\.google_apis/",\n "(^|/)Jenkinsfile$",\n "(^|/)\\\\.gitpod\\\\.Dockerfile$",\n "(^|/)\\\\.github/",\n "(^|/)\\\\.obsidian/",\n "(^|/)\\\\.teamcity/",\n "(^|/)xvba_modules/"\n ]\n}' -OTHER_LANG = "Other" -OTHER_COLOR = "#8b949e" - - -def _load_techdata(): - raw = TECHDATA - if raw == "__TECHDATA_PLACEHOLDER__": - sibling = Path(__file__).resolve().parent / "techdata.json" - if not sibling.exists(): - return {} - try: - return json.loads(sibling.read_text()) - except (json.JSONDecodeError, OSError): - return {} - try: - return json.loads(raw) - except (json.JSONDecodeError, ValueError): - return {} - - -_TECH = _load_techdata() -_LANG = _TECH.get("lang", {}) -EXT_LANG = _LANG.get("ext", {}) # extension (no dot, lower) -> language -FILENAME_LANG = _LANG.get("filename", {}) # lowercased filename -> language -NAME_COLOR = _LANG.get("color", {}) # language -> hex color -FW_DEPS = _TECH.get("fw_deps", {}) # {ecosystem: {dependency: framework}} -FW_SENTINELS_JS = _TECH.get("fw_sentinels_js", []) # [[basename, framework]] -FW_SENTINELS_OTHER = _TECH.get("fw_sentinels_other", []) # [[path, framework, lang]] - - -def _compile_vendor(patterns): - """One matcher from Linguist's vendor.yml regexes; skips Python-incompatible - ones (they're Ruby-flavored) so the union still compiles.""" - good = [] - for p in patterns: - try: - re.compile(p) - good.append(p) - except re.error: - continue - try: - return re.compile("|".join(f"(?:{p})" for p in good)) if good else None - except re.error: - return None - - -_VENDOR_RE = _compile_vendor(_TECH.get("vendor", [])) - -# Lockfiles Linguist classifies as *generated* (handled in code, not vendor.yml) -# — kept as a small supplement so they don't dominate the language bar. -NOISE_BASENAMES = frozenset({ - "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json", - "composer.lock", "cargo.lock", "gemfile.lock", "poetry.lock", "go.sum", - "pdm.lock", "uv.lock", "flake.lock", -}) - -# Shebang interpreter → language, for extensionless scripts Linguist can't name -# from a path alone (e.g. `bin/deploy` with `#!/usr/bin/env bash`). A small -# curated map mirroring Linguist's `interpreters:`; trailing version digits are -# stripped (`python3` → `python`) before lookup. Names must be real Linguist -# languages so they pick up a color. -SHEBANG_LANG = { - "sh": "Shell", "bash": "Shell", "zsh": "Shell", "dash": "Shell", - "ksh": "Shell", "fish": "fish", "python": "Python", "ruby": "Ruby", - "node": "JavaScript", "perl": "Perl", "awk": "Awk", "gawk": "Awk", - "lua": "Lua", "php": "PHP", "rscript": "R", "tclsh": "Tcl", - "groovy": "Groovy", "osascript": "AppleScript", -} - - -def shebang_lang(first_line): - """Language for a `#!…` first line, or None. Resolves `env interp` and pins - `python3`→Python by stripping trailing version digits from the interpreter.""" - if not first_line.startswith("#!"): - return None - interp = None - for tok in first_line[2:].split(): - name = tok.rsplit("/", 1)[-1] - if name != "env": # skip the `env` in `#!/usr/bin/env python3` - interp = name - break - if not interp: - return None - interp = interp.lower() - return SHEBANG_LANG.get(interp) or SHEBANG_LANG.get(interp.rstrip("0123456789")) - - -def numstat_newpath(field): - """Resolve a numstat path column to the post-rename path. - - Renames render as `old => new`, or with a shared brace group like - `src/{old => new}/file.js`; plain paths pass through unchanged. - """ - if " => " not in field: - return field - lo = field.find("{") - hi = field.find("}", lo) if lo != -1 else -1 - if lo != -1 and hi != -1 and " => " in field[lo:hi]: - new = field[lo + 1:hi].split(" => ", 1)[1] - return field[:lo] + new + field[hi + 1:] - return field.split(" => ", 1)[1] - - -def classify_path(field, present=None, shebang=None): - """Map a numstat path column to a language name, or None to exclude it. - - `present`: when given, the set of paths at HEAD — files absent from it - (deleted since, or renamed away) are excluded so the bar reflects the repo - as it stands, not churn against files that no longer exist. - `shebang`: {path: language} for extensionless/unknown scripts a `#!` line - identified, so they land in their real language instead of "Other". - """ - path = numstat_newpath(field.strip().strip('"')).replace("\\", "/") - if present is not None and path not in present: - return None # file no longer exists at HEAD — count only survivors - if _VENDOR_RE and _VENDOR_RE.search(path): # Linguist vendored paths - return None - base = path.rsplit("/", 1)[-1].lower() - if base in NOISE_BASENAMES: - return None - if base.endswith((".min.js", ".min.css", ".map")): - return None - if base in FILENAME_LANG: # Dockerfile, Makefile, Rakefile, … - return FILENAME_LANG[base] - dot = base.rfind(".") - if dot > 0: - lang = EXT_LANG.get(base[dot + 1:]) - if lang: - return lang - if shebang and path in shebang: # extensionless/unknown but has a #! line - return shebang[path] - return OTHER_LANG - - -def top_languages(langs, limit=6): - """Build a sorted language-bar list from {name: [added, deleted, files]}. - - Ranks by lines touched (added + deleted); languages past `limit` collapse - into a single grey "Other" segment. Returns [] when nothing qualifies. - """ - items = [(name, a + d, files) for name, (a, d, files) in langs.items()] - total = sum(lines for _, lines, _ in items) - if total <= 0: - return [] - items.sort(key=lambda x: x[1], reverse=True) - out = [ - { - "name": name, - "lines": lines, - "files": files, - "pct": round(lines * 100 / total, 1), - "color": NAME_COLOR.get(name, OTHER_COLOR), - } - for name, lines, files in items[:limit] - ] - overflow = sum(lines for _, lines, _ in items[limit:]) - if overflow > 0: - existing = next((o for o in out if o["name"] == OTHER_LANG), None) - if existing: - existing["lines"] += overflow - existing["pct"] = round(existing["lines"] * 100 / total, 1) - else: - out.append({ - "name": OTHER_LANG, - "lines": overflow, - "files": 0, - "pct": round(overflow * 100 / total, 1), - "color": OTHER_COLOR, - }) - return out - - -def git(*args, cwd=None, quiet=False): - # quiet=True hides git's stderr — for best-effort probes that are expected - # to fail (e.g. work-tree-only commands run against a bare clone). - return subprocess.check_output( - ["git", *args], - text=True, - cwd=cwd, - stderr=subprocess.DEVNULL if quiet else None, - ) - - -def _git_show(path, cwd=None): - """Contents of `path` at HEAD, or "" if missing. Works on bare clones.""" - try: - return git("show", f"HEAD:{path}", cwd=cwd) - except subprocess.CalledProcessError: - return "" - - -def _head_first_line(path, cwd=None): - """First line of `path` at HEAD, decoded leniently, or "". Reads bytes so a - stray binary doesn't crash the utf-8 decode `git(text=True)` would attempt.""" - try: - out = subprocess.run( - ["git", "show", f"HEAD:{path}"], cwd=cwd, capture_output=True - ).stdout - except OSError: - return "" - nl = out.find(b"\n") - return (out if nl < 0 else out[:nl]).decode("utf-8", "replace") - - -def detect_frameworks(paths, cwd=None): - """Detect frameworks at HEAD from a local repo / bare clone. - - `paths`: the HEAD tree (repo-relative), already listed by the caller. - Returns a list grouped by language, ordered by framework count: - [{"language": "TypeScript", "color": "#3178c6", "names": [...]}, ...] - Best-effort and local-only — the GraphQL remote path skips this. - """ - return _frameworks_from_files(paths, lambda p: _git_show(p, cwd)) - - -def _frameworks_from_files(paths, read_file): - """Core framework detection over a file list, driven by techdata maps. - - `paths`: repo-relative paths that exist. `read_file(path)` -> contents - ("" if unavailable; only called for manifests worth parsing). Decoupled - from git so the remote path can supply GraphQL-fetched blobs. - """ - if not FW_DEPS: - return [] - paths = set(paths) - by_base = defaultdict(list) - for p in paths: - by_base[p.rsplit("/", 1)[-1].lower()].append(p) - - found = defaultdict(list) - seen = defaultdict(set) - - def add(language, name): - if name and name not in seen[language]: - seen[language].add(name) - found[language].append(name) - - def present(dep, text): - return re.search(r"(?= 3: - field = cols[2] - if field in lang_cache: - lang = lang_cache[field] - else: - lang = classify_path(field, present=present, shebang=shebang) - lang_cache[field] = lang - if lang: - rec = lang_stats.setdefault(cur, {}).setdefault(lang, [0, 0, 0]) - rec[0] += added - rec[1] += deleted - rec[2] += 1 - - default_branch = detect_default_branch(cwd=cwd) - extras = {"lang_stats": lang_stats, "frameworks": detect_frameworks(present, cwd=cwd)} - return ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - {}, - {}, - default_branch, - repo_disk_kb(cwd=cwd), - collect_local_tags(cwd=cwd), - extras, - ) - - -def gh_graphql(query, variables, token): - """POST a GraphQL query to api.github.com. Returns the parsed JSON body.""" - payload = json.dumps({"query": query, "variables": variables}).encode() - req = urllib.request.Request( - "https://api.github.com/graphql", - data=payload, - headers={ - "Authorization": f"bearer {token}", - "Content-Type": "application/json", - "User-Agent": "repo-intel", - }, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - - -# GitHub returns these transient statuses when its GraphQL backend is -# overloaded or times out; they're worth retrying. -RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504}) - -# Plan for a single Commit.history page: (page_size, seconds_to_wait_first). -# Resolving Commit.history makes GitHub compute per-commit diff stats -# (additions/deletions), so a page holding a few large commits can blow past -# its backend timeout and return 502 — deterministically, at the same cursor. -# Shrinking `first` cuts the per-request work; the backoff rides out flakiness. -HISTORY_FETCH_PLAN = ( - (100, 0), - (100, 2), - (25, 4), - (25, 8), - (10, 15), -) - - -def fetch_history_page(query, variables, token, label): - """gh_graphql for a Commit.history page, retrying transient 5xx with - backoff and a shrinking page size. Raises the last error if all attempts - fail. `variables` must omit `pageSize` — it is injected per attempt.""" - last_exc = None - for page_size, sleep_s in HISTORY_FETCH_PLAN: - if sleep_s: - time.sleep(sleep_s) - try: - return gh_graphql(query, {**variables, "pageSize": page_size}, token) - except urllib.error.HTTPError as exc: - if exc.code not in RETRYABLE_STATUS: - raise - last_exc, detail = exc, f"HTTP {exc.code}" - except urllib.error.URLError as exc: - last_exc, detail = exc, str(exc.reason) - print( - f" warning: {label} page (size {page_size}) failed: {detail}", - file=sys.stderr, - ) - raise last_exc - - -def gh_repository(body): - """Extract data.repository defensively — GraphQL returns null on errors.""" - return (body.get("data") or {}).get("repository") or {} - - -def probe_remote_total(owner, repo, token): - """Total commits on the default branch via GraphQL; None on error.""" - query = """ -query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - defaultBranchRef { - target { ... on Commit { history(first: 1) { totalCount } } } - } - } -} -""".strip() - try: - body = gh_graphql(query, {"owner": owner, "repo": repo}, token) - except urllib.error.URLError: - return None - if "errors" in body: - return None - repo_node = gh_repository(body) - branch = repo_node.get("defaultBranchRef") or {} - target = branch.get("target") or {} - history = target.get("history") or {} - total = history.get("totalCount") - return total if isinstance(total, int) else None - - -def get_github_token(): - try: - token = subprocess.check_output( - ["gh", "auth", "token", "-h", "github.com"], - text=True, - stderr=subprocess.DEVNULL, - ).strip() - except (subprocess.CalledProcessError, FileNotFoundError): - token = "" - return token or os.environ.get("GITHUB_TOKEN") or None - - -def fetch_logins_for_commits(owner, repo, oids_by_email, token): - """Look up GitHub author login for each email using a few sample oids. - - Used in the local-repo path where the commit walk doesn't know logins. - Local commits not yet pushed return null, so multiple oids per email are - queried in one batched GraphQL call; the first that resolves wins. - Returns {email: login}. - """ - if not oids_by_email or not token: - return {} - aliases = [] - oid_values = [] - for email, oids in oids_by_email.items(): - for oid in oids: - aliases.append((len(oid_values), email)) - oid_values.append(oid) - if not aliases: - return {} - var_decls = ", ".join(f"$oid{i}: GitObjectID!" for i in range(len(oid_values))) - fragments = " ".join( - f"c{i}: object(oid: $oid{i}) {{ ... on Commit {{ author {{ user {{ login }} }} }} }}" - for i in range(len(oid_values)) - ) - query = ( - f"query($owner: String!, $repo: String!, {var_decls}) " - f"{{ repository(owner: $owner, name: $repo) {{ {fragments} }} }}" - ) - variables = {"owner": owner, "repo": repo} - for i, oid in enumerate(oid_values): - variables[f"oid{i}"] = oid - - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: login lookup failed: {exc}", file=sys.stderr) - return {} - if "errors" in body: - print(f" warning: login lookup errors: {body['errors']}", file=sys.stderr) - repo_node = gh_repository(body) - out = {} - for i, email in aliases: - if email in out: - continue - node = repo_node.get(f"c{i}") or {} - user = ((node.get("author") or {}).get("user")) or {} - login = user.get("login") - if login: - out[email] = login - return out - - -def fetch_user_profiles(logins, token): - """Fetch GitHub profile fields for `logins` in one aliased GraphQL query. - - Returns {login: {login,name,bio,location,websiteUrl,followers,following,publicRepos}}. - Missing/renamed users are silently skipped. - """ - if not logins or not token: - return {} - unique = [] - seen = set() - for login in logins: - if login and login not in seen: - seen.add(login) - unique.append(login) - if not unique: - return {} - - fields = ( - "login name bio location websiteUrl " - "followers { totalCount } following { totalCount } " - "repositories(privacy: PUBLIC, ownerAffiliations: OWNER) { totalCount }" - ) - var_decls = ", ".join(f"$l{i}: String!" for i in range(len(unique))) - fragments = " ".join( - f"u{i}: user(login: $l{i}) {{ {fields} }}" for i in range(len(unique)) - ) - query = f"query({var_decls}) {{ {fragments} }}" - variables = {f"l{i}": login for i, login in enumerate(unique)} - - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: profile fetch failed: {exc}", file=sys.stderr) - return {} - if "errors" in body: - print(f" warning: profile fetch errors: {body['errors']}", file=sys.stderr) - data = body.get("data") or {} - out = {} - for i, login in enumerate(unique): - node = data.get(f"u{i}") - if not node: - continue - out[login] = { - "login": node.get("login") or login, - "name": node.get("name") or "", - "bio": node.get("bio") or "", - "location": node.get("location") or "", - "websiteUrl": node.get("websiteUrl") or "", - "followers": (node.get("followers") or {}).get("totalCount") or 0, - "following": (node.get("following") or {}).get("totalCount") or 0, - "publicRepos": (node.get("repositories") or {}).get("totalCount") or 0, - } - return out - - -def fetch_remote_tags(owner, repo, token): - """Fetch all tag refs via GraphQL. Returns list of {name, oid, date, message}.""" - query = """ -query($owner: String!, $repo: String!, $cursor: String) { - repository(owner: $owner, name: $repo) { - refs(refPrefix: "refs/tags/", first: 100, after: $cursor, orderBy: {field: TAG_COMMIT_DATE, direction: ASC}) { - pageInfo { hasNextPage endCursor } - nodes { - name - target { - __typename - ... on Tag { - message - target { ... on Commit { oid committedDate } } - } - ... on Commit { oid committedDate } - } - } - } - } -} -""".strip() - cursor = None - tags = [] - while True: - try: - body = gh_graphql(query, {"owner": owner, "repo": repo, "cursor": cursor}, token) - except urllib.error.URLError as exc: - print(f" warning: tag fetch failed: {exc}", file=sys.stderr) - return tags - if "errors" in body: - print(f" warning: tag fetch GraphQL error: {body['errors']}", file=sys.stderr) - return tags - refs = gh_repository(body).get("refs") or {} - for node in refs.get("nodes") or []: - tgt = node.get("target") or {} - kind = tgt.get("__typename") - if kind == "Tag": - inner = tgt.get("target") or {} - oid = inner.get("oid") or "" - date = inner.get("committedDate") or "" - message = tgt.get("message") or "" - elif kind == "Commit": - oid = tgt.get("oid") or "" - date = tgt.get("committedDate") or "" - message = "" - else: - continue - if not oid or not date: - continue - tags.append({ - "name": node.get("name") or "", - "oid": oid, - "date": date, - "message": (message.splitlines() or [""])[0], - }) - page = refs.get("pageInfo") or {} - if not page.get("hasNextPage"): - break - cursor = page.get("endCursor") - tags.sort(key=lambda t: parse_iso_instant(t.get("date"))) - return tags - - -def gh_rest_get(path, token): - """GET an api.github.com REST endpoint; returns the parsed JSON body.""" - req = urllib.request.Request( - f"https://api.github.com{path}", - headers={ - "Authorization": f"bearer {token}", - "User-Agent": "repo-intel", - "Accept": "application/vnd.github+json", - }, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - - -# Manifests _frameworks_from_files actually parses (so we only fetch those -# blobs). tsconfig.json / sentinels are presence-only — covered by the tree. -_REMOTE_MANIFEST_BASES = frozenset({ - "package.json", "composer.json", "pyproject.toml", "pipfile", - "setup.py", "setup.cfg", "gemfile", "go.mod", "cargo.toml", -}) - - -def _remote_manifest_paths(paths): - out = [] - for p in paths: - base = p.rsplit("/", 1)[-1].lower() - if base in _REMOTE_MANIFEST_BASES or ( - base.startswith("requirements") and base.endswith(".txt") - ): - out.append(p) - return out - - -def fetch_blob_texts(owner, repo, paths, token): - """HEAD blob text for each path via aliased GraphQL. Returns {path: text}.""" - out = {} - paths = list(paths) - for start in range(0, len(paths), 50): - chunk = paths[start:start + 50] - var_decls = ", ".join(f"$p{i}: String!" for i in range(len(chunk))) - frags = " ".join( - f"b{i}: object(expression: $p{i}) {{ ... on Blob {{ text }} }}" - for i in range(len(chunk)) - ) - query = ( - f"query($owner: String!, $repo: String!, {var_decls}) " - f"{{ repository(owner: $owner, name: $repo) {{ {frags} }} }}" - ) - variables = {"owner": owner, "repo": repo} - for i, p in enumerate(chunk): - variables[f"p{i}"] = f"HEAD:{p}" - try: - body = gh_graphql(query, variables, token) - except urllib.error.URLError as exc: - print(f" warning: manifest fetch failed: {exc}", file=sys.stderr) - continue - node = gh_repository(body) - for i, p in enumerate(chunk): - blob = node.get(f"b{i}") - if blob and blob.get("text") is not None: - out[p] = blob["text"] - return out - - -def fetch_frameworks_remote(owner, repo, token): - """Detect frameworks on the GraphQL path without a clone. - - Lists the repo tree (REST, recursive — manifests can be nested) and fetches - just the manifest blobs (GraphQL), then runs the shared detection core. - Per-file *languages* stay local-only (too expensive over the network), but - manifests are cheap, so frameworks work here too. - """ - if not token: - return [] - try: - tree = gh_rest_get(f"/repos/{owner}/{repo}/git/trees/HEAD?recursive=1", token) - except urllib.error.URLError as exc: - print(f" warning: framework tree fetch failed: {exc}", file=sys.stderr) - return [] - if tree.get("truncated"): - # GitHub caps the recursive tree at ~100k entries / 7MB; deep manifests - # past the cap are dropped, so detection may miss frameworks silently. - print( - " warning: repo tree truncated by GitHub — framework detection " - "may be incomplete", - file=sys.stderr, - ) - paths = [e["path"] for e in (tree.get("tree") or []) if e.get("type") == "blob"] - if not paths: - return [] - contents = fetch_blob_texts(owner, repo, _remote_manifest_paths(paths), token) - return _frameworks_from_files(paths, lambda p: contents.get(p, "")) - - -def fetch_languages_remote(owner, repo, token): - """Repo-wide language breakdown on the GraphQL path, no clone needed. - - GitHub runs Linguist itself and exposes the result as bytes-per-language at - HEAD. That's a composition snapshot, not the per-commit line churn the local - path tracks — so it can only fill the repo-wide bar, never per-author or - per-commit language stats. Reuses `top_languages` (ranking by the first - slot, here byte size) so colors and overflow collapsing match local runs. - Returns [] on error or when the repo has no detected languages. - """ - if not token: - return [] - query = """ -query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - languages(first: 50, orderBy: {field: SIZE, direction: DESC}) { - edges { size node { name } } - } - } -} -""".strip() - try: - body = gh_graphql(query, {"owner": owner, "repo": repo}, token) - except urllib.error.URLError as exc: - print(f" warning: language fetch failed: {exc}", file=sys.stderr) - return [] - if "errors" in body: - return [] - edges = ((gh_repository(body).get("languages") or {}).get("edges")) or [] - langs = {} - for e in edges: - name = ((e.get("node") or {}).get("name") or "").strip() - size = e.get("size") or 0 - if name and size > 0: - langs[name] = [size, 0, 0] - return top_languages(langs) - - -def _paginate_history(fetch_page, cached_oids, last_n, since, - have_count_baseline, label, skip_first=False): - """Walk a Commit.history connection page by page. - - fetch_page(cursor) -> history dict, or None when the anchor object is gone. - Returns (nodes, reason) where reason ∈ - "hit_cache" | "short_circuit" | "page_end" | "anchor_null" | "fetch_failed" - On "fetch_failed" the returned nodes are still a contiguous run from the - walk's start, so the caller can persist them and resume on a re-run. - """ - nodes = [] - cursor = None - dropped_anchor = not skip_first - while True: - try: - history = fetch_page(cursor) - except urllib.error.URLError as exc: - # A non-retryable HTTP status (401/403/404) is a hard failure, not - # a resumable one — propagate it rather than persisting a partial - # cache and telling the user to re-run. - if isinstance(exc, urllib.error.HTTPError) and exc.code not in RETRYABLE_STATUS: - raise - print(f" error: {label} fetch aborted: {exc}", file=sys.stderr) - return nodes, "fetch_failed" - if history is None: - return nodes, "anchor_null" - for n in history.get("nodes") or []: - if not dropped_anchor: - dropped_anchor = True - continue - if n["oid"] in cached_oids: - return nodes, "hit_cache" - nodes.append(n) - if last_n is not None and len(nodes) + have_count_baseline >= last_n: - return nodes, "short_circuit" - if since: - d = ((n.get("author") or {}).get("date") or "")[:10] - if d and d < since: - return nodes, "short_circuit" - page = history.get("pageInfo") or {} - if not page.get("hasNextPage"): - return nodes, "page_end" - cursor = page.get("endCursor") - print(f" fetched {len(nodes)} {label} commits…", file=sys.stderr) - - -def collect_remote(slug, token, no_cache=False, commits_filter=None, since=None, until=None): - owner, repo = slug.split("/", 1) - - if not token: - clone_dir = ensure_bare_clone(owner, repo, no_cache) - ( - repo_name, - github_base, - _, - commits_meta, - line_stats, - _, - _, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_local(cwd=clone_dir, suppress_current_user=True) - if not github_base: - github_base = f"https://github.com/{owner}/{repo}" - return ( - repo_name, - github_base, - "", - commits_meta, - line_stats, - {}, - {}, - default_branch, - repo_size_kb, - tags, - extras, - ) - - history_block = """ -history(first: $pageSize, after: $cursor) { - pageInfo { hasNextPage endCursor } - nodes { - oid messageHeadline - author { name email date user { avatarUrl(size: 64) login } } - additions deletions - } -}""".strip() - - top_query = f""" -query($owner: String!, $repo: String!, $cursor: String, $pageSize: Int!) {{ - repository(owner: $owner, name: $repo) {{ - name url diskUsage - defaultBranchRef {{ - name - target {{ ... on Commit {{ {history_block} }} }} - }} - }} -}}""".strip() - - bottom_query = f""" -query($owner: String!, $repo: String!, $oid: GitObjectID!, $cursor: String, $pageSize: Int!) {{ - repository(owner: $owner, name: $repo) {{ - object(oid: $oid) {{ ... on Commit {{ {history_block} }} }} - }} -}}""".strip() - - loaded_nodes, loaded_complete = ([], False) if no_cache else load_cache(slug) - cached_nodes = loaded_nodes - cached_oids = {n["oid"] for n in cached_nodes} - if cached_nodes: - label = "complete" if loaded_complete else "partial" - print(f" cache: {len(cached_nodes)} commits ({label})", file=sys.stderr) - - last_n = commits_filter[1] if commits_filter and commits_filter[0] == "last" else None - - repo_meta = { - "name": repo, - "url": f"https://github.com/{owner}/{repo}", - "branch": "main", - "disk_kb": 0, - } - - def top_fetch_page(cursor): - body = fetch_history_page( - top_query, {"owner": owner, "repo": repo, "cursor": cursor}, token, "new" - ) - if "errors" in body: - sys.exit(f"GraphQL error: {body['errors']}") - repo_node = gh_repository(body) - if not repo_node: - sys.exit(f"Repository not found or inaccessible: {slug}") - repo_meta["name"] = repo_node["name"] - repo_meta["url"] = repo_node["url"] - repo_meta["disk_kb"] = repo_node.get("diskUsage") or 0 - branch_ref = repo_node.get("defaultBranchRef") - if not branch_ref or not branch_ref.get("target"): - sys.exit(f"error: {slug} has no commits on its default branch") - repo_meta["branch"] = branch_ref.get("name") or repo_meta["branch"] - return branch_ref["target"]["history"] - - def bail_partial(nodes): - """Persist a contiguous partial run after a fetch failure, then exit so - the next run resumes from its tail. Saved as incomplete on purpose.""" - if not no_cache and nodes: - save_cache(slug, nodes, False) - print( - f" cached {len(nodes)} commits so far — re-run to resume", - file=sys.stderr, - ) - sys.exit("error: GitHub fetch failed after repeated retries; aborting.") - - new_nodes, top_reason = _paginate_history( - top_fetch_page, cached_oids, last_n, since, - have_count_baseline=len(cached_nodes), label="new", - ) - - if top_reason == "fetch_failed": - # new_nodes is a contiguous run from HEAD. We never reached the old - # cache, so merging would leave a gap — persist just the fresh prefix - # (the next run resumes its tail via the older-fetch) and bail out. - bail_partial(new_nodes) - - if top_reason == "page_end" and cached_oids: - print( - f" cache: orphaned by force-push/rewrite, discarded ({len(cached_nodes)} commits)", - file=sys.stderr, - ) - cached_nodes = [] - cached_oids = set() - loaded_complete = False - if new_nodes: - print(f" fetched {len(new_nodes)} new commits", file=sys.stderr) - - older_nodes = [] - bottom_reason = None - have_count = len(new_nodes) + len(cached_nodes) - cached_oldest_date = ( - ((cached_nodes[-1].get("author") or {}).get("date") or "")[:10] - if cached_nodes else "" - ) - if needs_older_fetch( - have_count, cached_oldest_date, loaded_complete, - commits_filter, since, until, - ): - anchor_oid = cached_nodes[-1]["oid"] - - def bottom_fetch_page(cursor): - body = fetch_history_page( - bottom_query, - {"owner": owner, "repo": repo, "oid": anchor_oid, "cursor": cursor}, - token, - "older", - ) - if "errors" in body: - sys.exit(f"GraphQL error: {body['errors']}") - obj = gh_repository(body).get("object") - if not obj: - return None - return obj.get("history") or {"nodes": [], "pageInfo": {}} - - older_nodes, bottom_reason = _paginate_history( - bottom_fetch_page, cached_oids, last_n, since, - have_count_baseline=have_count, label="older", skip_first=True, - ) - if bottom_reason == "anchor_null": - print( - " warning: cache anchor commit no longer exists; keeping what we have", - file=sys.stderr, - ) - elif older_nodes: - print(f" fetched {len(older_nodes)} older commits", file=sys.stderr) - - repo_name = repo_meta["name"] - repo_url = repo_meta["url"] - default_branch = repo_meta["branch"] - repo_size_kb = repo_meta["disk_kb"] - - nodes = new_nodes + cached_nodes + older_nodes - if bottom_reason == "fetch_failed": - # new + cached + older are contiguous, so the partial run is a valid - # prefix to persist; the next run extends from its tail. - bail_partial(nodes) - if bottom_reason is None: - new_complete = top_reason == "page_end" or loaded_complete - else: - new_complete = bottom_reason == "page_end" - if not no_cache and nodes: - save_cache(slug, nodes, new_complete) - - commits_meta, line_stats, avatars, logins = {}, {}, {}, {} - for n in nodes: - author = n.get("author") or {} - email = (author.get("email") or "").lower() - commits_meta[n["oid"]] = { - "subject": n.get("messageHeadline") or "", - "email": email, - "name": author.get("name") or email or "unknown", - "iso": author.get("date"), - } - line_stats[n["oid"]] = [n.get("additions") or 0, n.get("deletions") or 0] - user = author.get("user") - if user and email: - if user.get("avatarUrl") and email not in avatars: - avatars[email] = user["avatarUrl"] - if user.get("login") and email not in logins: - logins[email] = user["login"] - - tags = fetch_remote_tags(owner, repo, token) - # Per-commit/per-author language churn needs a clone, so `lang_stats` stays - # empty here. But the repo-wide bar and frameworks both come straight from - # the API: GitHub runs Linguist for `repo_languages`, and manifests are - # cheap to fetch for frameworks. - frameworks = fetch_frameworks_remote(owner, repo, token) - repo_languages = fetch_languages_remote(owner, repo, token) - return ( - repo_name, - repo_url, - "", - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - {"lang_stats": {}, "frameworks": frameworks, "repo_languages": repo_languages}, - ) - - -def apply_filters(commits_meta, line_stats, commits_filter, since, until): - if since or until: - def in_range(m): - d = (m.get("iso") or "")[:10] - return bool(d) and (not since or d >= since) and (not until or d <= until) - commits_meta = {h: m for h, m in commits_meta.items() if in_range(m)} - if commits_filter: - epoch = datetime(1970, 1, 1, tzinfo=timezone.utc) - def _ts(h): - iso = commits_meta[h].get("iso") or "" - if not iso: - return epoch - try: - return datetime.fromisoformat(iso.replace("Z", "+00:00")) - except ValueError: - return epoch - ordered = sorted(commits_meta, key=_ts) - if commits_filter[0] == "last": - keep = set(ordered[-commits_filter[1]:]) - else: - keep = set(ordered[commits_filter[1]:commits_filter[2]]) - commits_meta = {h: m for h, m in commits_meta.items() if h in keep} - line_stats = {h: line_stats[h] for h in commits_meta if h in line_stats} - return commits_meta, line_stats - - -def filter_tags_to_range(tags, commits_meta): - if not tags or not commits_meta: - return [] - dates = [(m.get("iso") or "")[:10] for m in commits_meta.values()] - dates = [d for d in dates if d] - if not dates: - return list(tags) - lo, hi = min(dates), max(dates) - return [t for t in tags if lo <= (t.get("date") or "")[:10] <= hi] - - -def build_data( - top_n, - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, -): - lang_stats = (extras or {}).get("lang_stats", {}) - frameworks = (extras or {}).get("frameworks", []) - # Remote runs ship a precomputed repo-wide bar (bytes at HEAD); local/bare - # runs build it below from per-commit line churn. `repo_languages` being a - # non-empty list signals the former. - repo_languages = (extras or {}).get("repo_languages") or [] - repo_langs = {} - authors = {} - daily_by_author = defaultdict(lambda: defaultdict(int)) - hourly_by_author = defaultdict(lambda: [0] * 24) - dow_by_author = defaultdict(lambda: [0] * 7) - weekly_by_author = defaultdict(lambda: defaultdict(int)) - all_dates, all_weeks = set(), set() - total_added = total_deleted = total_commits = 0 - - for h, meta in commits_meta.items(): - iso = meta.get("iso") - if not iso: - continue - try: - dt = datetime.fromisoformat(iso.replace("Z", "+00:00")) - except ValueError: - continue - total_commits += 1 - d_key = dt.strftime("%Y-%m-%d") - wk, hr, dow = iso_week_label(dt), dt.hour, dt.weekday() - email, name = meta["email"], meta["name"] - a, d = line_stats.get(h, [0, 0]) - total_added += a - total_deleted += d - - rec = authors.setdefault( - email, - { - "name": name, - "email": email, - "commits": 0, - "added": 0, - "deleted": 0, - "dates": set(), - "daily_counts": defaultdict(int), - "langs": {}, - "first": d_key, - "last": d_key, - }, - ) - rec["commits"] += 1 - rec["added"] += a - rec["deleted"] += d - rec["dates"].add(d_key) - rec["daily_counts"][d_key] += 1 - for lang, (la, ld, lf) in lang_stats.get(h, {}).items(): - agg = rec["langs"].setdefault(lang, [0, 0, 0]) - agg[0] += la - agg[1] += ld - agg[2] += lf - repo = repo_langs.setdefault(lang, [0, 0, 0]) - repo[0] += la - repo[1] += ld - repo[2] += lf - if d_key < rec["first"]: - rec["first"] = d_key - if d_key > rec["last"]: - rec["last"] = d_key - - daily_by_author[email][d_key] += 1 - hourly_by_author[email][hr] += 1 - dow_by_author[email][dow] += 1 - weekly_by_author[email][wk] += 1 - all_dates.add(d_key) - all_weeks.add(wk) - - total_contributors = len(authors) - ranked = sorted(authors.values(), key=lambda r: r["commits"], reverse=True) - top = ranked[:top_n] - top_emails = {r["email"] for r in top} - - contributors = [] - for r in top: - busiest_day, busiest_count = "", 0 - for k, v in r["daily_counts"].items(): - if v > busiest_count: - busiest_day, busiest_count = k, v - login = logins.get(r["email"]) or login_from_email(r["email"]) - contributors.append( - { - "name": r["name"], - "email": r["email"], - "login": login, - "commits": r["commits"], - "added": r["added"], - "deleted": r["deleted"], - "activeDays": len(r["dates"]), - "first": r["first"], - "last": r["last"], - "busiestDay": busiest_day, - "busiestCount": busiest_count, - "avatarUrl": avatar_url(r["email"], override=avatars.get(r["email"])), - "highlight": bool(current_email) and r["email"] == current_email, - "languages": top_languages(r["langs"]), - } - ) - - weeks_sorted = sorted(all_weeks) - weekly_data = { - r["email"]: [weekly_by_author[r["email"]].get(w, 0) for w in weeks_sorted] - for r in top - } - daily_data = {r["email"]: dict(daily_by_author[r["email"]]) for r in top} - hourly_data = {r["email"]: hourly_by_author[r["email"]] for r in top} - dow_data = {r["email"]: dow_by_author[r["email"]] for r in top} - - commits_list = [] - for h, meta in commits_meta.items(): - if meta["email"] not in top_emails: - continue - a, d = line_stats.get(h, [0, 0]) - entry = { - "h": h[:7], - "s": (meta["subject"] or "")[:120], - "e": meta["email"], - "d": meta.get("iso") or "", - "a": a, - "l": d, - } - cl = lang_stats.get(h) - if cl: - ftypes = sorted( - ([name, NAME_COLOR.get(name, OTHER_COLOR), files] - for name, (_, _, files) in cl.items()), - key=lambda x: x[2], reverse=True, - ) - entry["f"] = ftypes[:4] - commits_list.append(entry) - - date_range = ( - {"start": min(all_dates), "end": max(all_dates)} - if all_dates - else {"start": "", "end": ""} - ) - return { - "repoName": repo_name, - "githubBaseUrl": github_base, - "defaultBranch": default_branch, - "repoSizeKb": repo_size_kb, - "dateRange": date_range, - "totals": { - "commits": total_commits, - "added": total_added, - "deleted": total_deleted, - "contributors": total_contributors, - }, - "contributors": contributors, - "weeks": weeks_sorted, - "weeklyData": weekly_data, - "dailyData": daily_data, - "hourlyData": hourly_data, - "dowData": dow_data, - "commits": commits_list, - "tags": tags or [], - "repoLanguages": repo_languages or top_languages(repo_langs), - "repoLanguagesBasis": "size" if repo_languages else "churn", - "frameworks": frameworks or [], - } - - -def _sample_oids_per_email(commits_meta, target_emails, per_email=3): - """Up to `per_email` oldest oids per email. Unknown-date commits sort last.""" - by_email = defaultdict(list) - for h, meta in commits_meta.items(): - if meta["email"] in target_emails: - by_email[meta["email"]].append((meta.get("iso") or "", h)) - sample = {} - for email, items in by_email.items(): - items.sort(key=lambda x: (not x[0], x[0])) - sample[email] = [h for _, h in items[:per_email]] - return sample - - -def enrich_contributor_profiles(contributors, commits_meta, github_base, token=None): - """In-place: attach `profile` dict to contributors using GitHub GraphQL.""" - if not github_base: - return - if token is None: - token = get_github_token() - if not token: - return - origin = ORIGIN_RE.match(github_base) - if not origin: - return - # gh_graphql is hardcoded to api.github.com; skip Enterprise hosts so we - # don't issue lookups against the wrong API. - if (origin.group("https_host") or "").lower() != "github.com": - return - - missing = [c for c in contributors if not c.get("login")] - if missing: - sample = _sample_oids_per_email( - commits_meta, {c["email"] for c in missing} - ) - resolved = fetch_logins_for_commits( - origin.group("owner"), origin.group("repo"), sample, token - ) - for c in missing: - login = resolved.get(c["email"]) - if login: - c["login"] = login - - top_logins = [c["login"] for c in contributors if c.get("login")] - profiles = fetch_user_profiles(top_logins, token) - for c in contributors: - p = profiles.get(c.get("login") or "") - if p: - c["profile"] = p - - -def main(): - top_n, remote, output, no_open, no_cache, clone, commits_filter, since, until = parse_args( - sys.argv[1:] - ) - - token = None - if remote: - owner, repo = remote.split("/", 1) - token = get_github_token() - # --clone forces the bare-clone path even when a token is present: a - # local clone unlocks per-author language churn the GraphQL history API - # can't provide. The token (if any) is still used below for hovercard - # enrichment, so `use_graphql` — not `token` — gates the API path. - use_graphql = bool(token) and not clone - - if not use_graphql and not clone: - print("No GitHub token — falling back to bare clone.", file=sys.stderr) - - # Subset prompt only in the GraphQL path: probing total via the API is - # cheap, and skipping `--commits N` actually saves network. In the - # bare-clone path the full clone runs regardless, so the prompt would - # only trim local display — pass `--commits` / `--since` for that. - if ( - use_graphql - and not (commits_filter or since or until) - and sys.stdin.isatty() - and sys.stderr.isatty() - ): - has_any_cache = not no_cache and cache_path(remote).exists() - total = None if has_any_cache else probe_remote_total(owner, repo, token) - if total and total > 1000: - commits_filter, since, until = prompt_subset(total) - - if use_graphql: - print(f"Fetching {remote} via GitHub GraphQL…", file=sys.stderr) - else: - print(f"Cloning {remote} (bare) for local analysis…", file=sys.stderr) - ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_remote( - remote, - token if use_graphql else None, - no_cache=no_cache, - commits_filter=commits_filter, - since=since, - until=until, - ) - else: - try: - subprocess.check_output( - ["git", "rev-parse", "--git-dir"], stderr=subprocess.DEVNULL - ) - except subprocess.CalledProcessError: - sys.exit( - "error: not in a git repository (and no owner/repo argument given)" - ) - ( - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) = collect_local() - - if not commits_meta: - sys.exit("error: no commits found") - - if commits_filter or since or until: - total_before = len(commits_meta) - commits_meta, line_stats = apply_filters( - commits_meta, line_stats, commits_filter, since, until - ) - print( - f" filtered: {len(commits_meta)}/{total_before} commits", file=sys.stderr - ) - if not commits_meta: - sys.exit("error: no commits match the given filters") - - tags = filter_tags_to_range(tags, commits_meta) - - data = build_data( - top_n, - repo_name, - github_base, - current_email, - commits_meta, - line_stats, - avatars, - logins, - default_branch, - repo_size_kb, - tags, - extras, - ) - - enrich_contributor_profiles(data["contributors"], commits_meta, github_base, token=token) - - payload = f"window.__DATA__ = {json.dumps(data, ensure_ascii=False, separators=(',', ':'))};" - template = TEMPLATE - if template == "__TEMPLATE_PLACEHOLDER__": - sibling = Path(__file__).resolve().parent / "template.html" - if not sibling.exists(): - sys.exit(f"error: unbuilt script and template.html not found at {sibling}") - template = sibling.read_text() - if PLACEHOLDER not in template: - sys.exit(f"error: placeholder {PLACEHOLDER!r} not found in template") - html = template.replace(PLACEHOLDER, payload) - - if output: - out_path = Path(output).expanduser() - else: - safe_name = _slugify(data["repoName"]) or "repo" - owner = "" - if data["githubBaseUrl"]: - m = ORIGIN_RE.match(data["githubBaseUrl"]) - if m: - owner = _slugify(m.group("owner")) - stem = f"{owner}--{safe_name}" if owner else safe_name - out_path = Path("/tmp") / f"{stem}.html" - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(html) - - print(f"Wrote {out_path}") - print( - f" {data['totals']['commits']} commits · " - f"{data['dateRange']['start']} — {data['dateRange']['end']} · " - f"{data['totals']['contributors']} contributor" - f"{'' if data['totals']['contributors'] == 1 else 's'}" - ) - print(" top 3:") - for c in data["contributors"][:3]: - print(f" {c['commits']:>5} {c['name']} <{c['email']}>") - - if no_open: - return - opener = "open" if sys.platform == "darwin" else "xdg-open" - try: - subprocess.run([opener, str(out_path)], check=False) - except FileNotFoundError: - webbrowser.open(out_path.as_uri()) - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\nAborted.", file=sys.stderr) - sys.exit(130) From d5c34fd89331248fcbcf118729d67362de3508f2 Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Fri, 22 May 2026 09:58:12 +0100 Subject: [PATCH 2/3] Fix repo-intel tap name in brew.sh log message --- scripts/install/brew.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install/brew.sh b/scripts/install/brew.sh index 144d2b8..6daa622 100755 --- a/scripts/install/brew.sh +++ b/scripts/install/brew.sh @@ -56,7 +56,7 @@ print_step "Installing Homebrew packages" && brew install "${packages[@]}" # repo-intel (standalone tool: github.com/tyom/repo-intel). Installed separately # and non-fatally so a missing/unpublished tap can't abort the core packages # above. If this is skipped, setup.sh's curl fallback installs it into ~/bin. -print_step "Installing repo-intel (tap: tyom/homebrew-tap)" && +print_step "Installing repo-intel (tap: tyom/tap)" && brew install tyom/tap/repo-intel || print_info "Skipping repo-intel via brew (setup will fetch it via curl)" From 6b5d19412765583026a4204e7002d86c9689879b Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Fri, 22 May 2026 11:08:50 +0100 Subject: [PATCH 3/3] Install repo-intel via brew only, drop it from the stow tree repo-intel is now published (tap tyom/tap/repo-intel + a standalone installer), and brew.sh runs on both macOS and Linux (Linuxbrew), so the Homebrew install covers every platform. Remove the now-redundant stow-tree curl fallback: - setup.sh: drop the curl-into-stow/bin fetch block - brew.sh: this is the sole install path; on failure hint at the manual installer instead of the removed fallback - .gitignore: drop stow/bin/repo-intel; ignore __pycache__/ and *.pyc - README: installed via Homebrew or the standalone repo's installer --- .gitignore | 7 +++---- README.md | 2 +- scripts/install/brew.sh | 8 ++++---- scripts/setup.sh | 18 ------------------ 4 files changed, 8 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index ab0287e..ede5fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ *.log .env -# repo-intel is now a standalone tool (github.com/tyom/repo-intel). -# The artifact is installed via Homebrew or fetched into the stow tree -# during setup, not committed here. -stow/bin/repo-intel +# Python bytecode (the stow tree previously hosted a Python tool) +__pycache__/ +*.pyc diff --git a/README.md b/README.md index fc128da..e2c4a04 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Personal dotfiles for macOS and Linux, designed for a smooth developer experienc - **Vim**: Pre-configured with vim-plug and curated plugins - **CLI Tools**: bat (syntax-highlighted cat), fzf (fuzzy finder), git-delta (better diffs), and more via Homebrew - **Dev Tools**: Volta and Node.js; Bun (optional) -- **Bin Scripts**: Handy commands like `ungit` (clone GitHub repos/subdirs as files or text) and [`repo-intel`](https://github.com/tyom/repo-intel) (contributor stats dashboard for any git repo — now a standalone tool, installed via Homebrew or fetched into `~/bin` during setup) +- **Bin Scripts**: Handy commands like `ungit` (clone GitHub repos/subdirs as files or text) and [`repo-intel`](https://github.com/tyom/repo-intel) (contributor stats dashboard for any git repo — now a standalone tool, installed via Homebrew or the standalone repo's installer) - **Claude Code Plugin**: Custom commands for code review, explanation, and refactoring ![Shell screenshot](https://tyom.github.io/dotfiles/shell.png) diff --git a/scripts/install/brew.sh b/scripts/install/brew.sh index 6daa622..c0183fc 100755 --- a/scripts/install/brew.sh +++ b/scripts/install/brew.sh @@ -53,11 +53,11 @@ fi print_step "Updating Homebrew" && brew update print_step "Installing Homebrew packages" && brew install "${packages[@]}" -# repo-intel (standalone tool: github.com/tyom/repo-intel). Installed separately -# and non-fatally so a missing/unpublished tap can't abort the core packages -# above. If this is skipped, setup.sh's curl fallback installs it into ~/bin. +# repo-intel (standalone tool: github.com/tyom/repo-intel). Installed non-fatally +# so a tap/network hiccup can't abort the core packages above. brew.sh runs on +# both macOS and Linux (Linuxbrew), so this is the sole install path. print_step "Installing repo-intel (tap: tyom/tap)" && brew install tyom/tap/repo-intel || - print_info "Skipping repo-intel via brew (setup will fetch it via curl)" + print_info "Skipping repo-intel — install manually: curl -fsSL https://tyom.github.io/repo-intel/install.sh | sh" print_info "Cleaning outdating brew packages" && brew cleanup diff --git a/scripts/setup.sh b/scripts/setup.sh index 194eb81..5bec4db 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -61,24 +61,6 @@ fi print_step 'Setting up zsh' && source "$DOTFILES_DIR/scripts/zsh.sh" -# repo-intel is a standalone tool (github.com/tyom/repo-intel). On macOS it's -# installed via Homebrew (tyom/tap/repo-intel) above. Elsewhere — or if brew -# was skipped — fetch the single-file artifact into the stow tree so it gets -# symlinked to ~/bin like the other bin scripts. -if ! command -v repo-intel &>/dev/null; then - print_step 'Installing repo-intel' - if curl -fsSL https://raw.githubusercontent.com/tyom/repo-intel/main/dist/repo-intel \ - -o "$DOTFILES_DIR/stow/bin/repo-intel"; then - chmod +x "$DOTFILES_DIR/stow/bin/repo-intel" - print_success 'repo-intel fetched into stow/bin' - else - rm -f "$DOTFILES_DIR/stow/bin/repo-intel" - print_error 'Failed to fetch repo-intel (skipping)' - fi -else - print_info 'repo-intel already installed' -fi - print_step 'Symlinking dotfiles' && source "$DOTFILES_DIR/scripts/stow.sh"