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}