Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

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 }