internal/control/marks.go
126 lines · 3468 bytes
1package control
2
3import (
4 "fmt"
5 "slices"
6 "strings"
7
8 "gitbay.org/gitbay/internal/store"
9)
10
11// checksMark sums a commit's statuses for a list row at a terminal:
12// "✗ 2 failed" red, "• 1 pending" dim, "✓ 3/3" green, blank with none.
13// The glyph leads so the column reads at a glance; the words stay for
14// a terminal without colour.
15func checksMark(sts []store.CommitStatus) cell {
16 failed, pending, passed := 0, 0, 0
17 for _, s := range sts {
18 switch s.State {
19 case "failure", "error":
20 failed++
21 case "pending":
22 pending++
23 default:
24 passed++
25 }
26 }
27 switch {
28 case len(sts) == 0:
29 return cText("")
30 case failed > 0:
31 return cMark(fmt.Sprintf("✗ %d failed", failed), sgrRed)
32 case pending > 0:
33 return cMark(fmt.Sprintf("• %d pending", pending), sgrDim)
34 }
35 return cMark(fmt.Sprintf("✓ %d/%d", passed, len(sts)), sgrGreen)
36}
37
38// reviewMark is where review of an open merge request stands, by the
39// rule the merge gates use: each counting reviewer's latest verdict on
40// the current head. Changes requested outranks an approval; a request
41// for the viewer's own review is what waits on them.
42func reviewMark(mr store.MR, reviews []store.MRReview, counts map[string]bool, viewer string) cell {
43 latest := map[string]string{}
44 for _, r := range reviews {
45 if r.Stale || r.Reviewer == mr.Author || !counts[r.Reviewer] {
46 continue
47 }
48 latest[r.Reviewer] = r.Verdict
49 }
50 approved := 0
51 for _, v := range latest {
52 switch v {
53 case "request_changes":
54 return cMark("changes requested", sgrRed)
55 case "approve":
56 approved++
57 }
58 }
59 switch {
60 case approved > 0:
61 return cMark("approved", sgrGreen)
62 case slices.Contains(mr.ReviewRequests, viewer):
63 return cMark("review requested", sgrYellow)
64 case len(mr.ReviewRequests) > 0:
65 return cMark("requested", sgrDim)
66 }
67 return cText("")
68}
69
70// mrMarks is the CHECKS and REVIEW cells for every open merge request
71// on a list page, from one query for statuses and one for reviews.
72func mrMarks(c *Ctx, repo store.Repo, mrs []store.MR) (checks, review map[int64]cell, err error) {
73 checks, review = map[int64]cell{}, map[int64]cell{}
74 var ids []int64
75 var shas []string
76 for _, m := range mrs {
77 if m.State == "open" {
78 ids = append(ids, m.ID)
79 shas = append(shas, m.HeadSHA)
80 }
81 }
82 statuses, err := c.Store.CommitStatusesFor(repo.ID, shas)
83 if err != nil {
84 return nil, nil, err
85 }
86 reviews, err := c.Store.MRReviewsFor(ids)
87 if err != nil {
88 return nil, nil, err
89 }
90 requests, err := c.Store.MRReviewRequestsFor(ids)
91 if err != nil {
92 return nil, nil, err
93 }
94 var all []store.MRReview
95 for _, rs := range reviews {
96 all = append(all, rs...)
97 }
98 counts := ReviewersWhoCount(c.Store, repo, all)
99 for _, m := range mrs {
100 if m.State != "open" {
101 continue
102 }
103 checks[m.ID] = checksMark(statuses[m.HeadSHA])
104 m.ReviewRequests = requests[m.ID]
105 review[m.ID] = reviewMark(m, reviews[m.ID], counts, c.User.Username)
106 }
107 return checks, review, nil
108}
109
110// labelsMark is a list row's labels at a terminal: the first two, then
111// how many more.
112func labelsMark(labels []string) string {
113 if len(labels) <= 2 {
114 return strings.Join(labels, ", ")
115 }
116 return fmt.Sprintf("%s, +%d", strings.Join(labels[:2], ", "), len(labels)-2)
117}
118
119// assigneesMark is a row's assignees, yellow when the viewer is one.
120func assigneesMark(assignees []string, viewer string) cell {
121 s := labelsMark(assignees)
122 if slices.Contains(assignees, viewer) {
123 return cMark(s, sgrYellow)
124 }
125 return cText(s)
126}