internal/httpd/badge.go

f65bfc3c4e65c08d17178303b42860fafff79a25
gitbay/internal/httpd/badge.go history · blame · raw

89 lines · 2635 bytes

 1package httpd
 2
 3import (
 4	"fmt"
 5	"net/http"
 6	"strings"
 7)
 8
 9// Status badges. A badge is a small SVG served for public repositories,
10// so a README on any host can show whether the latest build passed.
11// Private repositories 404 like everywhere else — a badge must not leak
12// that a repo exists, let alone its state.
13
14// badgeColors are the shield fills per build state.
15var badgeColors = map[string]string{
16	"success": "#2da44e",
17	"failure": "#cf222e",
18	"running": "#bf8700",
19	"pending": "#bf8700",
20	"unknown": "#6b7280",
21}
22
23// badgeWidth approximates Verdana 11px advance so the pill fits its text
24// without shipping a font metric table.
25func badgeWidth(s string) int {
26	w := 0
27	for _, r := range s {
28		switch {
29		case strings.ContainsRune("iljtIJ1.,:;'", r):
30			w += 4
31		case strings.ContainsRune("mwMW", r):
32			w += 10
33		case r >= 'A' && r <= 'Z':
34			w += 8
35		default:
36			w += 7
37		}
38	}
39	return w + 10
40}
41
42// badgeSVG renders a two-part pill: a grey label and a coloured state.
43func badgeSVG(label, state string) string {
44	color := badgeColors[state]
45	if color == "" {
46		color = badgeColors["unknown"]
47	}
48	lw, sw := badgeWidth(label), badgeWidth(state)
49	total := lw + sw
50	return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20" role="img" aria-label="%s: %s">
51<title>%s: %s</title>
52<rect width="%d" height="20" rx="3" fill="#444d56"/>
53<rect x="%d" width="%d" height="20" rx="3" fill="%s"/>
54<rect x="%d" width="4" height="20" fill="%s"/>
55<g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11">
56<text x="%d" y="14">%s</text>
57<text x="%d" y="14">%s</text>
58</g>
59</svg>`, total, label, state, label, state,
60		total, lw, sw, color, lw, color, lw/2, label, lw+sw/2, state)
61}
62
63// buildBadge answers GET /{owner}/{repo}/badge/build.svg[?job=name].
64func (s *Server) buildBadge(w http.ResponseWriter, r *http.Request) {
65	repo, ok := s.publicRepo(r.PathValue("owner"), strings.TrimSuffix(r.PathValue("repo"), ".git"))
66	if !ok {
67		http.NotFound(w, r)
68		return
69	}
70	job := r.URL.Query().Get("job")
71	label := "build"
72	if job != "" {
73		label = job
74	}
75	state := "unknown"
76	if b, err := s.st.LatestBuild(repo.ID, job); err == nil {
77		state = b.Status
78	}
79	writeBadge(w, label, state)
80}
81
82func writeBadge(w http.ResponseWriter, label, state string) {
83	w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")
84	// Badges are read by other people's caches; keep them briefly fresh
85	// rather than pinned to a stale result.
86	w.Header().Set("Cache-Control", "max-age=60, must-revalidate")
87	w.Header().Set("X-Content-Type-Options", "nosniff")
88	fmt.Fprint(w, badgeSVG(label, state))
89}