internal/httpd/feed.go
140 lines · 4257 bytes
1package httpd
2
3import (
4 "encoding/json"
5 "fmt"
6 "slices"
7 "strings"
8 "time"
9
10 "gitbay.org/gitbay/internal/store"
11)
12
13// feedLine is one activity entry, already phrased and linked.
14type feedLine struct {
15 Actor string
16 Verb string // "opened issue", "merged", "ran 2 jobs on"
17 Ref string // "#12", "!35", "v0.4.0", a short sha
18 Repo string
19 URL string
20 When string // the stored timestamp, for anything still reading it raw
21 WhenT time.Time // parsed from When, for ago/whenT rendering
22 State string // a build run's combined status; empty for anything else
23 Jobs []string // job names folded into a build run
24 sha string // the commit a build event fired on, for fold-matching
25}
26
27// feedLines turns stored events into readable lines. An unknown kind
28// still shows: the feed says what happened even for events added later.
29// Build events on the same commit, adjacent in the input, fold into one
30// "run" line (D04): its State is the worst of the folded jobs' outcomes,
31// via worstStatus — the same rule the builds tab uses for a run's status.
32// A repeated job name ends the line and starts the next, so a scheduled
33// job firing daily on an unchanged tip reads as one line a day rather than
34// "ran 9 jobs on" one commit (#240). groupRuns separates its runs by
35// created_at instead; these events are recorded when a build finishes, one
36// per job, so there is no queue moment here to key on. The cost is that
37// the oldest scheduled line on a commit folds in the push's jobs, which
38// have not been seen yet on that line.
39func feedLines(events []store.FeedEvent) []feedLine {
40 out := make([]feedLine, 0, len(events))
41 statuses := make([][]string, 0, len(events))
42 for _, e := range events {
43 var d struct {
44 Number int64 `json:"number"`
45 Job string `json:"job"`
46 Tag string `json:"tag"`
47 SHA string `json:"sha"`
48 }
49 json.Unmarshal([]byte(e.Data), &d)
50 kind, rest, _ := strings.Cut(e.Kind, ".")
51
52 if kind == "build" && d.SHA != "" {
53 if n := len(out); n > 0 && out[n-1].sha == d.SHA && out[n-1].Repo == e.RepoPath &&
54 !slices.Contains(out[n-1].Jobs, d.Job) {
55 i := n - 1
56 out[i].Jobs = append(out[i].Jobs, d.Job)
57 statuses[i] = append(statuses[i], rest)
58 out[i].Verb = fmt.Sprintf("ran %d jobs on", len(out[i].Jobs))
59 out[i].Ref = fmt.Sprintf("%.10s", d.SHA)
60 out[i].URL = fmt.Sprintf("/%s/commit/%s", e.RepoPath, d.SHA)
61 out[i].State = worstStatus(statuses[i])
62 continue
63 }
64 }
65
66 l := feedLine{Actor: e.Actor, Repo: e.RepoPath, When: e.CreatedAt, WhenT: parseEventTime(e.CreatedAt)}
67 if l.Actor == "" {
68 l.Actor = "gitbay"
69 }
70 var st []string
71 switch kind {
72 case "issue":
73 l.Verb, l.Ref = issueVerb(rest), fmt.Sprintf("#%d", d.Number)
74 l.URL = fmt.Sprintf("/%s/issues/%d", e.RepoPath, d.Number)
75 case "mr":
76 l.Verb, l.Ref = mrVerb(rest), fmt.Sprintf("!%d", d.Number)
77 l.URL = fmt.Sprintf("/%s/mrs/%d", e.RepoPath, d.Number)
78 case "build":
79 l.Verb, l.Ref = "build "+rest, d.Job
80 l.URL = fmt.Sprintf("/%s/builds/%d", e.RepoPath, d.Number)
81 if d.SHA != "" {
82 l.sha = d.SHA
83 l.Jobs = []string{d.Job}
84 l.State = rest
85 st = []string{rest}
86 }
87 case "release":
88 l.Verb, l.Ref = "released", d.Tag
89 l.URL = fmt.Sprintf("/%s/releases", e.RepoPath)
90 case "repo":
91 l.Verb = "repository " + rest
92 l.URL = "/" + e.RepoPath
93 default:
94 l.Verb = e.Kind
95 l.URL = "/" + e.RepoPath
96 }
97 out = append(out, l)
98 statuses = append(statuses, st)
99 }
100 return out
101}
102
103// parseEventTime parses a stored RFC3339 timestamp for ago/whenT
104// rendering; an unparseable value (or none) comes back zero rather than
105// guessing.
106func parseEventTime(s string) time.Time {
107 t, err := time.Parse(time.RFC3339Nano, s)
108 if err != nil {
109 return time.Time{}
110 }
111 return t
112}
113
114func issueVerb(s string) string {
115 switch s {
116 case "created":
117 return "opened issue"
118 case "closed":
119 return "closed issue"
120 case "reopened":
121 return "reopened issue"
122 case "commented":
123 return "commented on"
124 }
125 return "issue " + s
126}
127
128func mrVerb(s string) string {
129 switch s {
130 case "created":
131 return "opened merge request"
132 case "merged":
133 return "merged"
134 case "commented":
135 return "commented on"
136 case "closed":
137 return "closed merge request"
138 }
139 return "merge request " + s
140}