vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 1h ago
4b8c9e1d
| 1 | // Package lang names the language a filename suggests and picks the language a |
| 2 | // tree is mostly made of. |
| 3 | // |
| 4 | // It is shared by the web pages and by the index's head summary: the home card |
| 5 | // shows the same answer the repository overview groups by, and the summary is |
| 6 | // computed when a push lands rather than on every page read. |
| 7 | |
| 8 | package lang |
| 9 | |
| 10 | import ( |
| 11 | "path" |
| 12 | "sort" |
| 13 | "strings" |
| 14 | |
| 15 | "rodent.software/vole/core/git" |
| 16 | ) |
| 17 | |
| 18 | // For names the language a filename suggests. Unknown types are "Other"; the |
| 19 | // map is a hint, not a classifier. |
| 20 | func For(name string) string { |
| 21 | switch strings.ToLower(path.Ext(name)) { |
| 22 | case ".go": |
| 23 | return "Go" |
| 24 | case ".js", ".mjs", ".cjs": |
| 25 | return "JavaScript" |
| 26 | case ".ts", ".tsx": |
| 27 | return "TypeScript" |
| 28 | case ".jsx": |
| 29 | return "JavaScript" |
| 30 | case ".py": |
| 31 | return "Python" |
| 32 | case ".rs": |
| 33 | return "Rust" |
| 34 | case ".rb": |
| 35 | return "Ruby" |
| 36 | case ".java": |
| 37 | return "Java" |
| 38 | case ".kt", ".kts": |
| 39 | return "Kotlin" |
| 40 | case ".c", ".h": |
| 41 | return "C" |
| 42 | case ".cc", ".cpp", ".hpp": |
| 43 | return "C++" |
| 44 | case ".cs": |
| 45 | return "C#" |
| 46 | case ".sh", ".bash", ".zsh": |
| 47 | return "Shell" |
| 48 | case ".md", ".markdown": |
| 49 | return "Markdown" |
| 50 | case ".html", ".htm": |
| 51 | return "HTML" |
| 52 | case ".css", ".scss", ".sass": |
| 53 | return "CSS" |
| 54 | case ".json": |
| 55 | return "JSON" |
| 56 | case ".yml", ".yaml": |
| 57 | return "YAML" |
| 58 | case ".toml": |
| 59 | return "TOML" |
| 60 | case ".sql": |
| 61 | return "SQL" |
| 62 | case ".proto": |
| 63 | return "Protocol Buffers" |
| 64 | case ".lua": |
| 65 | return "Lua" |
| 66 | case ".php": |
| 67 | return "PHP" |
| 68 | case ".swift": |
| 69 | return "Swift" |
| 70 | case ".ex", ".exs": |
| 71 | return "Elixir" |
| 72 | case ".zig": |
| 73 | return "Zig" |
| 74 | } |
| 75 | return "Other" |
| 76 | } |
| 77 | |
| 78 | // Class is the CSS class a filename's language is drawn with. It is empty for a |
| 79 | // name the table does not know. |
| 80 | func Class(name string) string { |
| 81 | switch strings.ToLower(path.Ext(name)) { |
| 82 | case ".go": |
| 83 | return "lang-go" |
| 84 | case ".rs": |
| 85 | return "lang-rust" |
| 86 | case ".js", ".mjs", ".cjs", ".jsx": |
| 87 | return "lang-js" |
| 88 | case ".ts", ".tsx": |
| 89 | return "lang-ts" |
| 90 | case ".py": |
| 91 | return "lang-py" |
| 92 | case ".rb": |
| 93 | return "lang-rb" |
| 94 | case ".java", ".kt", ".scala": |
| 95 | return "lang-jvm" |
| 96 | case ".c", ".h": |
| 97 | return "lang-c" |
| 98 | case ".cc", ".cpp", ".cxx", ".hpp": |
| 99 | return "lang-cpp" |
| 100 | case ".cs": |
| 101 | return "lang-cs" |
| 102 | case ".php": |
| 103 | return "lang-php" |
| 104 | case ".sh", ".bash", ".zsh", ".fish": |
| 105 | return "lang-sh" |
| 106 | case ".html", ".htm": |
| 107 | return "lang-html" |
| 108 | case ".css", ".scss", ".sass": |
| 109 | return "lang-css" |
| 110 | case ".json", ".yaml", ".yml", ".toml", ".ini", ".xml", ".conf": |
| 111 | return "lang-config" |
| 112 | case ".md", ".markdown", ".mdown", ".mkd", ".txt", ".rst": |
| 113 | return "lang-text" |
| 114 | case ".sql": |
| 115 | return "lang-sql" |
| 116 | case ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico": |
| 117 | return "lang-image" |
| 118 | case ".lock": |
| 119 | return "lang-lock" |
| 120 | } |
| 121 | switch strings.ToLower(name) { |
| 122 | case "dockerfile", "containerfile", "makefile", "makefile.am", "cmakelists.txt": |
| 123 | return "lang-build" |
| 124 | case "go.mod", "go.sum": |
| 125 | return "lang-go" |
| 126 | } |
| 127 | return "" |
| 128 | } |
| 129 | |
| 130 | // Share is how many bytes one language accounts for in a tree. The overview's |
| 131 | // bar is about size; a card's label is about file count, so the two use |
| 132 | // different counts on purpose. |
| 133 | type Share struct { |
| 134 | Name string |
| 135 | Bytes int64 |
| 136 | } |
| 137 | |
| 138 | // Breakdown groups a tree's blobs by language, largest first, by bytes. Unknown |
| 139 | // types stay in the list under "Other" so the percentages add up. |
| 140 | func Breakdown(tree []git.EntrySize) []Share { |
| 141 | byName := map[string]int64{} |
| 142 | for _, e := range tree { |
| 143 | if e.Type != "blob" { |
| 144 | continue |
| 145 | } |
| 146 | byName[For(e.Name)] += e.Size |
| 147 | } |
| 148 | list := make([]Share, 0, len(byName)) |
| 149 | for name, n := range byName { |
| 150 | list = append(list, Share{Name: name, Bytes: n}) |
| 151 | } |
| 152 | sort.Slice(list, func(i, j int) bool { |
| 153 | if list[i].Bytes != list[j].Bytes { |
| 154 | return list[i].Bytes > list[j].Bytes |
| 155 | } |
| 156 | return list[i].Name < list[j].Name |
| 157 | }) |
| 158 | return list |
| 159 | } |
| 160 | |
| 161 | // TopSizes is Top for a sized listing, which is what a push already reads for |
| 162 | // the language breakdown. |
| 163 | func TopSizes(tree []git.EntrySize) (name, class string) { |
| 164 | entries := make([]git.Entry, len(tree)) |
| 165 | for i, e := range tree { |
| 166 | entries[i] = e.Entry |
| 167 | } |
| 168 | return Top(entries) |
| 169 | } |
| 170 | |
| 171 | // Top picks the language with the most files in a tree. Counting files rather |
| 172 | // than bytes is deliberate: one generated binary or lock file should not decide |
| 173 | // the label. Unknown types are ignored unless the tree contains nothing else. |
| 174 | func Top(tree []git.Entry) (name, class string) { |
| 175 | counts := map[string]int{} |
| 176 | sample := map[string]string{} |
| 177 | for _, e := range tree { |
| 178 | if e.Type != "blob" { |
| 179 | continue |
| 180 | } |
| 181 | l := For(e.Name) |
| 182 | counts[l]++ |
| 183 | if _, ok := sample[l]; !ok { |
| 184 | sample[l] = e.Name |
| 185 | } |
| 186 | } |
| 187 | best := "" |
| 188 | for l, n := range counts { |
| 189 | if l == "Other" { |
| 190 | continue |
| 191 | } |
| 192 | if best == "" || n > counts[best] || (n == counts[best] && l < best) { |
| 193 | best = l |
| 194 | } |
| 195 | } |
| 196 | if best == "" { |
| 197 | return "", "" |
| 198 | } |
| 199 | return best, Class(sample[best]) |
| 200 | } |
| 201 |