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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 21m ago 4b8c9e1d
history blame raw 3.1 KiB
1package 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
10import (
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.
18func (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.
76func (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
100func splitAssignees(s string) []string {
101 if s == "" {
102 return nil
103 }
104 return strings.Split(s, "\x1f")
105}
106