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