use std::collections::BTreeMap; use anyhow::Result; #[derive(Clone, Copy)] pub(crate) struct Stat { /// grammar id (after grouping); what the client filters on pub id: &'static str, pub name: &'static str, pub color: &'static str, pub bytes: u64, } pub(crate) fn analyze(repo: &gix::Repository, tree: &gix::Tree<'_>) -> Result> { fn walk( repo: &gix::Repository, tree: &gix::Tree<'_>, bytes: &mut BTreeMap<&'static str, u64>, ) -> Result<()> { for entry in tree.iter() { let entry = entry?; let mode = entry.mode(); let filename = entry.filename().to_string(); if mode.is_tree() { if !excluded_directory(&filename) { walk(repo, &repo.find_object(entry.oid())?.try_into_tree()?, bytes)?; } } else if !mode.is_link() && !mode.is_commit() && !excluded_file(&filename) && let Some(language) = crate::grammar_manifest::detect(&filename) { let size = repo.find_header(entry.oid())?.size() as u64; *bytes.entry(group(language)).or_default() += size; } } Ok(()) } let mut bytes = BTreeMap::new(); walk(repo, tree, &mut bytes)?; let mut stats = bytes .into_iter() .map(|(language, bytes)| { let (name, color) = metadata(language); Stat { id: language, name, color, bytes } }) .collect::>(); stats.sort_by(|a, b| b.bytes.cmp(&a.bytes).then_with(|| a.name.cmp(b.name))); Ok(stats) } // mirrored by looksGenerated in web/util.ts; keep them in sync fn excluded_directory(name: &str) -> bool { [ "node_modules", "vendor", "third_party", "third-party", "target", "dist", "build", "coverage", ".next", ".nuxt", ".output", ".yarn", ".gradle", ".terraform", ".dart_tool", ".direnv", ".venv", "venv", "__pycache__", "Pods", "Carthage", "generated", ] .iter() .any(|excluded| name.eq_ignore_ascii_case(excluded)) } fn excluded_file(name: &str) -> bool { if [ "Cargo.lock", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb", "deno.lock", "composer.lock", "Gemfile.lock", "go.sum", "poetry.lock", "uv.lock", "Pipfile.lock", "pdm.lock", "flake.lock", "mix.lock", "pubspec.lock", "Package.resolved", "packages.lock.json", "gradle.lockfile", "Podfile.lock", "Cartfile.resolved", ] .iter() .any(|excluded| name.eq_ignore_ascii_case(excluded)) { return true; } let name = name.to_ascii_lowercase(); name.ends_with(".lock") || name.ends_with(".min.js") || name.ends_with(".min.css") || name.contains(".generated.") || name.contains("_generated.") || name.ends_with(".g.dart") || name.ends_with(".pb.go") || name.ends_with(".pb.rs") || name.ends_with(".pb.cc") || name.ends_with(".pb.h") || name.ends_with("_pb2.py") || name.ends_with(".designer.cs") } // mirrored by languageGroup in web/languages.ts; keep them in sync fn group(language: &'static str) -> &'static str { match language { "tsx" => "typescript", _ => language, } } fn metadata(language: &'static str) -> (&'static str, &'static str) { match language { "ada" => ("Ada", "#02f88c"), "agda" => ("Agda", "#315665"), "asciidoc" => ("AsciiDoc", "#73a0c5"), "asm" | "x86asm" => ("Assembly", "#6e4c13"), "awk" => ("Awk", "#c30e9b"), "bash" | "zsh" => ("Shell", "#89e051"), "batch" => ("Batchfile", "#c1f12e"), "c" => ("C", "#555555"), "c-sharp" => ("C#", "#7355dd"), "caddy" => ("Caddyfile", "#22b638"), "capnp" => ("Cap'n Proto", "#c42727"), "cedar" => ("Cedar", "#958792"), "cedarschema" => ("Cedar Schema", "#958792"), "clojure" => ("Clojure", "#db5855"), "cmake" => ("CMake", "#da3434"), "cobol" => ("COBOL", "#958792"), "commonlisp" => ("Common Lisp", "#3fb68b"), "cpp" => ("C++", "#f34b7d"), "css" => ("CSS", "#663399"), "d" => ("D", "#ba595e"), "dart" => ("Dart", "#00b4ab"), "devicetree" => ("Device Tree", "#958792"), "diff" => ("Diff", "#958792"), "dockerfile" => ("Dockerfile", "#384d54"), "dot" => ("Graphviz (DOT)", "#2596be"), "elisp" => ("Emacs Lisp", "#c065db"), "elixir" => ("Elixir", "#6e4a7e"), "elm" => ("Elm", "#60b5cc"), "erlang" => ("Erlang", "#b83998"), "fish" => ("fish", "#4aae47"), "fsharp" => ("F#", "#b845fc"), "gitattributes" => ("Git Attributes", "#f44d27"), "gleam" => ("Gleam", "#ffaff3"), "glsl" => ("GLSL", "#5686a5"), "go" => ("Go", "#00add8"), "graphql" => ("GraphQL", "#e10098"), "groovy" => ("Groovy", "#4298b8"), "haskell" => ("Haskell", "#5e5086"), "hcl" => ("HCL", "#844fba"), "hlsl" => ("HLSL", "#aace60"), "html" => ("HTML", "#e34c26"), "idris" => ("Idris", "#b30000"), "ini" => ("INI", "#d1dbe0"), "java" => ("Java", "#b07219"), "javascript" => ("JavaScript", "#f1e05a"), "jinja2" => ("Jinja", "#a52a22"), "jq" => ("jq", "#c7254e"), "jsdoc" => ("JSDoc", "#958792"), "json" => ("JSON", "#292929"), "julia" => ("Julia", "#a270ba"), "just" => ("Just", "#384d54"), "kconfig" => ("Kconfig", "#958792"), "kdl" => ("KDL", "#ffb3b3"), "kotlin" => ("Kotlin", "#a97bff"), "lean" => ("Lean", "#958792"), "lua" => ("Lua", "#000080"), "make" => ("Makefile", "#427819"), "markdown" => ("Markdown", "#083fa1"), "matlab" => ("MATLAB", "#e16737"), "meson" => ("Meson", "#007800"), "nginx" => ("Nginx", "#009639"), "ninja" => ("Ninja", "#958792"), "nix" => ("Nix", "#7e7eff"), "objc" => ("Objective-C", "#438eff"), "ocaml" => ("OCaml", "#ef7a08"), "odin" => ("Odin", "#60affe"), "perl" => ("Perl", "#0298c3"), "php" => ("PHP", "#4f5d95"), "postscript" => ("PostScript", "#da291c"), "powershell" => ("PowerShell", "#012456"), "prolog" => ("Prolog", "#74283c"), "proto" => ("Protocol Buffer", "#958792"), "python" => ("Python", "#3572a5"), "query" => ("Tree-sitter Query", "#8ea64c"), "r" => ("R", "#198ce7"), "regex" => ("Regular Expression", "#009a00"), "rego" => ("Open Policy Agent", "#7d9199"), "rescript" => ("ReScript", "#ed5051"), "ron" => ("RON", "#a62c00"), "ruby" => ("Ruby", "#701516"), "rust" => ("Rust", "#dea584"), "scala" => ("Scala", "#c22d40"), "scheme" => ("Scheme", "#1e4aec"), "scss" => ("SCSS", "#c6538c"), "solidity" => ("Solidity", "#aa6746"), "sparql" => ("SPARQL", "#0c4597"), "sql" => ("SQL", "#e38c00"), "ssh-config" => ("SSH Config", "#958792"), "starlark" => ("Starlark", "#76d275"), "styx" => ("Styx", "#958792"), "svelte" => ("Svelte", "#ff3e00"), "swift" => ("Swift", "#f05138"), "textproto" => ("Protocol Buffer Text Format", "#958792"), "thrift" => ("Thrift", "#d12127"), "tlaplus" => ("TLA", "#4b0079"), "toml" => ("TOML", "#9c4221"), "typescript" => ("TypeScript", "#3178c6"), "typst" => ("Typst", "#239dad"), "uiua" => ("Uiua", "#958792"), "vb" => ("Visual Basic .NET", "#945db7"), "verilog" => ("Verilog", "#b2b7f8"), "vhdl" => ("VHDL", "#adb2cb"), "vim" => ("Vim script", "#199f4b"), "vue" => ("Vue", "#41b883"), "wit" => ("WebAssembly Interface Type", "#6250e7"), "xml" => ("XML", "#0060ac"), "yaml" => ("YAML", "#cb171e"), "yuri" => ("Yuri", "#958792"), "zig" => ("Zig", "#ec915c"), _ => (language, "#958792"), } } #[cfg(test)] mod tests { use super::excluded_file; #[test] fn generated_file_patterns_do_not_overreach() { for name in ["schema.generated.ts", "messages.pb.go", "app.min.js", "CARGO.LOCK"] { assert!(excluded_file(name), "expected {name} to be excluded"); } for name in ["Cargo.toml", "pnpm-workspace.yaml", "src.rs", "generator.ts"] { assert!(!excluded_file(name), "expected {name} to be retained"); } } }