vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Record the default branch summary when a push lands
8dc56e4e258dd56e2c20776f80d61dbed4993f31 · Keenan Nemetz · 10d ago · parent 20442b59
Record the default branch summary when a push lands The landing card showed the default branch tip and the language it is mostly made of by running `symbolic-ref`, `rev-parse`, `log -1` and `ls-tree -r` per repository on every home page read. Those facts change only when a push moves the branch, so the push now records them. - `repos` carries head_sha, head_subject, head_author, head_when, language and lang_class, and `Index.Entry` returns them with the rest of the row. - `Store.RefreshHead` recomputes the summary and skips the tree walk when the recorded tip is still the branch tip, so a metadata or tag push costs one rev-parse. It runs from post-receive, from the rebuild, and after the default branch changes in settings. - The language mapping moved to `internal/lang`, shared by the web pages and the summary; `fileClass` and `languageFor` are now one table. - The home cards read the head, language, counts, ACL and newest event time from the index. The home page starts no git process at all; the all-git card builder stays as the fallback for a Web without an index. Load harness at concurrency 4 with three repositories: home goes from 243 to 749 req/s, p50 from 16 ms to 5 ms, and the server runs no git child process for those requests.
internal/lang/lang.go +158
| @@ -0,0 +1,158 @@ | |||
| 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 | "strings" | ||
| 13 | |||
| 14 | "github.com/rodent-software/vole/core/git" | ||
| 15 | ) | ||
| 16 | |||
| 17 | // For names the language a filename suggests. Unknown types are "Other"; the | ||
| 18 | // map is a hint, not a classifier. | ||
| 19 | func For(name string) string { | ||
| 20 | switch strings.ToLower(path.Ext(name)) { | ||
| 21 | case ".go": | ||
| 22 | return "Go" | ||
| 23 | case ".js", ".mjs", ".cjs": | ||
| 24 | return "JavaScript" | ||
| 25 | case ".ts", ".tsx": | ||
| 26 | return "TypeScript" | ||
| 27 | case ".jsx": | ||
| 28 | return "JavaScript" | ||
| 29 | case ".py": | ||
| 30 | return "Python" | ||
| 31 | case ".rs": | ||
| 32 | return "Rust" | ||
| 33 | case ".rb": | ||
| 34 | return "Ruby" | ||
| 35 | case ".java": | ||
| 36 | return "Java" | ||
| 37 | case ".kt", ".kts": | ||
| 38 | return "Kotlin" | ||
| 39 | case ".c", ".h": | ||
| 40 | return "C" | ||
| 41 | case ".cc", ".cpp", ".hpp": | ||
| 42 | return "C++" | ||
| 43 | case ".cs": | ||
| 44 | return "C#" | ||
| 45 | case ".sh", ".bash", ".zsh": | ||
| 46 | return "Shell" | ||
| 47 | case ".md", ".markdown": | ||
| 48 | return "Markdown" | ||
| 49 | case ".html", ".htm": | ||
| 50 | return "HTML" | ||
| 51 | case ".css", ".scss", ".sass": | ||
| 52 | return "CSS" | ||
| 53 | case ".json": | ||
| 54 | return "JSON" | ||
| 55 | case ".yml", ".yaml": | ||
| 56 | return "YAML" | ||
| 57 | case ".toml": | ||
| 58 | return "TOML" | ||
| 59 | case ".sql": | ||
| 60 | return "SQL" | ||
| 61 | case ".proto": | ||
| 62 | return "Protocol Buffers" | ||
| 63 | case ".lua": | ||
| 64 | return "Lua" | ||
| 65 | case ".php": | ||
| 66 | return "PHP" | ||
| 67 | case ".swift": | ||
| 68 | return "Swift" | ||
| 69 | case ".ex", ".exs": | ||
| 70 | return "Elixir" | ||
| 71 | case ".zig": | ||
| 72 | return "Zig" | ||
| 73 | } | ||
| 74 | return "Other" | ||
| 75 | } | ||
| 76 | |||
| 77 | // Class is the CSS class a filename's language is drawn with. It is empty for a | ||
| 78 | // name the table does not know. | ||
| 79 | func Class(name string) string { | ||
| 80 | switch strings.ToLower(path.Ext(name)) { | ||
| 81 | case ".go": | ||
| 82 | return "lang-go" | ||
| 83 | case ".rs": | ||
| 84 | return "lang-rust" | ||
| 85 | case ".js", ".mjs", ".cjs", ".jsx": | ||
| 86 | return "lang-js" | ||
| 87 | case ".ts", ".tsx": | ||
| 88 | return "lang-ts" | ||
| 89 | case ".py": | ||
| 90 | return "lang-py" | ||
| 91 | case ".rb": | ||
| 92 | return "lang-rb" | ||
| 93 | case ".java", ".kt", ".scala": | ||
| 94 | return "lang-jvm" | ||
| 95 | case ".c", ".h": | ||
| 96 | return "lang-c" | ||
| 97 | case ".cc", ".cpp", ".cxx", ".hpp": | ||
| 98 | return "lang-cpp" | ||
| 99 | case ".cs": | ||
| 100 | return "lang-cs" | ||
| 101 | case ".php": | ||
| 102 | return "lang-php" | ||
| 103 | case ".sh", ".bash", ".zsh", ".fish": | ||
| 104 | return "lang-sh" | ||
| 105 | case ".html", ".htm": | ||
| 106 | return "lang-html" | ||
| 107 | case ".css", ".scss", ".sass": | ||
| 108 | return "lang-css" | ||
| 109 | case ".json", ".yaml", ".yml", ".toml", ".ini", ".xml", ".conf": | ||
| 110 | return "lang-config" | ||
| 111 | case ".md", ".markdown", ".mdown", ".mkd", ".txt", ".rst": | ||
| 112 | return "lang-text" | ||
| 113 | case ".sql": | ||
| 114 | return "lang-sql" | ||
| 115 | case ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico": | ||
| 116 | return "lang-image" | ||
| 117 | case ".lock": | ||
| 118 | return "lang-lock" | ||
| 119 | } | ||
| 120 | switch strings.ToLower(name) { | ||
| 121 | case "dockerfile", "containerfile", "makefile", "makefile.am", "cmakelists.txt": | ||
| 122 | return "lang-build" | ||
| 123 | case "go.mod", "go.sum": | ||
| 124 | return "lang-go" | ||
| 125 | } | ||
| 126 | return "" | ||
| 127 | } | ||
| 128 | |||
| 129 | // Top picks the language with the most files in a tree. Counting files rather | ||
| 130 | // than bytes is deliberate: one generated binary or lock file should not decide | ||
| 131 | // the label. Unknown types are ignored unless the tree contains nothing else. | ||
| 132 | func Top(tree []git.Entry) (name, class string) { | ||
| 133 | counts := map[string]int{} | ||
| 134 | sample := map[string]string{} | ||
| 135 | for _, e := range tree { | ||
| 136 | if e.Type != "blob" { | ||
| 137 | continue | ||
| 138 | } | ||
| 139 | l := For(e.Name) | ||
| 140 | counts[l]++ | ||
| 141 | if _, ok := sample[l]; !ok { | ||
| 142 | sample[l] = e.Name | ||
| 143 | } | ||
| 144 | } | ||
| 145 | best := "" | ||
| 146 | for l, n := range counts { | ||
| 147 | if l == "Other" { | ||
| 148 | continue | ||
| 149 | } | ||
| 150 | if best == "" || n > counts[best] || (n == counts[best] && l < best) { | ||
| 151 | best = l | ||
| 152 | } | ||
| 153 | } | ||
| 154 | if best == "" { | ||
| 155 | return "", "" | ||
| 156 | } | ||
| 157 | return best, Class(sample[best]) | ||
| 158 | } | ||