char/sorcery

static-files based git repo viewer

git clone https://git.t4t.associates/char/sorcery

Charlotte Somadd more languages from linguist languages.ymlceba7fd

main
8.7 KiB266 linesraw
1use std::collections::BTreeMap;
2
3use anyhow::Result;
4
5#[derive(Clone, Copy)]
6pub(crate) struct Stat {
7    /// grammar id (after grouping); what the client filters on
8    pub id: &'static str,
9    pub name: &'static str,
10    pub color: &'static str,
11    pub bytes: u64,
12}
13
14pub(crate) fn analyze(repo: &gix::Repository, tree: &gix::Tree<'_>) -> Result<Vec<Stat>> {
15    fn walk(
16        repo: &gix::Repository,
17        tree: &gix::Tree<'_>,
18        bytes: &mut BTreeMap<&'static str, u64>,
19    ) -> Result<()> {
20        for entry in tree.iter() {
21            let entry = entry?;
22            let mode = entry.mode();
23            let filename = entry.filename().to_string();
24            if mode.is_tree() {
25                if !excluded_directory(&filename) {
26                    walk(repo, &repo.find_object(entry.oid())?.try_into_tree()?, bytes)?;
27                }
28            } else if !mode.is_link()
29                && !mode.is_commit()
30                && !excluded_file(&filename)
31                && let Some(language) = crate::grammar_manifest::detect(&filename)
32            {
33                let size = repo.find_header(entry.oid())?.size() as u64;
34                *bytes.entry(group(language)).or_default() += size;
35            }
36        }
37        Ok(())
38    }
39
40    let mut bytes = BTreeMap::new();
41    walk(repo, tree, &mut bytes)?;
42    let mut stats = bytes
43        .into_iter()
44        .map(|(language, bytes)| {
45            let (name, color) = metadata(language);
46            Stat { id: language, name, color, bytes }
47        })
48        .collect::<Vec<_>>();
49    stats.sort_by(|a, b| b.bytes.cmp(&a.bytes).then_with(|| a.name.cmp(b.name)));
50    Ok(stats)
51}
52
53// mirrored by looksGenerated in web/util.ts; keep them in sync
54fn excluded_directory(name: &str) -> bool {
55    [
56        "node_modules",
57        "vendor",
58        "third_party",
59        "third-party",
60        "target",
61        "dist",
62        "build",
63        "coverage",
64        ".next",
65        ".nuxt",
66        ".output",
67        ".yarn",
68        ".gradle",
69        ".terraform",
70        ".dart_tool",
71        ".direnv",
72        ".venv",
73        "venv",
74        "__pycache__",
75        "Pods",
76        "Carthage",
77        "generated",
78    ]
79    .iter()
80    .any(|excluded| name.eq_ignore_ascii_case(excluded))
81}
82
83fn excluded_file(name: &str) -> bool {
84    if [
85        "Cargo.lock",
86        "package-lock.json",
87        "npm-shrinkwrap.json",
88        "yarn.lock",
89        "pnpm-lock.yaml",
90        "bun.lock",
91        "bun.lockb",
92        "deno.lock",
93        "composer.lock",
94        "Gemfile.lock",
95        "go.sum",
96        "poetry.lock",
97        "uv.lock",
98        "Pipfile.lock",
99        "pdm.lock",
100        "flake.lock",
101        "mix.lock",
102        "pubspec.lock",
103        "Package.resolved",
104        "packages.lock.json",
105        "gradle.lockfile",
106        "Podfile.lock",
107        "Cartfile.resolved",
108    ]
109    .iter()
110    .any(|excluded| name.eq_ignore_ascii_case(excluded))
111    {
112        return true;
113    }
114
115    let name = name.to_ascii_lowercase();
116    name.ends_with(".lock")
117        || name.ends_with(".min.js")
118        || name.ends_with(".min.css")
119        || name.contains(".generated.")
120        || name.contains("_generated.")
121        || name.ends_with(".g.dart")
122        || name.ends_with(".pb.go")
123        || name.ends_with(".pb.rs")
124        || name.ends_with(".pb.cc")
125        || name.ends_with(".pb.h")
126        || name.ends_with("_pb2.py")
127        || name.ends_with(".designer.cs")
128}
129
130// mirrored by languageGroup in web/languages.ts; keep them in sync
131fn group(language: &'static str) -> &'static str {
132    match language {
133        "tsx" => "typescript",
134        _ => language,
135    }
136}
137
138fn metadata(language: &'static str) -> (&'static str, &'static str) {
139    match language {
140        "ada" => ("Ada", "#02f88c"),
141        "agda" => ("Agda", "#315665"),
142        "asciidoc" => ("AsciiDoc", "#73a0c5"),
143        "asm" | "x86asm" => ("Assembly", "#6e4c13"),
144        "awk" => ("Awk", "#c30e9b"),
145        "bash" | "zsh" => ("Shell", "#89e051"),
146        "batch" => ("Batchfile", "#c1f12e"),
147        "c" => ("C", "#555555"),
148        "c-sharp" => ("C#", "#7355dd"),
149        "caddy" => ("Caddyfile", "#22b638"),
150        "capnp" => ("Cap'n Proto", "#c42727"),
151        "cedar" => ("Cedar", "#958792"),
152        "cedarschema" => ("Cedar Schema", "#958792"),
153        "clojure" => ("Clojure", "#db5855"),
154        "cmake" => ("CMake", "#da3434"),
155        "cobol" => ("COBOL", "#958792"),
156        "commonlisp" => ("Common Lisp", "#3fb68b"),
157        "cpp" => ("C++", "#f34b7d"),
158        "css" => ("CSS", "#663399"),
159        "d" => ("D", "#ba595e"),
160        "dart" => ("Dart", "#00b4ab"),
161        "devicetree" => ("Device Tree", "#958792"),
162        "diff" => ("Diff", "#958792"),
163        "dockerfile" => ("Dockerfile", "#384d54"),
164        "dot" => ("Graphviz (DOT)", "#2596be"),
165        "elisp" => ("Emacs Lisp", "#c065db"),
166        "elixir" => ("Elixir", "#6e4a7e"),
167        "elm" => ("Elm", "#60b5cc"),
168        "erlang" => ("Erlang", "#b83998"),
169        "fish" => ("fish", "#4aae47"),
170        "fsharp" => ("F#", "#b845fc"),
171        "gitattributes" => ("Git Attributes", "#f44d27"),
172        "gleam" => ("Gleam", "#ffaff3"),
173        "glsl" => ("GLSL", "#5686a5"),
174        "go" => ("Go", "#00add8"),
175        "graphql" => ("GraphQL", "#e10098"),
176        "groovy" => ("Groovy", "#4298b8"),
177        "haskell" => ("Haskell", "#5e5086"),
178        "hcl" => ("HCL", "#844fba"),
179        "hlsl" => ("HLSL", "#aace60"),
180        "html" => ("HTML", "#e34c26"),
181        "idris" => ("Idris", "#b30000"),
182        "ini" => ("INI", "#d1dbe0"),
183        "java" => ("Java", "#b07219"),
184        "javascript" => ("JavaScript", "#f1e05a"),
185        "jinja2" => ("Jinja", "#a52a22"),
186        "jq" => ("jq", "#c7254e"),
187        "jsdoc" => ("JSDoc", "#958792"),
188        "json" => ("JSON", "#292929"),
189        "julia" => ("Julia", "#a270ba"),
190        "just" => ("Just", "#384d54"),
191        "kconfig" => ("Kconfig", "#958792"),
192        "kdl" => ("KDL", "#ffb3b3"),
193        "kotlin" => ("Kotlin", "#a97bff"),
194        "lean" => ("Lean", "#958792"),
195        "lua" => ("Lua", "#000080"),
196        "make" => ("Makefile", "#427819"),
197        "markdown" => ("Markdown", "#083fa1"),
198        "matlab" => ("MATLAB", "#e16737"),
199        "meson" => ("Meson", "#007800"),
200        "nginx" => ("Nginx", "#009639"),
201        "ninja" => ("Ninja", "#958792"),
202        "nix" => ("Nix", "#7e7eff"),
203        "objc" => ("Objective-C", "#438eff"),
204        "ocaml" => ("OCaml", "#ef7a08"),
205        "odin" => ("Odin", "#60affe"),
206        "perl" => ("Perl", "#0298c3"),
207        "php" => ("PHP", "#4f5d95"),
208        "postscript" => ("PostScript", "#da291c"),
209        "powershell" => ("PowerShell", "#012456"),
210        "prolog" => ("Prolog", "#74283c"),
211        "proto" => ("Protocol Buffer", "#958792"),
212        "python" => ("Python", "#3572a5"),
213        "query" => ("Tree-sitter Query", "#8ea64c"),
214        "r" => ("R", "#198ce7"),
215        "regex" => ("Regular Expression", "#009a00"),
216        "rego" => ("Open Policy Agent", "#7d9199"),
217        "rescript" => ("ReScript", "#ed5051"),
218        "ron" => ("RON", "#a62c00"),
219        "ruby" => ("Ruby", "#701516"),
220        "rust" => ("Rust", "#dea584"),
221        "scala" => ("Scala", "#c22d40"),
222        "scheme" => ("Scheme", "#1e4aec"),
223        "scss" => ("SCSS", "#c6538c"),
224        "solidity" => ("Solidity", "#aa6746"),
225        "sparql" => ("SPARQL", "#0c4597"),
226        "sql" => ("SQL", "#e38c00"),
227        "ssh-config" => ("SSH Config", "#958792"),
228        "starlark" => ("Starlark", "#76d275"),
229        "styx" => ("Styx", "#958792"),
230        "svelte" => ("Svelte", "#ff3e00"),
231        "swift" => ("Swift", "#f05138"),
232        "textproto" => ("Protocol Buffer Text Format", "#958792"),
233        "thrift" => ("Thrift", "#d12127"),
234        "tlaplus" => ("TLA", "#4b0079"),
235        "toml" => ("TOML", "#9c4221"),
236        "typescript" => ("TypeScript", "#3178c6"),
237        "typst" => ("Typst", "#239dad"),
238        "uiua" => ("Uiua", "#958792"),
239        "vb" => ("Visual Basic .NET", "#945db7"),
240        "verilog" => ("Verilog", "#b2b7f8"),
241        "vhdl" => ("VHDL", "#adb2cb"),
242        "vim" => ("Vim script", "#199f4b"),
243        "vue" => ("Vue", "#41b883"),
244        "wit" => ("WebAssembly Interface Type", "#6250e7"),
245        "xml" => ("XML", "#0060ac"),
246        "yaml" => ("YAML", "#cb171e"),
247        "yuri" => ("Yuri", "#958792"),
248        "zig" => ("Zig", "#ec915c"),
249        _ => (language, "#958792"),
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::excluded_file;
256
257    #[test]
258    fn generated_file_patterns_do_not_overreach() {
259        for name in ["schema.generated.ts", "messages.pb.go", "app.min.js", "CARGO.LOCK"] {
260            assert!(excluded_file(name), "expected {name} to be excluded");
261        }
262        for name in ["Cargo.toml", "pnpm-workspace.yaml", "src.rs", "generator.ts"] {
263            assert!(!excluded_file(name), "expected {name} to be retained");
264        }
265    }
266}