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.
Build out the forge web UI
f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264
Build out the forge web UI Turn the web surface into a compact, server-rendered forge with HTMX as the progressive dynamic layer. - Split templates and assets into embedded files; add a tokenised dark theme, Lucide icons, and vendored HTMX. - Add the repository Overview page and a separate chronological Activity feed with type filters and HTMX fragment updates. - Finish the issue and change workflows: create pages, comments, close/reopen, labels and assignees, approval, discussion, line-anchored review comments, and HTMX list filters. - Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware code browsing, and a repository Runs list. - Add a loopback-only development login for local write testing, with explicit configuration and listener checks. - Extend tests for the new pages, fragments, labels, assignees, anchors, and development login.
core/git/stats.go +62
| @@ -0,0 +1,62 @@ | |||
| 1 | package git | ||
| 2 | |||
| 3 | // Repository statistics. These are read from git on demand and never stored: | ||
| 4 | // the same rule as every other page, and it means a stat cannot go stale after a | ||
| 5 | // push. The calls are bounded (one shortlog, one tree walk) because the overview | ||
| 6 | // is the first page a visitor sees. | ||
| 7 | |||
| 8 | import ( | ||
| 9 | "fmt" | ||
| 10 | "strconv" | ||
| 11 | "strings" | ||
| 12 | ) | ||
| 13 | |||
| 14 | // Contributor is one author in shortlog order (most commits first). | ||
| 15 | type Contributor struct { | ||
| 16 | Name string | ||
| 17 | Email string | ||
| 18 | Commits int | ||
| 19 | } | ||
| 20 | |||
| 21 | // Contributors summarises the authors reachable from rev. Merge commits are | ||
| 22 | // excluded: a maintainer who merges ten branches did not write those changes, | ||
| 23 | // and counting the merges would rank them above the people who did. | ||
| 24 | func (r *Repo) Contributors(rev string) ([]Contributor, error) { | ||
| 25 | if rev == "" { | ||
| 26 | return nil, nil | ||
| 27 | } | ||
| 28 | out, errs, err := r.run(nil, "shortlog", "-sne", "--no-merges", rev) | ||
| 29 | if err != nil { | ||
| 30 | return nil, fmt.Errorf("shortlog %s: %w: %s", rev, err, errs) | ||
| 31 | } | ||
| 32 | var list []Contributor | ||
| 33 | for _, line := range strings.Split(string(out), "\n") { | ||
| 34 | line = strings.TrimRight(line, "\r") | ||
| 35 | if strings.TrimSpace(line) == "" { | ||
| 36 | continue | ||
| 37 | } | ||
| 38 | count, identity, ok := strings.Cut(line, "\t") | ||
| 39 | if !ok { | ||
| 40 | // shortlog separates with a tab; a line without one is not a record. | ||
| 41 | continue | ||
| 42 | } | ||
| 43 | n, err := strconv.Atoi(strings.TrimSpace(count)) | ||
| 44 | if err != nil || n == 0 { | ||
| 45 | continue | ||
| 46 | } | ||
| 47 | name, email := splitIdentity(identity) | ||
| 48 | list = append(list, Contributor{Name: name, Email: email, Commits: n}) | ||
| 49 | } | ||
| 50 | return list, nil | ||
| 51 | } | ||
| 52 | |||
| 53 | // splitIdentity turns "Name <email>" into its parts. A malformed identity keeps | ||
| 54 | // the whole string as the name rather than dropping the author. | ||
| 55 | func splitIdentity(s string) (name, email string) { | ||
| 56 | s = strings.TrimSpace(s) | ||
| 57 | i := strings.LastIndex(s, " <") | ||
| 58 | if i < 0 || !strings.HasSuffix(s, ">") { | ||
| 59 | return s, "" | ||
| 60 | } | ||
| 61 | return strings.TrimSpace(s[:i]), s[i+2 : len(s)-1] | ||
| 62 | } | ||