repos / git-pr

a self-hosted git collaboration server
git clone https://github.com/picosh/git-pr.git

commit
ae4dc9b
parent
68e303f
author
Eric Bower
date
2024-05-31 12:54:31 -0400 EDT
refactor: use templates
7 files changed,  +275, -109
M web.go
A tmpl/base.html
+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}}
A tmpl/pr-detail-patches.html
+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}}
A tmpl/pr-detail.html
+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}} &lt;{{.AuthorEmail}}&gt;</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}}
A tmpl/pr-header.html
+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> &middot; <a href="{{.PatchesUrl}}">patches</a>
18+  {{else}}
19+    <a href="{{.SummaryUrl}}">summary</a> &middot; <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}}
A tmpl/repo-detail.html
+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}}
A tmpl/repo-list.html
+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> &middot; <a href="/prs/%d/patches">patches</a></div>`, pr.ID)
194-	} else {
195-		str += fmt.Sprintf(`<div><a href="/prs/%d">summary</a> &middot; <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)