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 · 21m ago
4b8c9e1d
| 1 | package web |
| 2 | |
| 3 | // The activity feed as a page of events. |
| 4 | // |
| 5 | // The rows come from forge/index (`Events` and `EventCount`), which keeps a row |
| 6 | // per feed event with a (repository, time) index: a page is an index range read, |
| 7 | // and an unfiltered page total is a stored count. Releases are rows there too, so |
| 8 | // nothing is fetched from git to render a page. |
| 9 | |
| 10 | import ( |
| 11 | "strings" |
| 12 | "time" |
| 13 | |
| 14 | "rodent.software/vole/forge/index" |
| 15 | ) |
| 16 | |
| 17 | // activityEventFromRow maps one indexed event to the event a template renders. |
| 18 | func (w *Web) activityEventFromRow(row index.EventRow, repoName string, withRepo bool) activityEvent { |
| 19 | ev := activityEvent{ |
| 20 | Time: time.Unix(row.At, 0).UTC(), ActorName: row.Actor, |
| 21 | Title: row.Title, Assignees: splitAssignees(row.Assignees), |
| 22 | } |
| 23 | if withRepo { |
| 24 | ev.Repo = repoName |
| 25 | } |
| 26 | base := "/" + repoName |
| 27 | switch row.Kind { |
| 28 | case "release": |
| 29 | ev.Kind, ev.Icon, ev.Detail = "release", "tag", "released" |
| 30 | ev.Actor = row.Actor // the tagger identity, as the tag records it |
| 31 | // A tag's tagger is a Git identity, not the credential that proves an |
| 32 | // account, so it is shown as text and never linked to a profile. |
| 33 | ev.ActorName = "" |
| 34 | ev.URL = base + "/releases/" + row.EntityID |
| 35 | return ev |
| 36 | case "issue": |
| 37 | ev.Kind, ev.Icon = "issue", "circle-dot" |
| 38 | case "change": |
| 39 | ev.Kind, ev.Icon = "change", "git-pull-request" |
| 40 | } |
| 41 | ev.Actor = w.who(row.Actor) |
| 42 | switch row.Type { |
| 43 | case "open": |
| 44 | if row.Kind == "issue" { |
| 45 | ev.Detail = "opened issue" |
| 46 | ev.URL = base + "/issues/" + row.EntityID |
| 47 | } else { |
| 48 | ev.Detail = "opened change" |
| 49 | ev.URL = base + "/changes/" + row.EntityID |
| 50 | } |
| 51 | case "comment": |
| 52 | ev.Icon = "message-square" |
| 53 | if row.Kind == "issue" { |
| 54 | ev.Detail = "commented on issue" |
| 55 | ev.URL = base + "/issues/" + row.ParentID + "#c-" + row.CommentID |
| 56 | } else { |
| 57 | ev.Detail = "commented on change" |
| 58 | ev.URL = base + "/changes/" + row.ParentID + "#c-" + row.CommentID |
| 59 | } |
| 60 | case "approval": |
| 61 | ev.Icon, ev.Detail = "check", "approved change" |
| 62 | ev.URL = base + "/changes/" + row.EntityID |
| 63 | case "state": |
| 64 | if row.Kind == "issue" { |
| 65 | ev.Detail = issueStateDetail(row.State) |
| 66 | ev.URL = base + "/issues/" + row.EntityID |
| 67 | } else { |
| 68 | ev.Detail = requestStateDetail(row.State) |
| 69 | ev.URL = base + "/changes/" + row.EntityID |
| 70 | } |
| 71 | } |
| 72 | return ev |
| 73 | } |
| 74 | |
| 75 | // activityPage returns one page of the feed and the pager that describes it. |
| 76 | func (w *Web) activityPage(repoIDs []string, names map[string]string, |
| 77 | kind, actor, assignee string, withRepo bool, number, perPage int, |
| 78 | urlFor func(int) string, label string) ([]activityEvent, pager, error) { |
| 79 | count, err := w.Index.EventCount(repoIDs, kind, actor, assignee) |
| 80 | if err != nil { |
| 81 | return nil, pager{}, err |
| 82 | } |
| 83 | p := newPager(count, perPage, number, urlFor) |
| 84 | p.Label = label |
| 85 | if p.Total == 0 { |
| 86 | return nil, p, nil |
| 87 | } |
| 88 | from, _ := p.rows() |
| 89 | rows, err := w.Index.Events(repoIDs, kind, actor, assignee, perPage, from) |
| 90 | if err != nil { |
| 91 | return nil, p, err |
| 92 | } |
| 93 | events := make([]activityEvent, 0, len(rows)) |
| 94 | for _, row := range rows { |
| 95 | events = append(events, w.activityEventFromRow(row, names[row.RepoID], withRepo)) |
| 96 | } |
| 97 | return events, p, nil |
| 98 | } |
| 99 | |
| 100 | func splitAssignees(s string) []string { |
| 101 | if s == "" { |
| 102 | return nil |
| 103 | } |
| 104 | return strings.Split(s, "\x1f") |
| 105 | } |
| 106 |