internal/httpd/feed.go

f327db6192d9a0877606a40385b24c0cda29fd2d
gitbay/internal/httpd/feed.go history · blame · raw

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}