133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"embed"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"html/template"
133728e barerepo 1mo
7
"net/http"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
"time"
133728e barerepo 1mo
10
"unicode"
133728e barerepo 1mo
11
"unicode/utf8"
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/cache"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/gitread"
133728e barerepo 1mo
17
//go:embed templates
133728e barerepo 1mo
18
var templateFS embed.FS
133728e barerepo 1mo
20
// Version is the only number the interface shows about itself, per chapter 25.
133728e barerepo 1mo
21
const Version = "0.1.0"
133728e barerepo 1mo
23
// Source is where this program is kept, and it is absolute because an install elsewhere does not hold a copy of it.
133728e barerepo 1mo
24
const Source = "https://barerepo.com/barerepo/server"
133728e barerepo 1mo
26
// pages are parsed once at start, each defining "body" and "footleft" for layout.html.
133728e barerepo 1mo
27
var pages = map[string]*template.Template{}
133728e barerepo 1mo
29
func init() {
133728e barerepo 1mo
30
layout := must(templateFS.ReadFile("templates/layout.html"))
133728e barerepo 1mo
31
entries := must(templateFS.ReadDir("templates"))
133728e barerepo 1mo
32
for _, e := range entries {
133728e barerepo 1mo
33
if e.Name() == "layout.html" {
133728e barerepo 1mo
34
continue
133728e barerepo 1mo
36
body := must(templateFS.ReadFile("templates/" + e.Name()))
133728e barerepo 1mo
37
name := strings.TrimSuffix(e.Name(), ".html")
133728e barerepo 1mo
38
pages[name] = template.Must(template.New(name).Parse(string(layout) + string(body)))
133728e barerepo 1mo
42
func must[T any](v T, err error) T {
133728e barerepo 1mo
43
if err != nil {
133728e barerepo 1mo
44
panic(err)
133728e barerepo 1mo
46
return v
133728e barerepo 1mo
49
// render builds into memory first, because a template that fails mid-write cannot become an error page.
133728e barerepo 1mo
50
func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, data any) {
133728e barerepo 1mo
51
t, ok := pages[name]
133728e barerepo 1mo
52
if !ok {
133728e barerepo 1mo
53
s.oops(w, r, fmt.Errorf("no template %q", name))
133728e barerepo 1mo
54
return
133728e barerepo 1mo
56
var buf strings.Builder
133728e barerepo 1mo
57
if err := t.ExecuteTemplate(&buf, "layout", data); err != nil {
133728e barerepo 1mo
58
s.oops(w, r, err)
133728e barerepo 1mo
59
return
133728e barerepo 1mo
61
w.Header().Set("Content-Type", "text/html; charset=utf-8")
133728e barerepo 1mo
62
w.Write([]byte(buf.String()))
133728e barerepo 1mo
65
// chrome is what every page needs and no page thinks about.
133728e barerepo 1mo
66
type chrome struct {
133728e barerepo 1mo
67
Title string
133728e barerepo 1mo
68
Summary string
133728e barerepo 1mo
69
Version string
133728e barerepo 1mo
70
Source string
133728e barerepo 1mo
71
// Share is what a link preview shows, because the summary above describes the page to a screen reader and reads as a spec anywhere else.
133728e barerepo 1mo
72
Share string
133728e barerepo 1mo
73
// Site is the absolute origin, because a share card cannot follow a relative image.
133728e barerepo 1mo
74
Site string
133728e barerepo 1mo
75
// Card is the share image for this page, and empty means the one for the whole site.
133728e barerepo 1mo
76
Card string
133728e barerepo 1mo
77
// Assets is what the stylesheet url carries, so a changed file is a url no browser has kept.
133728e barerepo 1mo
78
Assets string
133728e barerepo 1mo
81
// Site is the origin a share card quotes, set once at start from the external url.
133728e barerepo 1mo
82
var Site string
133728e barerepo 1mo
84
func newChrome(title, summary string) chrome {
133728e barerepo 1mo
85
return chrome{Title: title, Summary: summary, Version: Version, Source: Source, Assets: assetTag, Site: Site}
133728e barerepo 1mo
88
// newCardChrome is a page whose share image says whose page it is.
133728e barerepo 1mo
89
func newCardChrome(title, summary, card string) chrome {
133728e barerepo 1mo
90
c := newChrome(title, summary)
133728e barerepo 1mo
91
c.Card = card
133728e barerepo 1mo
92
return c
133728e barerepo 1mo
95
// bit is one item in a headline, linked where barerepo has somewhere to send it and plain where it does not.
133728e barerepo 1mo
96
type bit struct {
133728e barerepo 1mo
97
Text string
133728e barerepo 1mo
98
Href string
133728e barerepo 1mo
99
// Danger is the one colour the mockups use in a headline, and only a failed build earns it.
133728e barerepo 1mo
100
Danger bool
133728e barerepo 1mo
103
// plain is a bit that goes nowhere, which is most of a headline.
133728e barerepo 1mo
104
func plain(text string) bit { return bit{Text: text} }
133728e barerepo 1mo
106
type tab struct {
133728e barerepo 1mo
107
Label string
133728e barerepo 1mo
108
Href string
133728e barerepo 1mo
109
On bool
133728e barerepo 1mo
110
// A tab that is not ready still shows but does not link, because a 404 is worse than nothing.
133728e barerepo 1mo
111
Ready bool
133728e barerepo 1mo
114
// built lists the views that exist, and is the only place that decides whether a tab links.
133728e barerepo 1mo
115
var built = map[string]bool{
133728e barerepo 1mo
116
"log": true,
133728e barerepo 1mo
117
"files": true,
133728e barerepo 1mo
118
"config": true,
133728e barerepo 1mo
119
"threads": true,
133728e barerepo 1mo
120
"runs": true,
133728e barerepo 1mo
121
"releases": true,
133728e barerepo 1mo
124
// repoTabs is the row under the breadcrumb, where config is barely a special case. Chapter 24.
133728e barerepo 1mo
125
func repoTabs(owner, name, active, branch string, threads int) []tab {
133728e barerepo 1mo
126
base := "/" + owner + "/" + name
133728e barerepo 1mo
127
if branch == "" {
133728e barerepo 1mo
128
branch = "HEAD"
133728e barerepo 1mo
129
}
133728e barerepo 1mo
130
labels := []struct{ key, label, href string }{
133728e barerepo 1mo
131
{"log", "log", base},
133728e barerepo 1mo
132
{"files", "files", base + "/files"},
133728e barerepo 1mo
133
{"threads", threadLabel(threads), base + "/threads"},
133728e barerepo 1mo
134
{"runs", "runs", base + "/runs"},
133728e barerepo 1mo
135
// The mockups draw five tabs, and none of them links to releases, so its page had no way in.
133728e barerepo 1mo
136
{"releases", "releases", base + "/releases"},
133728e barerepo 1mo
137
{"config", "config", base + "/config"},
133728e barerepo 1mo
138
}
133728e barerepo 1mo
139
out := make([]tab, 0, len(labels))
133728e barerepo 1mo
140
for _, l := range labels {
133728e barerepo 1mo
141
out = append(out, tab{
133728e barerepo 1mo
142
Label: l.label,
133728e barerepo 1mo
143
Href: l.href,
133728e barerepo 1mo
144
On: l.key == active,
133728e barerepo 1mo
145
Ready: built[l.key],
133728e barerepo 1mo
146
})
133728e barerepo 1mo
147
}
133728e barerepo 1mo
148
return out
133728e barerepo 1mo
151
func threadLabel(n int) string {
133728e barerepo 1mo
152
if n == 0 {
133728e barerepo 1mo
153
return "threads"
133728e barerepo 1mo
154
}
133728e barerepo 1mo
155
return fmt.Sprintf("threads %d", n)
133728e barerepo 1mo
158
// commitView is one entry on the log page.
133728e barerepo 1mo
159
type commitView struct {
133728e barerepo 1mo
160
gitread.Commit
133728e barerepo 1mo
161
Ago string
133728e barerepo 1mo
162
Summary string
133728e barerepo 1mo
163
Href string
133728e barerepo 1mo
164
// AuthorHref is the account page, where a name that is not an account still reads as a name.
133728e barerepo 1mo
165
AuthorHref string
133728e barerepo 1mo
166
// Diff says there is something to open, so a commit with no inlined hunk gets no disclosure.
133728e barerepo 1mo
167
Diff bool
133728e barerepo 1mo
168
Files []fileView
133728e barerepo 1mo
169
// SigNote is what the mark means, in the same words the commit page uses.
133728e barerepo 1mo
170
SigNote string
133728e barerepo 1mo
173
type fileView struct {
133728e barerepo 1mo
174
gitread.FileDiff
133728e barerepo 1mo
175
Hunks []hunkView
133728e barerepo 1mo
178
type hunkView struct {
133728e barerepo 1mo
179
Header string
133728e barerepo 1mo
180
Lines []lineView
133728e barerepo 1mo
183
// lineView carries the prefix as data, so the template never decides what a diff line means.
133728e barerepo 1mo
184
type lineView struct {
133728e barerepo 1mo
185
Kind byte
133728e barerepo 1mo
186
Prefix string
133728e barerepo 1mo
187
Text string
133728e barerepo 1mo
188
New int
133728e barerepo 1mo
189
CommentHref string
133728e barerepo 1mo
192
// Cache keeps rendered diffs, which chapter 25 asks for by name, keyed by the commit that made them.
133728e barerepo 1mo
193
var Cache *cache.Disk
133728e barerepo 1mo
195
func newCommitView(owner, name string, c gitread.Commit) commitView {
133728e barerepo 1mo
196
return commitView{
133728e barerepo 1mo
197
Commit: c,
133728e barerepo 1mo
198
Ago: ago(c.When),
133728e barerepo 1mo
199
Summary: changeSummary(c),
133728e barerepo 1mo
200
Href: "/" + owner + "/" + name + "/commit/" + c.SHA,
133728e barerepo 1mo
201
}
133728e barerepo 1mo
204
// SignatureNote is what the commit page says about a signature, and empty means there is none.
133728e barerepo 1mo
205
func SignatureNote(c gitread.Commit, account string) (string, bool) {
133728e barerepo 1mo
206
if !c.Signed {
133728e barerepo 1mo
207
return "", false
133728e barerepo 1mo
208
}
133728e barerepo 1mo
209
switch c.SigState {
133728e barerepo 1mo
210
case "G", "U":
133728e barerepo 1mo
211
// The account that published the key, because the name inside a key is typed by its owner.
133728e barerepo 1mo
212
if account != "" {
133728e barerepo 1mo
213
return "signed by " + account, false
133728e barerepo 1mo
214
}
133728e barerepo 1mo
215
return "signed by " + c.Signer, false
133728e barerepo 1mo
216
case "B":
133728e barerepo 1mo
217
return "the signature does not match this commit", true
133728e barerepo 1mo
218
case "X", "Y", "R":
133728e barerepo 1mo
219
return "signed on a key that is expired or revoked", true
133728e barerepo 1mo
220
}
133728e barerepo 1mo
221
// Nobody has published the key this was signed with, so barerepo says so and claims nothing.