- commit
- ae4dc9b
- parent
- 68e303f
- author
- Eric Bower
- date
- 2024-05-31 12:54:31 -0400 EDT
refactor: use templates
7 files changed,
+275,
-109
+19,
-0
1@@ -0,0 +1,19 @@
2+{{define "base"}}
3+<!doctype html>
4+<html lang="en">
5+ <head>
6+ <meta charset='utf-8'>
7+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8+ <title>{{template "title" .}}</title>
9+
10+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11+
12+ <meta name="keywords" content="git, collaboration, patch, requests" />
13+ {{template "meta" .}}
14+
15+ <link rel="stylesheet" href="https://pico.sh/smol.css" />
16+ <link rel="stylesheet" href="/syntax.css" />
17+ </head>
18+ <body class="container">{{template "body" .}}</body>
19+</html>
20+{{end}}
+27,
-0
1@@ -0,0 +1,27 @@
2+{{template "base" .}}
3+
4+{{define "title"}}{{.Pr.Title}} - pr patches{{end}}
5+
6+{{define "meta"}}
7+{{end}}
8+
9+{{define "body"}}
10+{{template "pr-header" .}}
11+
12+<main>
13+ <div class="group">
14+ {{range .Patches}}
15+ <div class="box">
16+ <div class="group-h">
17+ <h2 class="text-lg m-0 p-0">
18+ {{if .Review}}<code>review</code>{{end}}
19+ <span>{{.Title}}</span>
20+ </h2>
21+ </div>
22+ <div>{{.DiffStr}}</div>
23+ </div>
24+ {{end}}
25+ </div>
26+ <hr />
27+</main>
28+{{end}}
+31,
-0
1@@ -0,0 +1,31 @@
2+{{template "base" .}}
3+
4+{{define "title"}}{{.Pr.Title}} - pr summary{{end}}
5+
6+{{define "meta"}}
7+{{end}}
8+
9+{{define "body"}}
10+{{template "pr-header" .}}
11+
12+<main>
13+ <div class="group">
14+ {{range .Patches}}
15+ <div class="box">
16+ <h2 class="text-lg m-0 p-0">
17+ {{if .Review}}<code>review</code>{{end}}
18+ <span>{{.Title}}</span>
19+ </h2>
20+ <div class="group-h text-sm">
21+ <code>{{.AuthorName}} <{{.AuthorEmail}}></code>
22+ <date>{{.AuthorDate}}</date>
23+ </div>
24+
25+ <div>
26+ <div>{{.Body}}</div>
27+ </div>
28+ </div>
29+ {{end}}
30+ </div>
31+</main>
32+{{end}}
+27,
-0
1@@ -0,0 +1,27 @@
2+{{define "pr-header"}}
3+<header>
4+ <h1 class="text-2xl mb">
5+ <a href="{{.Repo.Url}}">{{.Repo.Text}}</a>
6+ <span> / {{.Pr.Title}} <code>#{{.Pr.ID}}</code></span>
7+ </h1>
8+
9+ <div class="text-sm">
10+ <code>{{.Pr.Status}}</code>
11+ <span>opened on <date>{{.Pr.Date}}</date> by</span>
12+ <code>{{.Pr.Pubkey}}</code>
13+ </div>
14+
15+ <div class="mt">
16+ {{if eq .Page "pr"}}
17+ <strong>summary</strong> · <a href="{{.PatchesUrl}}">patches</a>
18+ {{else}}
19+ <a href="{{.SummaryUrl}}">summary</a> · <strong>patches</strong>
20+ {{end}}
21+ </div>
22+
23+ <div>
24+ <span>Submit change to patch: </span>
25+ <code>git format-patch -1 HEAD --stdout | ssh pr.pico.sh pr add {{.Pr.ID}}</code>
26+ </div>
27+</header>
28+{{end}}
+28,
-0
1@@ -0,0 +1,28 @@
2+{{template "base" .}}
3+
4+{{define "title"}}{{.ID}} - repo{{end}}
5+
6+{{define "meta"}}
7+{{end}}
8+
9+{{define "body"}}
10+<header>
11+ <h1 class="text-2xl mb"><a href="/">repos</a> / {{.ID}}</h1>
12+ <div class="group">
13+ <div><code>git clone {{.CloneAddr}}</code></div>
14+ <div>Submit patch request: <code>git format-patch -1 HEAD --stdout | ssh pr.pico.sh pr create {{.ID}}</code></div>
15+ </div>
16+</header>
17+<main>
18+ {{range .Prs}}
19+ <article class="box">
20+ <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
21+ <div class="text-sm">
22+ <code>#{{.ID}}</code>
23+ <span>opened on <date>{{.Date}}</date> by </span>
24+ <code>{{.Pubkey}}</code>
25+ </div>
26+ </article>
27+ {{end}}
28+</main>
29+{{end}}
+19,
-0
1@@ -0,0 +1,19 @@
2+{{template "base" .}}
3+
4+{{define "title"}}repo list{{end}}
5+
6+{{define "meta"}}
7+{{end}}
8+
9+{{define "body"}}
10+<header>
11+ <h1 class="text-2xl mb">Repos</h1>
12+</header>
13+<main>
14+ <ul>
15+ {{range .Repos}}
16+ <li><a href="{{.Url}}">{{.Text}}</a></li>
17+ {{end}}
18+ </ul>
19+</main>
20+{{end}}
M
web.go
+124,
-109
1@@ -3,6 +3,7 @@ package git
2 import (
3 "bytes"
4 "context"
5+ "embed"
6 "fmt"
7 "html/template"
8 "log/slog"
9@@ -18,6 +19,9 @@ import (
10 "github.com/alecthomas/chroma/v2/styles"
11 )
12
13+//go:embed tmpl/*
14+var tmplFS embed.FS
15+
16 type WebCtx struct {
17 Pr *PrCmd
18 Backend *Backend
19@@ -60,36 +64,34 @@ func ctxMdw(ctx context.Context, handler http.HandlerFunc) http.HandlerFunc {
20 }
21 }
22
23-type TemplateData struct {
24- Title string
25- Body template.HTML
26+func getTemplate(file string) *template.Template {
27+ tmpl := template.Must(
28+ template.ParseFS(
29+ tmplFS,
30+ filepath.Join("tmpl", file),
31+ filepath.Join("tmpl", "pr-header.html"),
32+ filepath.Join("tmpl", "base.html"),
33+ ),
34+ )
35+ return tmpl
36 }
37
38-func getTemplate() *template.Template {
39- str := `<!doctype html>
40-<html lang="en">
41- <head>
42- <title>{{.Title}}</title>
43- <link rel="stylesheet" href="https://pico.sh/smol.css" />
44- <link rel="stylesheet" href="/syntax.css" />
45- </head>
46- <body class="container">
47- {{.Body}}
48- </body>
49-</html>`
50- tmpl := template.Must(template.New("main").Parse(str))
51- return tmpl
52+type LinkData struct {
53+ Url template.URL
54+ Text string
55+}
56+
57+type RepoListData struct {
58+ Repos []LinkData
59 }
60
61 func repoListHandler(w http.ResponseWriter, r *http.Request) {
62 web, err := getWebCtx(r)
63 if err != nil {
64- fmt.Println(err)
65 w.WriteHeader(http.StatusInternalServerError)
66 return
67 }
68
69- str := `<h1 class="text-2xl">repos</h1>`
70 repos, err := web.Pr.GetRepos()
71 if err != nil {
72 web.Pr.Backend.Logger.Error("cannot get repos", "err", err)
73@@ -97,27 +99,39 @@ func repoListHandler(w http.ResponseWriter, r *http.Request) {
74 return
75 }
76
77- str += "<ul>"
78+ repoUrls := []LinkData{}
79 for _, repo := range repos {
80- str += fmt.Sprintf(
81- `<li><a href="%s">%s</a></li>`,
82- template.URL("/repos/"+repo.ID),
83- repo.ID,
84- )
85+ url := LinkData{
86+ Url: template.URL("/repos/" + repo.ID),
87+ Text: repo.ID,
88+ }
89+ repoUrls = append(repoUrls, url)
90 }
91- str += "</ul>"
92
93 w.Header().Set("content-type", "text/html")
94- tmpl := getTemplate()
95- err = tmpl.Execute(w, TemplateData{
96- Title: "Repos",
97- Body: template.HTML(str),
98+ tmpl := getTemplate("repo-list.html")
99+ err = tmpl.Execute(w, RepoListData{
100+ Repos: repoUrls,
101 })
102 if err != nil {
103 fmt.Println(err)
104 }
105 }
106
107+type PrListData struct {
108+ LinkData
109+ ID int64
110+ Pubkey string
111+ Date string
112+ Status string
113+}
114+
115+type RepoDetailData struct {
116+ ID string
117+ CloneAddr string
118+ Prs []PrListData
119+}
120+
121 func repoHandler(w http.ResponseWriter, r *http.Request) {
122 repoID := r.PathValue("id")
123
124@@ -135,12 +149,6 @@ func repoHandler(w http.ResponseWriter, r *http.Request) {
125 return
126 }
127
128- str := fmt.Sprintf(`<h1 class="text-2xl"><a href="/">repos</a> / %s</h1>`, repo.ID)
129- str += `<div class="group">`
130- str += fmt.Sprintf(`<div><code>git clone %s</code></div>`, repo.CloneAddr)
131- str += fmt.Sprintf(`<div>Submit patch request: <code>git format-patch --stdout | ssh pr.pico.sh pr create %s</code></div>`, repo.ID)
132- str += `<div class="group"></div>`
133-
134 prs, err := web.Pr.GetPatchRequestsByRepoID(repoID)
135 if err != nil {
136 web.Logger.Error("cannot get prs", "err", err)
137@@ -148,62 +156,65 @@ func repoHandler(w http.ResponseWriter, r *http.Request) {
138 return
139 }
140
141- if len(prs) == 0 {
142- str += "No PRs found for repo"
143- }
144-
145+ prList := []PrListData{}
146 for _, curpr := range prs {
147- row := `
148-<div class="group-h">
149- <div>%d</div>
150- <div><a href="%s">%s</a></div>
151- <div>%s</div>
152-</div>`
153- str += fmt.Sprintf(
154- row,
155- curpr.ID,
156- template.URL(fmt.Sprintf("/prs/%d", curpr.ID)),
157- curpr.Name,
158- curpr.Pubkey,
159- )
160+ ls := PrListData{
161+ ID: curpr.ID,
162+ Pubkey: curpr.Pubkey,
163+ LinkData: LinkData{
164+ Url: template.URL(fmt.Sprintf("/prs/%d", curpr.ID)),
165+ Text: curpr.Name,
166+ },
167+ Date: curpr.CreatedAt.Format(time.RFC3339),
168+ Status: curpr.Status,
169+ }
170+ prList = append(prList, ls)
171 }
172
173 w.Header().Set("content-type", "text/html")
174- tmpl := getTemplate()
175- err = tmpl.Execute(w, TemplateData{
176- Title: "Patch Requests",
177- Body: template.HTML(str),
178+ tmpl := getTemplate("repo-detail.html")
179+ err = tmpl.Execute(w, RepoDetailData{
180+ ID: repo.ID,
181+ CloneAddr: repo.CloneAddr,
182+ Prs: prList,
183 })
184 if err != nil {
185 fmt.Println(err)
186 }
187 }
188
189-func header(repo *Repo, pr *PatchRequest, page string) string {
190- str := fmt.Sprintf(`<h1 class="text-2xl"><a href="/repos/%s">%s</a> / %s</h1>`, repo.ID, repo.ID, pr.Name)
191- str += fmt.Sprintf("<div>[%s] %s %s</div>", pr.Status, pr.CreatedAt.Format(time.DateTime), pr.Pubkey)
192- if page == "pr" {
193- str += fmt.Sprintf(`<div><strong>summary</strong> · <a href="/prs/%d/patches">patches</a></div>`, pr.ID)
194- } else {
195- str += fmt.Sprintf(`<div><a href="/prs/%d">summary</a> · <strong>patches</strong></div>`, pr.ID)
196- }
197+type PrData struct {
198+ ID int64
199+ Title string
200+ Date string
201+ Pubkey string
202+ Status string
203+}
204
205- str += fmt.Sprintf(`<div>Submit change to patch: <code>git format-patch HEAD~1 --stdout | ssh pr.pico.sh pr add %d</code></div>`, pr.ID)
206- return str
207+type PatchData struct {
208+ *Patch
209+ DiffStr template.HTML
210+}
211+
212+type PrHeaderData struct {
213+ Page string
214+ Repo LinkData
215+ Pr PrData
216+ PatchesUrl template.URL
217+ SummaryUrl template.URL
218+ Patches []PatchData
219 }
220
221 func prHandler(w http.ResponseWriter, r *http.Request) {
222 id := r.PathValue("id")
223 prID, err := strconv.Atoi(id)
224 if err != nil {
225- fmt.Println(err)
226 w.WriteHeader(http.StatusUnprocessableEntity)
227 return
228 }
229
230 web, err := getWebCtx(r)
231 if err != nil {
232- fmt.Println(err)
233 w.WriteHeader(http.StatusInternalServerError)
234 return
235 }
236@@ -222,9 +233,6 @@ func prHandler(w http.ResponseWriter, r *http.Request) {
237 return
238 }
239
240- str := header(repo, pr, "pr")
241- str += fmt.Sprintf("<p>%s</p>", pr.Text)
242-
243 patches, err := web.Pr.GetPatchesByPrID(int64(prID))
244 if err != nil {
245 web.Logger.Error("cannot get patches", "err", err)
246@@ -232,27 +240,32 @@ func prHandler(w http.ResponseWriter, r *http.Request) {
247 return
248 }
249
250+ patchesData := []PatchData{}
251 for _, patch := range patches {
252- reviewTxt := ""
253- if patch.Review {
254- reviewTxt = "[review]"
255- }
256- str += fmt.Sprintf(
257- "<div>%s\t%s\t%s\t%s <%s>\t%s\n</div>",
258- reviewTxt,
259- patch.Title,
260- truncateSha(patch.CommitSha),
261- patch.AuthorName,
262- patch.AuthorEmail,
263- patch.AuthorDate,
264- )
265+ patchesData = append(patchesData, PatchData{
266+ Patch: patch,
267+ DiffStr: "",
268+ })
269 }
270
271 w.Header().Set("content-type", "text/html")
272- tmpl := getTemplate()
273- err = tmpl.Execute(w, TemplateData{
274- Title: fmt.Sprintf("%s (%s)", pr.Name, pr.Status),
275- Body: template.HTML(str),
276+ tmpl := getTemplate("pr-detail.html")
277+ err = tmpl.Execute(w, PrHeaderData{
278+ Page: "pr",
279+ Repo: LinkData{
280+ Url: template.URL("/repos/" + repo.ID),
281+ Text: repo.ID,
282+ },
283+ SummaryUrl: template.URL(fmt.Sprintf("/prs/%d", pr.ID)),
284+ PatchesUrl: template.URL(fmt.Sprintf("/prs/%d/patches", pr.ID)),
285+ Patches: patchesData,
286+ Pr: PrData{
287+ ID: pr.ID,
288+ Title: pr.Name,
289+ Pubkey: pr.Pubkey,
290+ Date: pr.CreatedAt.Format(time.RFC3339),
291+ Status: pr.Status,
292+ },
293 })
294 if err != nil {
295 fmt.Println(err)
296@@ -263,14 +276,12 @@ func prPatchesHandler(w http.ResponseWriter, r *http.Request) {
297 id := r.PathValue("id")
298 prID, err := strconv.Atoi(id)
299 if err != nil {
300- fmt.Println(err)
301 w.WriteHeader(http.StatusUnprocessableEntity)
302 return
303 }
304
305 web, err := getWebCtx(r)
306 if err != nil {
307- fmt.Println(err)
308 w.WriteHeader(http.StatusInternalServerError)
309 return
310 }
311@@ -289,8 +300,6 @@ func prPatchesHandler(w http.ResponseWriter, r *http.Request) {
312 return
313 }
314
315- str := header(repo, pr, "patches")
316-
317 patches, err := web.Pr.GetPatchesByPrID(int64(prID))
318 if err != nil {
319 web.Pr.Backend.Logger.Error("cannot get patches", "err", err)
320@@ -298,32 +307,38 @@ func prPatchesHandler(w http.ResponseWriter, r *http.Request) {
321 return
322 }
323
324+ patchesData := []PatchData{}
325 for _, patch := range patches {
326- rev := ""
327- if patch.Review {
328- rev = "[review]"
329- }
330 diffStr, err := parseText(web.Formatter, web.Theme, patch.RawText)
331 if err != nil {
332 w.WriteHeader(http.StatusUnprocessableEntity)
333 return
334 }
335
336- row := `
337-<h2 class="text-xl">%s %s</h2>
338-<div>%s</div>`
339- str += fmt.Sprintf(
340- row,
341- rev, patch.Title,
342- diffStr,
343- )
344+ patchesData = append(patchesData, PatchData{
345+ Patch: patch,
346+ DiffStr: template.HTML(diffStr),
347+ })
348 }
349
350 w.Header().Set("content-type", "text/html")
351- tmpl := getTemplate()
352- err = tmpl.Execute(w, TemplateData{
353- Title: fmt.Sprintf("patches - %s", pr.Name),
354- Body: template.HTML(str),
355+ tmpl := getTemplate("pr-detail-patches.html")
356+ err = tmpl.Execute(w, PrHeaderData{
357+ Page: "patches",
358+ Repo: LinkData{
359+ Url: template.URL("/repos/" + repo.ID),
360+ Text: repo.ID,
361+ },
362+ SummaryUrl: template.URL(fmt.Sprintf("/prs/%d", pr.ID)),
363+ PatchesUrl: template.URL(fmt.Sprintf("/prs/%d/patches", pr.ID)),
364+ Patches: patchesData,
365+ Pr: PrData{
366+ ID: pr.ID,
367+ Title: pr.Name,
368+ Pubkey: pr.Pubkey,
369+ Date: pr.CreatedAt.Format(time.RFC3339),
370+ Status: pr.Status,
371+ },
372 })
373 if err != nil {
374 fmt.Println(err)