Logs
erock
created pr with ps-3
on erock
added ps-4
on jolheiser
reviewed pr with ps-5
on jolheiser
changed status
on {"status":"reviewed"}
erock
added ps-9
on jolheiser
changed status
on {"status":"accepted"}
Patchsets
Diff ↕
feat: static assets
Eric Bower <me@erock.io>
Makefile | 4 ++++ web.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+)
1From 3b99dc0adc225ad4f4be24f36d164de82b7f8b01 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 00:27:17 -0400
4Subject: [PATCH 1/4] feat: static assets
5
6---
7 Makefile | 4 ++++
8 web.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 2 files changed, 61 insertions(+)
10
11diff --git a/Makefile b/Makefile
12index cb3a367..bc76fd9 100644
13--- a/Makefile
14+++ b/Makefile
15@@ -26,3 +26,7 @@ bp: bp-setup
16 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
17 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
18 .PHONY: bp
19+
20+smol:
21+ curl https://pico.sh/smol.css -o ./static/smol.css
22+.PHONY: smol
23diff --git a/web.go b/web.go
24index 4e782c8..12db697 100644
25--- a/web.go
26+++ b/web.go
27@@ -6,8 +6,11 @@ import (
28 "embed"
29 "fmt"
30 "html/template"
31+ "io"
32+ "io/fs"
33 "log/slog"
34 "net/http"
35+ "os"
36 "path/filepath"
37 "slices"
38 "strconv"
39@@ -23,6 +26,9 @@ import (
40 //go:embed tmpl/*
41 var tmplFS embed.FS
42
43+//go:embed static/*
44+var staticFS embed.FS
45+
46 type WebCtx struct {
47 Pr *PrCmd
48 Backend *Backend
49@@ -618,6 +624,49 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
50 }
51 }
52
53+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
54+ return func(w http.ResponseWriter, r *http.Request) {
55+ web, err := getWebCtx(r)
56+ if err != nil {
57+ w.WriteHeader(http.StatusUnprocessableEntity)
58+ return
59+ }
60+ logger := web.Logger
61+
62+ file := r.PathValue("file")
63+ reader, err := staticfs.Open(file)
64+ if err != nil {
65+ logger.Error(err.Error())
66+ http.Error(w, "file not found", 404)
67+ return
68+ }
69+ contents, err := io.ReadAll(reader)
70+ if err != nil {
71+ logger.Error(err.Error())
72+ http.Error(w, "file not found", 404)
73+ return
74+ }
75+ contentType := http.DetectContentType(contents)
76+ w.Header().Add("Content-Type", contentType)
77+
78+ _, err = w.Write(contents)
79+ if err != nil {
80+ logger.Error(err.Error())
81+ http.Error(w, "server error", 500)
82+ return
83+ }
84+ }
85+}
86+
87+type StaticFs struct {
88+ Dir string
89+}
90+
91+func (fs *StaticFs) Open(name string) (os.File, error) {
92+ fp, err := os.Open(filepath.Join(fs.Dir, name))
93+ return *fp, err
94+}
95+
96 func StartWebServer(cfg *GitCfg) {
97 addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
98
99@@ -659,6 +708,14 @@ func StartWebServer(cfg *GitCfg) {
100 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
101 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
102 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
103+ dir := filepath.Join(cfg.DataDir, "static")
104+ var filesys fs.FS = staticFS
105+ _, err = os.Stat(dir)
106+ if err == nil {
107+ cfg.Logger.Info("detected static folder, using instead of the default one")
108+ filesys = os.DirFS(dir)
109+ }
110+ http.HandleFunc("GET /static/{file}", serveFile(filesys))
111
112 cfg.Logger.Info("starting web server", "addr", addr)
113 err = http.ListenAndServe(addr, nil)
114
115base-commit: d60d7b4823c8c2be3024983ee128818b000e40fa
116--
1172.45.2
118
chore: inline smol.css
Eric Bower <me@erock.io>
static/smol.css | 768 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 768 insertions(+) create mode 100644 static/smol.css
1From 8919af530f0ef2b4e03255a99edb45a90f3b7782 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 00:28:04 -0400
4Subject: [PATCH 2/4] chore: inline smol.css
5
6---
7 static/smol.css | 768 ++++++++++++++++++++++++++++++++++++++++++++++++
8 1 file changed, 768 insertions(+)
9 create mode 100644 static/smol.css
10
11diff --git a/static/smol.css b/static/smol.css
12new file mode 100644
13index 0000000..e9b59ec
14--- /dev/null
15+++ b/static/smol.css
16@@ -0,0 +1,768 @@
17+*,
18+::before,
19+::after {
20+ box-sizing: border-box;
21+}
22+
23+::-moz-focus-inner {
24+ border-style: none;
25+ padding: 0;
26+}
27+:-moz-focusring {
28+ outline: 1px dotted ButtonText;
29+}
30+:-moz-ui-invalid {
31+ box-shadow: none;
32+}
33+
34+@media (prefers-color-scheme: light) {
35+ :root {
36+ --main-hue: 250;
37+ --white: #2e3f53;
38+ --white-light: #cfe0f4;
39+ --white-dark: #6c6a6a;
40+ --code: #52576f;
41+ --pre: #e1e7ee;
42+ --bg-color: #f4f4f4;
43+ --text-color: #24292f;
44+ --link-color: #005cc5;
45+ --visited: #6f42c1;
46+ --blockquote: #005cc5;
47+ --blockquote-bg: #cfe0f4;
48+ --hover: #c11e7a;
49+ --grey: #ccc;
50+ --grey-light: #6a708e;
51+ --shadow: #e8e8e8;
52+ }
53+}
54+
55+@media (prefers-color-scheme: dark) {
56+ :root {
57+ --main-hue: 250;
58+ --white: #f2f2f2;
59+ --white-light: #f2f2f2;
60+ --white-dark: #e8e8e8;
61+ --code: #414558;
62+ --pre: #252525;
63+ --bg-color: #282a36;
64+ --text-color: #f2f2f2;
65+ --link-color: #8be9fd;
66+ --visited: #bd93f9;
67+ --blockquote: #bd93f9;
68+ --blockquote-bg: #353548;
69+ --hover: #ff80bf;
70+ --grey: #414558;
71+ --grey-light: #6a708e;
72+ --shadow: #252525;
73+ }
74+}
75+
76+html {
77+ background-color: var(--bg-color);
78+ color: var(--text-color);
79+ font-size: 18px;
80+ line-height: 1.5;
81+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
82+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
83+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
84+ -webkit-text-size-adjust: 100%;
85+ -moz-tab-size: 4;
86+ -o-tab-size: 4;
87+ tab-size: 4;
88+}
89+
90+body {
91+ margin: 0 auto;
92+}
93+
94+img {
95+ max-width: 100%;
96+ height: auto;
97+}
98+
99+b,
100+strong {
101+ font-weight: bold;
102+}
103+
104+code,
105+kbd,
106+samp,
107+pre {
108+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
109+ monospace;
110+}
111+
112+code,
113+kbd,
114+samp {
115+ border: 2px solid var(--code);
116+}
117+
118+pre > code {
119+ background-color: inherit;
120+ padding: 0;
121+ border: none;
122+}
123+
124+code {
125+ font-size: 90%;
126+ border-radius: 0.3rem;
127+ padding: 0.1rem 0.3rem;
128+}
129+
130+pre {
131+ font-size: 14px;
132+ border-radius: 5px;
133+ padding: 1rem;
134+ margin: 1rem 0;
135+ overflow-x: auto;
136+ background-color: var(--pre) !important;
137+}
138+
139+small {
140+ font-size: 0.8rem;
141+}
142+
143+summary {
144+ display: list-item;
145+ cursor: pointer;
146+}
147+
148+h1,
149+h2,
150+h3,
151+h4 {
152+ margin: 0;
153+ padding: 0.5rem 0 0 0;
154+ border: 0;
155+ font-style: normal;
156+ font-weight: inherit;
157+ font-size: inherit;
158+}
159+
160+path {
161+ fill: var(--text-color);
162+ stroke: var(--text-color);
163+}
164+
165+hr {
166+ color: inherit;
167+ border: 0;
168+ margin: 0;
169+ height: 2px;
170+ background: var(--grey);
171+ margin: 1rem auto;
172+ text-align: center;
173+}
174+
175+a {
176+ text-decoration: none;
177+ color: var(--link-color);
178+}
179+
180+a:hover,
181+a:visited:hover {
182+ text-decoration: underline;
183+ color: var(--hover);
184+}
185+
186+a:visited {
187+ color: var(--visited);
188+}
189+
190+a.link-grey {
191+ text-decoration: underline;
192+ color: var(--white);
193+}
194+
195+a.link-grey:visited {
196+ color: var(--white);
197+}
198+
199+section {
200+ margin-bottom: 1.4rem;
201+}
202+
203+section:last-child {
204+ margin-bottom: 0;
205+}
206+
207+header {
208+ margin: 1rem auto;
209+}
210+
211+p {
212+ margin: 0.5rem 0;
213+}
214+
215+article {
216+ overflow-wrap: break-word;
217+}
218+
219+blockquote {
220+ border-left: 5px solid var(--blockquote);
221+ background-color: var(--blockquote-bg);
222+ padding: 0.5rem 0.75rem;
223+ margin: 0.5rem 0;
224+}
225+
226+blockquote > p {
227+ margin: 0;
228+}
229+
230+blockquote code {
231+ border: 1px solid var(--blockquote);
232+}
233+
234+ul,
235+ol {
236+ padding: 0 0 0 1rem;
237+ list-style-position: outside;
238+}
239+
240+ul[style*="list-style-type: none;"] {
241+ padding: 0;
242+}
243+
244+li {
245+ margin: 0.5rem 0;
246+}
247+
248+li > pre {
249+ padding: 0;
250+}
251+
252+footer {
253+ text-align: center;
254+ margin-bottom: 4rem;
255+}
256+
257+dt {
258+ font-weight: bold;
259+}
260+
261+dd {
262+ margin-left: 0;
263+}
264+
265+dd:not(:last-child) {
266+ margin-bottom: 0.5rem;
267+}
268+
269+figure {
270+ margin: 0;
271+}
272+
273+.container {
274+ max-width: 50em;
275+ width: 100%;
276+}
277+
278+.container-sm {
279+ max-width: 40em;
280+ width: 100%;
281+}
282+
283+.container-center {
284+ width: 100%;
285+ height: 100%;
286+ display: flex;
287+ justify-content: center;
288+}
289+
290+.mono {
291+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
292+ monospace;
293+}
294+
295+.link-alt-adj,
296+.link-alt-adj:visited,
297+.link-alt-adj:visited:hover,
298+.link-alt-adj:hover {
299+ color: var(--link-color);
300+ text-decoration: none;
301+}
302+
303+.link-alt-adj:visited:hover,
304+.link-alt-adj:hover {
305+ text-decoration: underline;
306+}
307+
308+.link-alt-hover,
309+.link-alt-hover:visited,
310+.link-alt-hover:visited:hover,
311+.link-alt-hover:hover {
312+ color: var(--hover);
313+ text-decoration: none;
314+}
315+
316+.link-alt-hover:visited:hover,
317+.link-alt-hover:hover {
318+ text-decoration: underline;
319+}
320+
321+.link-alt,
322+.link-alt:visited,
323+.link-alt:visited:hover,
324+.link-alt:hover {
325+ color: var(--white);
326+ text-decoration: none;
327+}
328+
329+.link-alt:visited:hover,
330+.link-alt:hover {
331+ text-decoration: underline;
332+}
333+
334+.text-3xl {
335+ font-size: 2.5rem;
336+}
337+
338+.text-2xl {
339+ font-size: 1.9rem;
340+ line-height: 1.15;
341+}
342+
343+.text-xl {
344+ font-size: 1.55rem;
345+ line-height: 1.15;
346+}
347+
348+.text-lg {
349+ font-size: 1.35rem;
350+ line-height: 1.15;
351+}
352+
353+.text-md {
354+ font-size: 1.15rem;
355+ line-height: 1.15;
356+}
357+
358+.text-sm {
359+ font-size: 0.875rem;
360+}
361+
362+.text-xs {
363+ font-size: 0.775rem;
364+}
365+
366+.cursor-pointer {
367+ cursor: pointer;
368+}
369+
370+.w-full {
371+ width: 100%;
372+}
373+
374+.h-full {
375+ height: 100%;
376+}
377+
378+.border {
379+ border: 2px solid var(--grey-light);
380+}
381+
382+.text-left {
383+ text-align: left;
384+}
385+
386+.text-center {
387+ text-align: center;
388+}
389+
390+.text-underline {
391+ border-bottom: 3px solid var(--text-color);
392+ padding-bottom: 3px;
393+}
394+
395+.text-hdr {
396+ color: var(--hover);
397+}
398+
399+.text-underline-hdr {
400+ border-bottom: 3px solid var(--hover);
401+ padding-bottom: 3px;
402+}
403+
404+.font-bold {
405+ font-weight: bold;
406+}
407+
408+.font-italic {
409+ font-style: italic;
410+}
411+
412+.inline {
413+ display: inline;
414+}
415+
416+.inline-block {
417+ display: inline-block;
418+}
419+
420+.max-w-half {
421+ max-width: 50%;
422+}
423+
424+.h-screen {
425+ height: 100vh;
426+}
427+
428+.w-screen {
429+ width: 100vw;
430+}
431+
432+.flex {
433+ display: flex;
434+}
435+
436+.flex-col {
437+ flex-direction: column;
438+}
439+
440+.items-center {
441+ align-items: center;
442+}
443+
444+.m-0 {
445+ margin: 0;
446+}
447+
448+.mt {
449+ margin-top: 0.5rem;
450+}
451+
452+.mt-2 {
453+ margin-top: 1rem;
454+}
455+
456+.mt-4 {
457+ margin-top: 2rem;
458+}
459+
460+.mt-8 {
461+ margin-top: 4rem;
462+}
463+
464+.mb {
465+ margin-bottom: 0.5rem;
466+}
467+
468+.mb-2 {
469+ margin-bottom: 1rem;
470+}
471+
472+.mb-4 {
473+ margin-bottom: 2rem;
474+}
475+
476+.mb-8 {
477+ margin-bottom: 4rem;
478+}
479+
480+.mb-16 {
481+ margin-bottom: 8rem;
482+}
483+
484+.mr {
485+ margin-right: 0.5rem;
486+}
487+
488+.ml-sm {
489+ margin-left: 0.25rem;
490+}
491+
492+.ml {
493+ margin-left: 0.5rem;
494+}
495+
496+.pt-0 {
497+ padding-top: 0;
498+}
499+
500+.my {
501+ margin-top: 0.5rem;
502+ margin-bottom: 0.5rem;
503+}
504+
505+.my-2 {
506+ margin-top: 1rem;
507+ margin-bottom: 1rem;
508+}
509+
510+.my-4 {
511+ margin-top: 2rem;
512+ margin-bottom: 2rem;
513+}
514+
515+.my-8 {
516+ margin-top: 4rem;
517+ margin-bottom: 4rem;
518+}
519+
520+.mx {
521+ margin-left: 0.5rem;
522+ margin-right: 0.5rem;
523+}
524+
525+.mx-2 {
526+ margin-left: 1rem;
527+ margin-right: 1rem;
528+}
529+
530+.m-1 {
531+ margin: 0.5rem;
532+}
533+
534+.p-1 {
535+ padding: 0.5rem;
536+}
537+
538+.p-0 {
539+ padding: 0;
540+}
541+
542+.px-2 {
543+ padding-left: 1rem;
544+ padding-right: 1rem;
545+}
546+
547+.px-4 {
548+ padding-left: 2rem;
549+ padding-right: 2rem;
550+}
551+
552+.py {
553+ padding-top: 0.5rem;
554+ padding-bottom: 0.5rem;
555+}
556+
557+.py-2 {
558+ padding-top: 1rem;
559+ padding-bottom: 1rem;
560+}
561+
562+.py-4 {
563+ padding-top: 2rem;
564+ padding-bottom: 2rem;
565+}
566+
567+.py-8 {
568+ padding-top: 4rem;
569+ padding-bottom: 4rem;
570+}
571+
572+.justify-between {
573+ justify-content: space-between;
574+}
575+
576+.justify-center {
577+ justify-content: center;
578+}
579+
580+.gap {
581+ gap: 0.5rem;
582+}
583+
584+.gap-2 {
585+ gap: 1rem;
586+}
587+
588+.group {
589+ display: flex;
590+ flex-direction: column;
591+ gap: 0.5rem;
592+}
593+
594+.group-2 {
595+ display: flex;
596+ flex-direction: column;
597+ gap: 1rem;
598+}
599+
600+.group-h {
601+ display: flex;
602+ gap: 0.5rem;
603+ align-items: center;
604+}
605+
606+.flex-1 {
607+ flex: 1;
608+}
609+
610+.items-end {
611+ align-items: end;
612+}
613+
614+.items-start {
615+ align-items: start;
616+}
617+
618+.justify-end {
619+ justify-content: end;
620+}
621+
622+.font-grey-light {
623+ color: var(--grey-light);
624+}
625+
626+.hidden {
627+ display: none;
628+}
629+
630+.align-right {
631+ text-align: right;
632+}
633+
634+/* ==== MARKDOWN ==== */
635+
636+.md h1,
637+.md h2,
638+.md h3,
639+.md h4 {
640+ padding: 0;
641+ margin: 1.5rem 0 0.9rem 0;
642+ font-weight: bold;
643+}
644+
645+.md h1 a,
646+.md h2 a,
647+.md h3 a,
648+.md h4 a {
649+ color: var(--grey-light);
650+ text-decoration: none;
651+}
652+
653+.md h1 {
654+ font-size: 1.6rem;
655+ line-height: 1.15;
656+ border-bottom: 2px solid var(--grey);
657+ padding-bottom: 0.7rem;
658+}
659+
660+.md h2 {
661+ font-size: 1.3rem;
662+ line-height: 1.15;
663+ color: var(--white-dark);
664+}
665+
666+.md h3 {
667+ font-size: 1.2rem;
668+ color: var(--white-dark);
669+}
670+
671+.md h4 {
672+ font-size: 1rem;
673+ color: var(--white-dark);
674+}
675+
676+/* ==== HELPERS ==== */
677+
678+.logo-header {
679+ line-height: 1;
680+ display: inline-block;
681+ background-color: #FF79C6;
682+ background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
683+ color: transparent;
684+ background-clip: text;
685+ border: 3px solid #FF79C6;
686+ padding: 8px 10px 10px 10px;
687+ border-radius: 10px;
688+ box-shadow: 0px 5px 0px 0px var(--shadow);
689+ background-size: 100%;
690+ -webkit-background-clip: text;
691+ -moz-background-clip: text;
692+ -webkit-text-fill-color: transparent;
693+ -moz-text-fill-color: transparent;
694+}
695+
696+.btn {
697+ border: 2px solid var(--link-color);
698+ color: var(--link-color);
699+ padding: 0.4rem 1rem;
700+ font-weight: bold;
701+ display: inline-block;
702+}
703+
704+.btn-link,
705+.btn-link:visited {
706+ border: 2px solid var(--link-color);
707+ color: var(--link-color);
708+ padding: 0.4rem 1rem;
709+ text-decoration: none;
710+ font-weight: bold;
711+ display: inline-block;
712+}
713+
714+.btn-link:visited:hover,
715+.btn-link:hover {
716+ border: 2px solid var(--hover);
717+}
718+
719+.btn-link-alt,
720+.btn-link-alt:visited {
721+ border: 2px solid var(--white);
722+ color: var(--white);
723+}
724+
725+.box {
726+ border: 2px solid var(--grey-light);
727+ padding: 0.5rem 0.75rem;
728+}
729+
730+.box-sm {
731+ border: 2px solid var(--grey-light);
732+ padding: 0.15rem 0.35rem;
733+}
734+
735+.box-alert {
736+ border: 2px solid var(--hover);
737+ padding: 0.5rem 0.75rem;
738+}
739+
740+.box-sm-alert {
741+ border: 2px solid var(--hover);
742+ padding: 0.15rem 0.35rem;
743+}
744+
745+.list-none {
746+ list-style-type: none;
747+}
748+
749+.list-disc {
750+ list-style-type: disc;
751+}
752+
753+.list-decimal {
754+ list-style-type: decimal;
755+}
756+
757+.pill {
758+ border: 1px solid var(--link-color);
759+ color: var(--link-color);
760+}
761+
762+.pill-alert {
763+ border: 1px solid var(--hover);
764+ color: var(--hover);
765+}
766+
767+.pill-info {
768+ border: 1px solid var(--visited);
769+ color: var(--visited);
770+}
771+
772+@media only screen and (max-width: 40em) {
773+ body {
774+ padding: 0 1rem;
775+ }
776+
777+ header {
778+ margin: 0;
779+ }
780+
781+ .flex-collapse {
782+ flex-direction: column;
783+ }
784+}
785--
7862.45.2
787
chore: create patch for smol
Eric Bower <me@erock.io>
Makefile | 1 + patches/smol.diff | 53 +++++++++++++++++++++++++++++++++++++++++++++++ static/smol.css | 42 ------------------------------------- 3 files changed, 54 insertions(+), 42 deletions(-) create mode 100644 patches/smol.diff
1From 734612255548a4cd83eae59566581764154adb41 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 00:29:31 -0400
4Subject: [PATCH 3/4] chore: create patch for smol
5
6---
7 Makefile | 1 +
8 patches/smol.diff | 53 +++++++++++++++++++++++++++++++++++++++++++++++
9 static/smol.css | 42 -------------------------------------
10 3 files changed, 54 insertions(+), 42 deletions(-)
11 create mode 100644 patches/smol.diff
12
13diff --git a/Makefile b/Makefile
14index bc76fd9..e521f82 100644
15--- a/Makefile
16+++ b/Makefile
17@@ -29,4 +29,5 @@ bp: bp-setup
18
19 smol:
20 curl https://pico.sh/smol.css -o ./static/smol.css
21+ cat patches/smol.diff | git apply
22 .PHONY: smol
23diff --git a/patches/smol.diff b/patches/smol.diff
24new file mode 100644
25index 0000000..ae2df55
26--- /dev/null
27+++ b/patches/smol.diff
28@@ -0,0 +1,53 @@
29+diff --git a/static/smol.css b/static/smol.css
30+index e9b59ec..9e9d925 100644
31+--- a/static/smol.css
32++++ b/static/smol.css
33+@@ -15,48 +15,6 @@
34+ box-shadow: none;
35+ }
36+
37+-@media (prefers-color-scheme: light) {
38+- :root {
39+- --main-hue: 250;
40+- --white: #2e3f53;
41+- --white-light: #cfe0f4;
42+- --white-dark: #6c6a6a;
43+- --code: #52576f;
44+- --pre: #e1e7ee;
45+- --bg-color: #f4f4f4;
46+- --text-color: #24292f;
47+- --link-color: #005cc5;
48+- --visited: #6f42c1;
49+- --blockquote: #005cc5;
50+- --blockquote-bg: #cfe0f4;
51+- --hover: #c11e7a;
52+- --grey: #ccc;
53+- --grey-light: #6a708e;
54+- --shadow: #e8e8e8;
55+- }
56+-}
57+-
58+-@media (prefers-color-scheme: dark) {
59+- :root {
60+- --main-hue: 250;
61+- --white: #f2f2f2;
62+- --white-light: #f2f2f2;
63+- --white-dark: #e8e8e8;
64+- --code: #414558;
65+- --pre: #252525;
66+- --bg-color: #282a36;
67+- --text-color: #f2f2f2;
68+- --link-color: #8be9fd;
69+- --visited: #bd93f9;
70+- --blockquote: #bd93f9;
71+- --blockquote-bg: #353548;
72+- --hover: #ff80bf;
73+- --grey: #414558;
74+- --grey-light: #6a708e;
75+- --shadow: #252525;
76+- }
77+-}
78+-
79+ html {
80+ background-color: var(--bg-color);
81+ color: var(--text-color);
82diff --git a/static/smol.css b/static/smol.css
83index e9b59ec..9e9d925 100644
84--- a/static/smol.css
85+++ b/static/smol.css
86@@ -15,48 +15,6 @@
87 box-shadow: none;
88 }
89
90-@media (prefers-color-scheme: light) {
91- :root {
92- --main-hue: 250;
93- --white: #2e3f53;
94- --white-light: #cfe0f4;
95- --white-dark: #6c6a6a;
96- --code: #52576f;
97- --pre: #e1e7ee;
98- --bg-color: #f4f4f4;
99- --text-color: #24292f;
100- --link-color: #005cc5;
101- --visited: #6f42c1;
102- --blockquote: #005cc5;
103- --blockquote-bg: #cfe0f4;
104- --hover: #c11e7a;
105- --grey: #ccc;
106- --grey-light: #6a708e;
107- --shadow: #e8e8e8;
108- }
109-}
110-
111-@media (prefers-color-scheme: dark) {
112- :root {
113- --main-hue: 250;
114- --white: #f2f2f2;
115- --white-light: #f2f2f2;
116- --white-dark: #e8e8e8;
117- --code: #414558;
118- --pre: #252525;
119- --bg-color: #282a36;
120- --text-color: #f2f2f2;
121- --link-color: #8be9fd;
122- --visited: #bd93f9;
123- --blockquote: #bd93f9;
124- --blockquote-bg: #353548;
125- --hover: #ff80bf;
126- --grey: #414558;
127- --grey-light: #6a708e;
128- --shadow: #252525;
129- }
130-}
131-
132 html {
133 background-color: var(--bg-color);
134 color: var(--text-color);
135--
1362.45.2
137
feat: static folder
Eric Bower <me@erock.io>
ssh.go | 2 +- static/vars.css | 18 ++++++++++++++++++ tmpl/base.html | 5 +++-- web.go | 40 +++++++++++++++++++++++++--------------- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 static/vars.css
1From d8792d5fa1df3fbbcbc80c04783935cdf15df2c9 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 10:52:39 -0400
4Subject: [PATCH 4/4] feat: static folder
5
6---
7 ssh.go | 2 +-
8 static/vars.css | 18 ++++++++++++++++++
9 tmpl/base.html | 5 +++--
10 web.go | 40 +++++++++++++++++++++++++---------------
11 4 files changed, 47 insertions(+), 18 deletions(-)
12 create mode 100644 static/vars.css
13
14diff --git a/ssh.go b/ssh.go
15index 1fb34d0..3e5e407 100644
16--- a/ssh.go
17+++ b/ssh.go
18@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
19 dbpath := filepath.Join(cfg.DataDir, "pr.db")
20 dbh, err := Open(dbpath, cfg.Logger)
21 if err != nil {
22- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
23+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
24 }
25
26 be := &Backend{
27diff --git a/static/vars.css b/static/vars.css
28new file mode 100644
29index 0000000..6300534
30--- /dev/null
31+++ b/static/vars.css
32@@ -0,0 +1,18 @@
33+:root {
34+ --main-hue: 250;
35+ --white: #f2f2f2;
36+ --white-light: #f2f2f2;
37+ --white-dark: #e8e8e8;
38+ --code: #414558;
39+ --pre: #252525;
40+ --bg-color: #282a36;
41+ --text-color: #f2f2f2;
42+ --link-color: #8be9fd;
43+ --visited: #bd93f9;
44+ --blockquote: #bd93f9;
45+ --blockquote-bg: #353548;
46+ --hover: #ff80bf;
47+ --grey: #414558;
48+ --grey-light: #6a708e;
49+ --shadow: #252525;
50+}
51diff --git a/tmpl/base.html b/tmpl/base.html
52index a656cd5..b5f5fb3 100644
53--- a/tmpl/base.html
54+++ b/tmpl/base.html
55@@ -9,8 +9,9 @@
56 <meta name="keywords" content="git, collaboration, patch, requests" />
57 {{template "meta" .}}
58
59- <link rel="stylesheet" href="https://pico.sh/smol.css" />
60- <link rel="stylesheet" href="https://pico.sh/syntax.css" />
61+ <link rel="stylesheet" href="/static/smol.css" />
62+ <link rel="stylesheet" href="/static/vars.css" />
63+ <link rel="stylesheet" href="/syntax.css" />
64 </head>
65 <body class="container">{{template "body" .}}</body>
66 </html>
67diff --git a/web.go b/web.go
68index 12db697..99bfec6 100644
69--- a/web.go
70+++ b/web.go
71@@ -9,6 +9,7 @@ import (
72 "io"
73 "io/fs"
74 "log/slog"
75+ "mime"
76 "net/http"
77 "os"
78 "path/filepath"
79@@ -634,6 +635,7 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
80 logger := web.Logger
81
82 file := r.PathValue("file")
83+ logger.Info("serving file", "file", file, "fs", staticfs)
84 reader, err := staticfs.Open(file)
85 if err != nil {
86 logger.Error(err.Error())
87@@ -646,7 +648,10 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
88 http.Error(w, "file not found", 404)
89 return
90 }
91- contentType := http.DetectContentType(contents)
92+ contentType := mime.TypeByExtension(filepath.Ext(file))
93+ if contentType == "" {
94+ contentType = http.DetectContentType(contents)
95+ }
96 w.Header().Add("Content-Type", contentType)
97
98 _, err = w.Write(contents)
99@@ -658,13 +663,21 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
100 }
101 }
102
103-type StaticFs struct {
104- Dir string
105-}
106+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
107+ dir := filepath.Join(datadir, dirName)
108+ _, err := os.Stat(dir)
109+ if err == nil {
110+ logger.Info("found folder in data_dir", "dir", dir)
111+ return os.DirFS(dir), nil
112+ }
113
114-func (fs *StaticFs) Open(name string) (os.File, error) {
115- fp, err := os.Open(filepath.Join(fs.Dir, name))
116- return *fp, err
117+ logger.Info("using embeded folder", "dir", dir)
118+ fsys, err := fs.Sub(ffs, dirName)
119+ if err != nil {
120+ return nil, err
121+ }
122+
123+ return fsys, nil
124 }
125
126 func StartWebServer(cfg *GitCfg) {
127@@ -673,7 +686,7 @@ func StartWebServer(cfg *GitCfg) {
128 dbpath := filepath.Join(cfg.DataDir, "pr.db")
129 dbh, err := Open(dbpath, cfg.Logger)
130 if err != nil {
131- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
132+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
133 }
134
135 be := &Backend{
136@@ -708,14 +721,11 @@ func StartWebServer(cfg *GitCfg) {
137 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
138 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
139 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
140- dir := filepath.Join(cfg.DataDir, "static")
141- var filesys fs.FS = staticFS
142- _, err = os.Stat(dir)
143- if err == nil {
144- cfg.Logger.Info("detected static folder, using instead of the default one")
145- filesys = os.DirFS(dir)
146+ filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
147+ if err != nil {
148+ panic(err)
149 }
150- http.HandleFunc("GET /static/{file}", serveFile(filesys))
151+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
152
153 cfg.Logger.Info("starting web server", "addr", addr)
154 err = http.ListenAndServe(addr, nil)
155--
1562.45.2
157
ps-3
by
erock
on Diff ↕
feat: static assets folder
Eric Bower <me@erock.io>
Create a static folder that will be served as-is with the ability for users to bring-their-own static folder. If we detect `data_dir/static/` we will serve that instead of the embedded one we provide by default.
Makefile | 5 + patches/smol.diff | 53 ++++ ssh.go | 2 +- static/smol.css | 726 ++++++++++++++++++++++++++++++++++++++++++++++ static/vars.css | 18 ++ tmpl/base.html | 5 +- web.go | 69 ++++- 7 files changed, 874 insertions(+), 4 deletions(-) create mode 100644 patches/smol.diff create mode 100644 static/smol.css create mode 100644 static/vars.css
1From 66cafc6a1d992c467314f51d443dba4bb5688ec7 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 00:27:17 -0400
4Subject: [PATCH] feat: static assets folder
5
6Create a static folder that will be served as-is with the ability for
7users to bring-their-own static folder.
8
9If we detect `data_dir/static/` we will serve that instead of the
10embedded one we provide by default.
11---
12 Makefile | 5 +
13 patches/smol.diff | 53 ++++
14 ssh.go | 2 +-
15 static/smol.css | 726 ++++++++++++++++++++++++++++++++++++++++++++++
16 static/vars.css | 18 ++
17 tmpl/base.html | 5 +-
18 web.go | 69 ++++-
19 7 files changed, 874 insertions(+), 4 deletions(-)
20 create mode 100644 patches/smol.diff
21 create mode 100644 static/smol.css
22 create mode 100644 static/vars.css
23
24diff --git a/Makefile b/Makefile
25index cb3a367..e521f82 100644
26--- a/Makefile
27+++ b/Makefile
28@@ -26,3 +26,8 @@ bp: bp-setup
29 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
30 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
31 .PHONY: bp
32+
33+smol:
34+ curl https://pico.sh/smol.css -o ./static/smol.css
35+ cat patches/smol.diff | git apply
36+.PHONY: smol
37diff --git a/patches/smol.diff b/patches/smol.diff
38new file mode 100644
39index 0000000..ae2df55
40--- /dev/null
41+++ b/patches/smol.diff
42@@ -0,0 +1,53 @@
43+diff --git a/static/smol.css b/static/smol.css
44+index e9b59ec..9e9d925 100644
45+--- a/static/smol.css
46++++ b/static/smol.css
47+@@ -15,48 +15,6 @@
48+ box-shadow: none;
49+ }
50+
51+-@media (prefers-color-scheme: light) {
52+- :root {
53+- --main-hue: 250;
54+- --white: #2e3f53;
55+- --white-light: #cfe0f4;
56+- --white-dark: #6c6a6a;
57+- --code: #52576f;
58+- --pre: #e1e7ee;
59+- --bg-color: #f4f4f4;
60+- --text-color: #24292f;
61+- --link-color: #005cc5;
62+- --visited: #6f42c1;
63+- --blockquote: #005cc5;
64+- --blockquote-bg: #cfe0f4;
65+- --hover: #c11e7a;
66+- --grey: #ccc;
67+- --grey-light: #6a708e;
68+- --shadow: #e8e8e8;
69+- }
70+-}
71+-
72+-@media (prefers-color-scheme: dark) {
73+- :root {
74+- --main-hue: 250;
75+- --white: #f2f2f2;
76+- --white-light: #f2f2f2;
77+- --white-dark: #e8e8e8;
78+- --code: #414558;
79+- --pre: #252525;
80+- --bg-color: #282a36;
81+- --text-color: #f2f2f2;
82+- --link-color: #8be9fd;
83+- --visited: #bd93f9;
84+- --blockquote: #bd93f9;
85+- --blockquote-bg: #353548;
86+- --hover: #ff80bf;
87+- --grey: #414558;
88+- --grey-light: #6a708e;
89+- --shadow: #252525;
90+- }
91+-}
92+-
93+ html {
94+ background-color: var(--bg-color);
95+ color: var(--text-color);
96diff --git a/ssh.go b/ssh.go
97index 1fb34d0..3e5e407 100644
98--- a/ssh.go
99+++ b/ssh.go
100@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
101 dbpath := filepath.Join(cfg.DataDir, "pr.db")
102 dbh, err := Open(dbpath, cfg.Logger)
103 if err != nil {
104- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
105+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
106 }
107
108 be := &Backend{
109diff --git a/static/smol.css b/static/smol.css
110new file mode 100644
111index 0000000..9e9d925
112--- /dev/null
113+++ b/static/smol.css
114@@ -0,0 +1,726 @@
115+*,
116+::before,
117+::after {
118+ box-sizing: border-box;
119+}
120+
121+::-moz-focus-inner {
122+ border-style: none;
123+ padding: 0;
124+}
125+:-moz-focusring {
126+ outline: 1px dotted ButtonText;
127+}
128+:-moz-ui-invalid {
129+ box-shadow: none;
130+}
131+
132+html {
133+ background-color: var(--bg-color);
134+ color: var(--text-color);
135+ font-size: 18px;
136+ line-height: 1.5;
137+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
138+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
139+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
140+ -webkit-text-size-adjust: 100%;
141+ -moz-tab-size: 4;
142+ -o-tab-size: 4;
143+ tab-size: 4;
144+}
145+
146+body {
147+ margin: 0 auto;
148+}
149+
150+img {
151+ max-width: 100%;
152+ height: auto;
153+}
154+
155+b,
156+strong {
157+ font-weight: bold;
158+}
159+
160+code,
161+kbd,
162+samp,
163+pre {
164+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
165+ monospace;
166+}
167+
168+code,
169+kbd,
170+samp {
171+ border: 2px solid var(--code);
172+}
173+
174+pre > code {
175+ background-color: inherit;
176+ padding: 0;
177+ border: none;
178+}
179+
180+code {
181+ font-size: 90%;
182+ border-radius: 0.3rem;
183+ padding: 0.1rem 0.3rem;
184+}
185+
186+pre {
187+ font-size: 14px;
188+ border-radius: 5px;
189+ padding: 1rem;
190+ margin: 1rem 0;
191+ overflow-x: auto;
192+ background-color: var(--pre) !important;
193+}
194+
195+small {
196+ font-size: 0.8rem;
197+}
198+
199+summary {
200+ display: list-item;
201+ cursor: pointer;
202+}
203+
204+h1,
205+h2,
206+h3,
207+h4 {
208+ margin: 0;
209+ padding: 0.5rem 0 0 0;
210+ border: 0;
211+ font-style: normal;
212+ font-weight: inherit;
213+ font-size: inherit;
214+}
215+
216+path {
217+ fill: var(--text-color);
218+ stroke: var(--text-color);
219+}
220+
221+hr {
222+ color: inherit;
223+ border: 0;
224+ margin: 0;
225+ height: 2px;
226+ background: var(--grey);
227+ margin: 1rem auto;
228+ text-align: center;
229+}
230+
231+a {
232+ text-decoration: none;
233+ color: var(--link-color);
234+}
235+
236+a:hover,
237+a:visited:hover {
238+ text-decoration: underline;
239+ color: var(--hover);
240+}
241+
242+a:visited {
243+ color: var(--visited);
244+}
245+
246+a.link-grey {
247+ text-decoration: underline;
248+ color: var(--white);
249+}
250+
251+a.link-grey:visited {
252+ color: var(--white);
253+}
254+
255+section {
256+ margin-bottom: 1.4rem;
257+}
258+
259+section:last-child {
260+ margin-bottom: 0;
261+}
262+
263+header {
264+ margin: 1rem auto;
265+}
266+
267+p {
268+ margin: 0.5rem 0;
269+}
270+
271+article {
272+ overflow-wrap: break-word;
273+}
274+
275+blockquote {
276+ border-left: 5px solid var(--blockquote);
277+ background-color: var(--blockquote-bg);
278+ padding: 0.5rem 0.75rem;
279+ margin: 0.5rem 0;
280+}
281+
282+blockquote > p {
283+ margin: 0;
284+}
285+
286+blockquote code {
287+ border: 1px solid var(--blockquote);
288+}
289+
290+ul,
291+ol {
292+ padding: 0 0 0 1rem;
293+ list-style-position: outside;
294+}
295+
296+ul[style*="list-style-type: none;"] {
297+ padding: 0;
298+}
299+
300+li {
301+ margin: 0.5rem 0;
302+}
303+
304+li > pre {
305+ padding: 0;
306+}
307+
308+footer {
309+ text-align: center;
310+ margin-bottom: 4rem;
311+}
312+
313+dt {
314+ font-weight: bold;
315+}
316+
317+dd {
318+ margin-left: 0;
319+}
320+
321+dd:not(:last-child) {
322+ margin-bottom: 0.5rem;
323+}
324+
325+figure {
326+ margin: 0;
327+}
328+
329+.container {
330+ max-width: 50em;
331+ width: 100%;
332+}
333+
334+.container-sm {
335+ max-width: 40em;
336+ width: 100%;
337+}
338+
339+.container-center {
340+ width: 100%;
341+ height: 100%;
342+ display: flex;
343+ justify-content: center;
344+}
345+
346+.mono {
347+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
348+ monospace;
349+}
350+
351+.link-alt-adj,
352+.link-alt-adj:visited,
353+.link-alt-adj:visited:hover,
354+.link-alt-adj:hover {
355+ color: var(--link-color);
356+ text-decoration: none;
357+}
358+
359+.link-alt-adj:visited:hover,
360+.link-alt-adj:hover {
361+ text-decoration: underline;
362+}
363+
364+.link-alt-hover,
365+.link-alt-hover:visited,
366+.link-alt-hover:visited:hover,
367+.link-alt-hover:hover {
368+ color: var(--hover);
369+ text-decoration: none;
370+}
371+
372+.link-alt-hover:visited:hover,
373+.link-alt-hover:hover {
374+ text-decoration: underline;
375+}
376+
377+.link-alt,
378+.link-alt:visited,
379+.link-alt:visited:hover,
380+.link-alt:hover {
381+ color: var(--white);
382+ text-decoration: none;
383+}
384+
385+.link-alt:visited:hover,
386+.link-alt:hover {
387+ text-decoration: underline;
388+}
389+
390+.text-3xl {
391+ font-size: 2.5rem;
392+}
393+
394+.text-2xl {
395+ font-size: 1.9rem;
396+ line-height: 1.15;
397+}
398+
399+.text-xl {
400+ font-size: 1.55rem;
401+ line-height: 1.15;
402+}
403+
404+.text-lg {
405+ font-size: 1.35rem;
406+ line-height: 1.15;
407+}
408+
409+.text-md {
410+ font-size: 1.15rem;
411+ line-height: 1.15;
412+}
413+
414+.text-sm {
415+ font-size: 0.875rem;
416+}
417+
418+.text-xs {
419+ font-size: 0.775rem;
420+}
421+
422+.cursor-pointer {
423+ cursor: pointer;
424+}
425+
426+.w-full {
427+ width: 100%;
428+}
429+
430+.h-full {
431+ height: 100%;
432+}
433+
434+.border {
435+ border: 2px solid var(--grey-light);
436+}
437+
438+.text-left {
439+ text-align: left;
440+}
441+
442+.text-center {
443+ text-align: center;
444+}
445+
446+.text-underline {
447+ border-bottom: 3px solid var(--text-color);
448+ padding-bottom: 3px;
449+}
450+
451+.text-hdr {
452+ color: var(--hover);
453+}
454+
455+.text-underline-hdr {
456+ border-bottom: 3px solid var(--hover);
457+ padding-bottom: 3px;
458+}
459+
460+.font-bold {
461+ font-weight: bold;
462+}
463+
464+.font-italic {
465+ font-style: italic;
466+}
467+
468+.inline {
469+ display: inline;
470+}
471+
472+.inline-block {
473+ display: inline-block;
474+}
475+
476+.max-w-half {
477+ max-width: 50%;
478+}
479+
480+.h-screen {
481+ height: 100vh;
482+}
483+
484+.w-screen {
485+ width: 100vw;
486+}
487+
488+.flex {
489+ display: flex;
490+}
491+
492+.flex-col {
493+ flex-direction: column;
494+}
495+
496+.items-center {
497+ align-items: center;
498+}
499+
500+.m-0 {
501+ margin: 0;
502+}
503+
504+.mt {
505+ margin-top: 0.5rem;
506+}
507+
508+.mt-2 {
509+ margin-top: 1rem;
510+}
511+
512+.mt-4 {
513+ margin-top: 2rem;
514+}
515+
516+.mt-8 {
517+ margin-top: 4rem;
518+}
519+
520+.mb {
521+ margin-bottom: 0.5rem;
522+}
523+
524+.mb-2 {
525+ margin-bottom: 1rem;
526+}
527+
528+.mb-4 {
529+ margin-bottom: 2rem;
530+}
531+
532+.mb-8 {
533+ margin-bottom: 4rem;
534+}
535+
536+.mb-16 {
537+ margin-bottom: 8rem;
538+}
539+
540+.mr {
541+ margin-right: 0.5rem;
542+}
543+
544+.ml-sm {
545+ margin-left: 0.25rem;
546+}
547+
548+.ml {
549+ margin-left: 0.5rem;
550+}
551+
552+.pt-0 {
553+ padding-top: 0;
554+}
555+
556+.my {
557+ margin-top: 0.5rem;
558+ margin-bottom: 0.5rem;
559+}
560+
561+.my-2 {
562+ margin-top: 1rem;
563+ margin-bottom: 1rem;
564+}
565+
566+.my-4 {
567+ margin-top: 2rem;
568+ margin-bottom: 2rem;
569+}
570+
571+.my-8 {
572+ margin-top: 4rem;
573+ margin-bottom: 4rem;
574+}
575+
576+.mx {
577+ margin-left: 0.5rem;
578+ margin-right: 0.5rem;
579+}
580+
581+.mx-2 {
582+ margin-left: 1rem;
583+ margin-right: 1rem;
584+}
585+
586+.m-1 {
587+ margin: 0.5rem;
588+}
589+
590+.p-1 {
591+ padding: 0.5rem;
592+}
593+
594+.p-0 {
595+ padding: 0;
596+}
597+
598+.px-2 {
599+ padding-left: 1rem;
600+ padding-right: 1rem;
601+}
602+
603+.px-4 {
604+ padding-left: 2rem;
605+ padding-right: 2rem;
606+}
607+
608+.py {
609+ padding-top: 0.5rem;
610+ padding-bottom: 0.5rem;
611+}
612+
613+.py-2 {
614+ padding-top: 1rem;
615+ padding-bottom: 1rem;
616+}
617+
618+.py-4 {
619+ padding-top: 2rem;
620+ padding-bottom: 2rem;
621+}
622+
623+.py-8 {
624+ padding-top: 4rem;
625+ padding-bottom: 4rem;
626+}
627+
628+.justify-between {
629+ justify-content: space-between;
630+}
631+
632+.justify-center {
633+ justify-content: center;
634+}
635+
636+.gap {
637+ gap: 0.5rem;
638+}
639+
640+.gap-2 {
641+ gap: 1rem;
642+}
643+
644+.group {
645+ display: flex;
646+ flex-direction: column;
647+ gap: 0.5rem;
648+}
649+
650+.group-2 {
651+ display: flex;
652+ flex-direction: column;
653+ gap: 1rem;
654+}
655+
656+.group-h {
657+ display: flex;
658+ gap: 0.5rem;
659+ align-items: center;
660+}
661+
662+.flex-1 {
663+ flex: 1;
664+}
665+
666+.items-end {
667+ align-items: end;
668+}
669+
670+.items-start {
671+ align-items: start;
672+}
673+
674+.justify-end {
675+ justify-content: end;
676+}
677+
678+.font-grey-light {
679+ color: var(--grey-light);
680+}
681+
682+.hidden {
683+ display: none;
684+}
685+
686+.align-right {
687+ text-align: right;
688+}
689+
690+/* ==== MARKDOWN ==== */
691+
692+.md h1,
693+.md h2,
694+.md h3,
695+.md h4 {
696+ padding: 0;
697+ margin: 1.5rem 0 0.9rem 0;
698+ font-weight: bold;
699+}
700+
701+.md h1 a,
702+.md h2 a,
703+.md h3 a,
704+.md h4 a {
705+ color: var(--grey-light);
706+ text-decoration: none;
707+}
708+
709+.md h1 {
710+ font-size: 1.6rem;
711+ line-height: 1.15;
712+ border-bottom: 2px solid var(--grey);
713+ padding-bottom: 0.7rem;
714+}
715+
716+.md h2 {
717+ font-size: 1.3rem;
718+ line-height: 1.15;
719+ color: var(--white-dark);
720+}
721+
722+.md h3 {
723+ font-size: 1.2rem;
724+ color: var(--white-dark);
725+}
726+
727+.md h4 {
728+ font-size: 1rem;
729+ color: var(--white-dark);
730+}
731+
732+/* ==== HELPERS ==== */
733+
734+.logo-header {
735+ line-height: 1;
736+ display: inline-block;
737+ background-color: #FF79C6;
738+ background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
739+ color: transparent;
740+ background-clip: text;
741+ border: 3px solid #FF79C6;
742+ padding: 8px 10px 10px 10px;
743+ border-radius: 10px;
744+ box-shadow: 0px 5px 0px 0px var(--shadow);
745+ background-size: 100%;
746+ -webkit-background-clip: text;
747+ -moz-background-clip: text;
748+ -webkit-text-fill-color: transparent;
749+ -moz-text-fill-color: transparent;
750+}
751+
752+.btn {
753+ border: 2px solid var(--link-color);
754+ color: var(--link-color);
755+ padding: 0.4rem 1rem;
756+ font-weight: bold;
757+ display: inline-block;
758+}
759+
760+.btn-link,
761+.btn-link:visited {
762+ border: 2px solid var(--link-color);
763+ color: var(--link-color);
764+ padding: 0.4rem 1rem;
765+ text-decoration: none;
766+ font-weight: bold;
767+ display: inline-block;
768+}
769+
770+.btn-link:visited:hover,
771+.btn-link:hover {
772+ border: 2px solid var(--hover);
773+}
774+
775+.btn-link-alt,
776+.btn-link-alt:visited {
777+ border: 2px solid var(--white);
778+ color: var(--white);
779+}
780+
781+.box {
782+ border: 2px solid var(--grey-light);
783+ padding: 0.5rem 0.75rem;
784+}
785+
786+.box-sm {
787+ border: 2px solid var(--grey-light);
788+ padding: 0.15rem 0.35rem;
789+}
790+
791+.box-alert {
792+ border: 2px solid var(--hover);
793+ padding: 0.5rem 0.75rem;
794+}
795+
796+.box-sm-alert {
797+ border: 2px solid var(--hover);
798+ padding: 0.15rem 0.35rem;
799+}
800+
801+.list-none {
802+ list-style-type: none;
803+}
804+
805+.list-disc {
806+ list-style-type: disc;
807+}
808+
809+.list-decimal {
810+ list-style-type: decimal;
811+}
812+
813+.pill {
814+ border: 1px solid var(--link-color);
815+ color: var(--link-color);
816+}
817+
818+.pill-alert {
819+ border: 1px solid var(--hover);
820+ color: var(--hover);
821+}
822+
823+.pill-info {
824+ border: 1px solid var(--visited);
825+ color: var(--visited);
826+}
827+
828+@media only screen and (max-width: 40em) {
829+ body {
830+ padding: 0 1rem;
831+ }
832+
833+ header {
834+ margin: 0;
835+ }
836+
837+ .flex-collapse {
838+ flex-direction: column;
839+ }
840+}
841diff --git a/static/vars.css b/static/vars.css
842new file mode 100644
843index 0000000..6300534
844--- /dev/null
845+++ b/static/vars.css
846@@ -0,0 +1,18 @@
847+:root {
848+ --main-hue: 250;
849+ --white: #f2f2f2;
850+ --white-light: #f2f2f2;
851+ --white-dark: #e8e8e8;
852+ --code: #414558;
853+ --pre: #252525;
854+ --bg-color: #282a36;
855+ --text-color: #f2f2f2;
856+ --link-color: #8be9fd;
857+ --visited: #bd93f9;
858+ --blockquote: #bd93f9;
859+ --blockquote-bg: #353548;
860+ --hover: #ff80bf;
861+ --grey: #414558;
862+ --grey-light: #6a708e;
863+ --shadow: #252525;
864+}
865diff --git a/tmpl/base.html b/tmpl/base.html
866index a656cd5..b5f5fb3 100644
867--- a/tmpl/base.html
868+++ b/tmpl/base.html
869@@ -9,8 +9,9 @@
870 <meta name="keywords" content="git, collaboration, patch, requests" />
871 {{template "meta" .}}
872
873- <link rel="stylesheet" href="https://pico.sh/smol.css" />
874- <link rel="stylesheet" href="https://pico.sh/syntax.css" />
875+ <link rel="stylesheet" href="/static/smol.css" />
876+ <link rel="stylesheet" href="/static/vars.css" />
877+ <link rel="stylesheet" href="/syntax.css" />
878 </head>
879 <body class="container">{{template "body" .}}</body>
880 </html>
881diff --git a/web.go b/web.go
882index 4e782c8..99bfec6 100644
883--- a/web.go
884+++ b/web.go
885@@ -6,8 +6,12 @@ import (
886 "embed"
887 "fmt"
888 "html/template"
889+ "io"
890+ "io/fs"
891 "log/slog"
892+ "mime"
893 "net/http"
894+ "os"
895 "path/filepath"
896 "slices"
897 "strconv"
898@@ -23,6 +27,9 @@ import (
899 //go:embed tmpl/*
900 var tmplFS embed.FS
901
902+//go:embed static/*
903+var staticFS embed.FS
904+
905 type WebCtx struct {
906 Pr *PrCmd
907 Backend *Backend
908@@ -618,13 +625,68 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
909 }
910 }
911
912+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
913+ return func(w http.ResponseWriter, r *http.Request) {
914+ web, err := getWebCtx(r)
915+ if err != nil {
916+ w.WriteHeader(http.StatusUnprocessableEntity)
917+ return
918+ }
919+ logger := web.Logger
920+
921+ file := r.PathValue("file")
922+ logger.Info("serving file", "file", file, "fs", staticfs)
923+ reader, err := staticfs.Open(file)
924+ if err != nil {
925+ logger.Error(err.Error())
926+ http.Error(w, "file not found", 404)
927+ return
928+ }
929+ contents, err := io.ReadAll(reader)
930+ if err != nil {
931+ logger.Error(err.Error())
932+ http.Error(w, "file not found", 404)
933+ return
934+ }
935+ contentType := mime.TypeByExtension(filepath.Ext(file))
936+ if contentType == "" {
937+ contentType = http.DetectContentType(contents)
938+ }
939+ w.Header().Add("Content-Type", contentType)
940+
941+ _, err = w.Write(contents)
942+ if err != nil {
943+ logger.Error(err.Error())
944+ http.Error(w, "server error", 500)
945+ return
946+ }
947+ }
948+}
949+
950+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
951+ dir := filepath.Join(datadir, dirName)
952+ _, err := os.Stat(dir)
953+ if err == nil {
954+ logger.Info("found folder in data_dir", "dir", dir)
955+ return os.DirFS(dir), nil
956+ }
957+
958+ logger.Info("using embeded folder", "dir", dir)
959+ fsys, err := fs.Sub(ffs, dirName)
960+ if err != nil {
961+ return nil, err
962+ }
963+
964+ return fsys, nil
965+}
966+
967 func StartWebServer(cfg *GitCfg) {
968 addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
969
970 dbpath := filepath.Join(cfg.DataDir, "pr.db")
971 dbh, err := Open(dbpath, cfg.Logger)
972 if err != nil {
973- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
974+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
975 }
976
977 be := &Backend{
978@@ -659,6 +721,11 @@ func StartWebServer(cfg *GitCfg) {
979 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
980 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
981 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
982+ filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
983+ if err != nil {
984+ panic(err)
985+ }
986+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
987
988 cfg.Logger.Info("starting web server", "addr", addr)
989 err = http.ListenAndServe(addr, nil)
990
991base-commit: d60d7b4823c8c2be3024983ee128818b000e40fa
992--
9932.45.2
994
fix(cli): access control for removing patchsets
Eric Bower <me@erock.io>
I also fixed some other access control issues for changing PR status.
cli.go | 36 ++++++++++++++++++++++++++++++------ pr.go | 11 +++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-)
1From 5e76ed37da95bbeca9a025c918039b906ca3fd88 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 12:20:18 -0400
4Subject: [PATCH] fix(cli): access control for removing patchsets
5
6I also fixed some other access control issues for changing PR status.
7---
8 cli.go | 36 ++++++++++++++++++++++++++++++------
9 pr.go | 11 +++++++++++
10 2 files changed, 41 insertions(+), 6 deletions(-)
11
12diff --git a/cli.go b/cli.go
13index fcd2543..16c696d 100644
14--- a/cli.go
15+++ b/cli.go
16@@ -213,7 +213,30 @@ Here's how it works:
17 if err != nil {
18 return err
19 }
20- return pr.DeletePatchsetByID(patchsetID)
21+
22+ patchset, err := pr.GetPatchsetByID(patchsetID)
23+ if err != nil {
24+ return err
25+ }
26+
27+ user, err := pr.GetUserByID(patchset.UserID)
28+ if err != nil {
29+ return err
30+ }
31+
32+ pk := sesh.PublicKey()
33+ isAdmin := be.IsAdmin(pk)
34+ isContrib := pubkey == user.Pubkey
35+ if !isAdmin && !isContrib {
36+ return fmt.Errorf("you are not authorized to delete a patchset")
37+ }
38+
39+ err = pr.DeletePatchsetByID(patchsetID)
40+ if err != nil {
41+ return err
42+ }
43+ wish.Printf(sesh, "successfully removed patchset: %d\n", patchsetID)
44+ return nil
45 },
46 },
47 },
48@@ -597,17 +620,18 @@ Here's how it works:
49 return err
50 }
51
52- user, err := pr.UpsertUser(pubkey, userName)
53+ patchReq, err := pr.GetPatchRequestByID(prID)
54 if err != nil {
55 return err
56 }
57
58- patchReq, err := pr.GetPatchRequestByID(prID)
59+ user, err := pr.GetUserByID(patchReq.UserID)
60 if err != nil {
61 return err
62 }
63+
64 pk := sesh.PublicKey()
65- isContrib := be.Pubkey(pk) == user.Pubkey
66+ isContrib := pubkey == user.Pubkey
67 isAdmin := be.IsAdmin(pk)
68 if !isAdmin && !isContrib {
69 return fmt.Errorf("you are not authorized to change PR status")
70@@ -645,13 +669,13 @@ Here's how it works:
71 return err
72 }
73
74- user, err := pr.UpsertUser(pubkey, userName)
75+ user, err := pr.GetUserByID(patchReq.UserID)
76 if err != nil {
77 return err
78 }
79
80 pk := sesh.PublicKey()
81- isContrib := be.Pubkey(pk) == user.Pubkey
82+ isContrib := pubkey == user.Pubkey
83 isAdmin := be.IsAdmin(pk)
84 if !isAdmin && !isContrib {
85 return fmt.Errorf("you are not authorized to change PR status")
86diff --git a/pr.go b/pr.go
87index 0078ecf..1a35e13 100644
88--- a/pr.go
89+++ b/pr.go
90@@ -34,6 +34,7 @@ type GitPatchRequest interface {
91 GetPatchRequests() ([]*PatchRequest, error)
92 GetPatchRequestsByRepoID(repoID string) ([]*PatchRequest, error)
93 GetPatchsetsByPrID(prID int64) ([]*Patchset, error)
94+ GetPatchsetByID(patchsetID int64) (*Patchset, error)
95 GetLatestPatchsetByPrID(prID int64) (*Patchset, error)
96 GetPatchesByPatchsetID(prID int64) ([]*Patch, error)
97 UpdatePatchRequestStatus(prID, userID int64, status string) error
98@@ -234,6 +235,16 @@ func (pr PrCmd) GetPatchsetsByPrID(prID int64) ([]*Patchset, error) {
99 return patchsets, nil
100 }
101
102+func (pr PrCmd) GetPatchsetByID(patchsetID int64) (*Patchset, error) {
103+ var patchset Patchset
104+ err := pr.Backend.DB.Get(
105+ &patchset,
106+ "SELECT * FROM patchsets WHERE id=?",
107+ patchsetID,
108+ )
109+ return &patchset, err
110+}
111+
112 func (pr PrCmd) GetLatestPatchsetByPrID(prID int64) (*Patchset, error) {
113 patchsets, err := pr.GetPatchsetsByPrID(prID)
114 if err != nil {
115
116base-commit: 8bfa5553d898de829e896c897efde7a1e0d98276
117--
1182.45.2
119
ps-4
by
erock
on Diff ↕
REVIEW
review: typo and future enhancement comment
jolheiser <git@jolheiser.com>
Signed-off-by: jolheiser <git@jolheiser.com>
web.go | 3 +++ 1 file changed, 3 insertions(+)
1From ef749a401e19707f4cd8bdc193a49951285e5947 Mon Sep 17 00:00:00 2001
2From: jolheiser <git@jolheiser.com>
3Date: Fri, 19 Jul 2024 10:40:15 -0500
4Subject: [PATCH 2/2] review: typo and future enhancement comment
5
6Signed-off-by: jolheiser <git@jolheiser.com>
7---
8 web.go | 3 +++
9 1 file changed, 3 insertions(+)
10
11diff --git a/web.go b/web.go
12index 99bfec6..022c259 100644
13--- a/web.go
14+++ b/web.go
15@@ -663,6 +663,9 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
16 }
17 }
18
19+// review(jolheiser):
20+// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
21+// embeded -> embedded
22 func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
23 dir := filepath.Join(datadir, dirName)
24 _, err := os.Stat(dir)
25--
262.45.1
27
ps-5
by
jolheiser
on Diff ↕
refactor: per-file override for static folder
Eric Bower <me@erock.io>
web.go | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-)
1From c038404dd712c5c04dc9825daffc3d9fc0d2ae68 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 13:43:12 -0400
4Subject: [PATCH 3/3] refactor: per-file override for static folder
5
6---
7 web.go | 42 +++++++++++++++++++++++++++---------------
8 1 file changed, 27 insertions(+), 15 deletions(-)
9
10diff --git a/web.go b/web.go
11index 022c259..9419dea 100644
12--- a/web.go
13+++ b/web.go
14@@ -28,7 +28,7 @@ import (
15 var tmplFS embed.FS
16
17 //go:embed static/*
18-var staticFS embed.FS
19+var embedStaticFS embed.FS
20
21 type WebCtx struct {
22 Pr *PrCmd
23@@ -625,7 +625,7 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
24 }
25 }
26
27-func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
28+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
29 return func(w http.ResponseWriter, r *http.Request) {
30 web, err := getWebCtx(r)
31 if err != nil {
32@@ -635,13 +635,26 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
33 logger := web.Logger
34
35 file := r.PathValue("file")
36- logger.Info("serving file", "file", file, "fs", staticfs)
37- reader, err := staticfs.Open(file)
38+
39+ logger.Info("serving file", "file", file)
40+ // merging both embedded fs and whatever user provides
41+ var reader fs.File
42+ if userfs == nil {
43+ reader, err = embedfs.Open(file)
44+ } else {
45+ reader, err = userfs.Open(file)
46+ if err != nil {
47+ // serve embeded static folder
48+ reader, err = embedfs.Open(file)
49+ }
50+ }
51+
52 if err != nil {
53 logger.Error(err.Error())
54 http.Error(w, "file not found", 404)
55 return
56 }
57+
58 contents, err := io.ReadAll(reader)
59 if err != nil {
60 logger.Error(err.Error())
61@@ -663,23 +676,20 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
62 }
63 }
64
65-// review(jolheiser):
66-// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
67-// embeded -> embedded
68-func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
69+func getUserDefinedFS(datadir, dirName string) fs.FS {
70 dir := filepath.Join(datadir, dirName)
71 _, err := os.Stat(dir)
72- if err == nil {
73- logger.Info("found folder in data_dir", "dir", dir)
74- return os.DirFS(dir), nil
75+ if err != nil {
76+ return nil
77 }
78+ return os.DirFS(dir)
79+}
80
81- logger.Info("using embeded folder", "dir", dir)
82+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
83 fsys, err := fs.Sub(ffs, dirName)
84 if err != nil {
85 return nil, err
86 }
87-
88 return fsys, nil
89 }
90
91@@ -724,11 +734,13 @@ func StartWebServer(cfg *GitCfg) {
92 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
93 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
94 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
95- filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
96+ embedFS, err := getEmbedFS(embedStaticFS, "static")
97 if err != nil {
98 panic(err)
99 }
100- http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
101+ userFS := getUserDefinedFS(cfg.DataDir, "static")
102+
103+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
104
105 cfg.Logger.Info("starting web server", "addr", addr)
106 err = http.ListenAndServe(addr, nil)
107--
1082.45.2
109
ps-9
by
erock
on feat: static assets folder
Eric Bower <me@erock.io>
Create a static folder that will be served as-is with the ability for users to bring-their-own static folder. If we detect `data_dir/static/` we will serve that instead of the embedded one we provide by default.
Makefile | 5 + patches/smol.diff | 53 ++++ ssh.go | 2 +- static/smol.css | 726 ++++++++++++++++++++++++++++++++++++++++++++++ static/vars.css | 18 ++ tmpl/base.html | 5 +- web.go | 69 ++++- 7 files changed, 874 insertions(+), 4 deletions(-) create mode 100644 patches/smol.diff create mode 100644 static/smol.css create mode 100644 static/vars.css
1From 0467f9eefe7a52405b81052b2480f1ef4e53d3fe Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 00:27:17 -0400
4Subject: [PATCH 1/3] feat: static assets folder
5
6Create a static folder that will be served as-is with the ability for
7users to bring-their-own static folder.
8
9If we detect `data_dir/static/` we will serve that instead of the
10embedded one we provide by default.
11---
12 Makefile | 5 +
13 patches/smol.diff | 53 ++++
14 ssh.go | 2 +-
15 static/smol.css | 726 ++++++++++++++++++++++++++++++++++++++++++++++
16 static/vars.css | 18 ++
17 tmpl/base.html | 5 +-
18 web.go | 69 ++++-
19 7 files changed, 874 insertions(+), 4 deletions(-)
20 create mode 100644 patches/smol.diff
21 create mode 100644 static/smol.css
22 create mode 100644 static/vars.css
23
24diff --git a/Makefile b/Makefile
25index cb3a367..e521f82 100644
26--- a/Makefile
27+++ b/Makefile
28@@ -26,3 +26,8 @@ bp: bp-setup
29 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
30 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
31 .PHONY: bp
32+
33+smol:
34+ curl https://pico.sh/smol.css -o ./static/smol.css
35+ cat patches/smol.diff | git apply
36+.PHONY: smol
37diff --git a/patches/smol.diff b/patches/smol.diff
38new file mode 100644
39index 0000000..ae2df55
40--- /dev/null
41+++ b/patches/smol.diff
42@@ -0,0 +1,53 @@
43+diff --git a/static/smol.css b/static/smol.css
44+index e9b59ec..9e9d925 100644
45+--- a/static/smol.css
46++++ b/static/smol.css
47+@@ -15,48 +15,6 @@
48+ box-shadow: none;
49+ }
50+
51+-@media (prefers-color-scheme: light) {
52+- :root {
53+- --main-hue: 250;
54+- --white: #2e3f53;
55+- --white-light: #cfe0f4;
56+- --white-dark: #6c6a6a;
57+- --code: #52576f;
58+- --pre: #e1e7ee;
59+- --bg-color: #f4f4f4;
60+- --text-color: #24292f;
61+- --link-color: #005cc5;
62+- --visited: #6f42c1;
63+- --blockquote: #005cc5;
64+- --blockquote-bg: #cfe0f4;
65+- --hover: #c11e7a;
66+- --grey: #ccc;
67+- --grey-light: #6a708e;
68+- --shadow: #e8e8e8;
69+- }
70+-}
71+-
72+-@media (prefers-color-scheme: dark) {
73+- :root {
74+- --main-hue: 250;
75+- --white: #f2f2f2;
76+- --white-light: #f2f2f2;
77+- --white-dark: #e8e8e8;
78+- --code: #414558;
79+- --pre: #252525;
80+- --bg-color: #282a36;
81+- --text-color: #f2f2f2;
82+- --link-color: #8be9fd;
83+- --visited: #bd93f9;
84+- --blockquote: #bd93f9;
85+- --blockquote-bg: #353548;
86+- --hover: #ff80bf;
87+- --grey: #414558;
88+- --grey-light: #6a708e;
89+- --shadow: #252525;
90+- }
91+-}
92+-
93+ html {
94+ background-color: var(--bg-color);
95+ color: var(--text-color);
96diff --git a/ssh.go b/ssh.go
97index 1fb34d0..3e5e407 100644
98--- a/ssh.go
99+++ b/ssh.go
100@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
101 dbpath := filepath.Join(cfg.DataDir, "pr.db")
102 dbh, err := Open(dbpath, cfg.Logger)
103 if err != nil {
104- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
105+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
106 }
107
108 be := &Backend{
109diff --git a/static/smol.css b/static/smol.css
110new file mode 100644
111index 0000000..9e9d925
112--- /dev/null
113+++ b/static/smol.css
114@@ -0,0 +1,726 @@
115+*,
116+::before,
117+::after {
118+ box-sizing: border-box;
119+}
120+
121+::-moz-focus-inner {
122+ border-style: none;
123+ padding: 0;
124+}
125+:-moz-focusring {
126+ outline: 1px dotted ButtonText;
127+}
128+:-moz-ui-invalid {
129+ box-shadow: none;
130+}
131+
132+html {
133+ background-color: var(--bg-color);
134+ color: var(--text-color);
135+ font-size: 18px;
136+ line-height: 1.5;
137+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
138+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
139+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
140+ -webkit-text-size-adjust: 100%;
141+ -moz-tab-size: 4;
142+ -o-tab-size: 4;
143+ tab-size: 4;
144+}
145+
146+body {
147+ margin: 0 auto;
148+}
149+
150+img {
151+ max-width: 100%;
152+ height: auto;
153+}
154+
155+b,
156+strong {
157+ font-weight: bold;
158+}
159+
160+code,
161+kbd,
162+samp,
163+pre {
164+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
165+ monospace;
166+}
167+
168+code,
169+kbd,
170+samp {
171+ border: 2px solid var(--code);
172+}
173+
174+pre > code {
175+ background-color: inherit;
176+ padding: 0;
177+ border: none;
178+}
179+
180+code {
181+ font-size: 90%;
182+ border-radius: 0.3rem;
183+ padding: 0.1rem 0.3rem;
184+}
185+
186+pre {
187+ font-size: 14px;
188+ border-radius: 5px;
189+ padding: 1rem;
190+ margin: 1rem 0;
191+ overflow-x: auto;
192+ background-color: var(--pre) !important;
193+}
194+
195+small {
196+ font-size: 0.8rem;
197+}
198+
199+summary {
200+ display: list-item;
201+ cursor: pointer;
202+}
203+
204+h1,
205+h2,
206+h3,
207+h4 {
208+ margin: 0;
209+ padding: 0.5rem 0 0 0;
210+ border: 0;
211+ font-style: normal;
212+ font-weight: inherit;
213+ font-size: inherit;
214+}
215+
216+path {
217+ fill: var(--text-color);
218+ stroke: var(--text-color);
219+}
220+
221+hr {
222+ color: inherit;
223+ border: 0;
224+ margin: 0;
225+ height: 2px;
226+ background: var(--grey);
227+ margin: 1rem auto;
228+ text-align: center;
229+}
230+
231+a {
232+ text-decoration: none;
233+ color: var(--link-color);
234+}
235+
236+a:hover,
237+a:visited:hover {
238+ text-decoration: underline;
239+ color: var(--hover);
240+}
241+
242+a:visited {
243+ color: var(--visited);
244+}
245+
246+a.link-grey {
247+ text-decoration: underline;
248+ color: var(--white);
249+}
250+
251+a.link-grey:visited {
252+ color: var(--white);
253+}
254+
255+section {
256+ margin-bottom: 1.4rem;
257+}
258+
259+section:last-child {
260+ margin-bottom: 0;
261+}
262+
263+header {
264+ margin: 1rem auto;
265+}
266+
267+p {
268+ margin: 0.5rem 0;
269+}
270+
271+article {
272+ overflow-wrap: break-word;
273+}
274+
275+blockquote {
276+ border-left: 5px solid var(--blockquote);
277+ background-color: var(--blockquote-bg);
278+ padding: 0.5rem 0.75rem;
279+ margin: 0.5rem 0;
280+}
281+
282+blockquote > p {
283+ margin: 0;
284+}
285+
286+blockquote code {
287+ border: 1px solid var(--blockquote);
288+}
289+
290+ul,
291+ol {
292+ padding: 0 0 0 1rem;
293+ list-style-position: outside;
294+}
295+
296+ul[style*="list-style-type: none;"] {
297+ padding: 0;
298+}
299+
300+li {
301+ margin: 0.5rem 0;
302+}
303+
304+li > pre {
305+ padding: 0;
306+}
307+
308+footer {
309+ text-align: center;
310+ margin-bottom: 4rem;
311+}
312+
313+dt {
314+ font-weight: bold;
315+}
316+
317+dd {
318+ margin-left: 0;
319+}
320+
321+dd:not(:last-child) {
322+ margin-bottom: 0.5rem;
323+}
324+
325+figure {
326+ margin: 0;
327+}
328+
329+.container {
330+ max-width: 50em;
331+ width: 100%;
332+}
333+
334+.container-sm {
335+ max-width: 40em;
336+ width: 100%;
337+}
338+
339+.container-center {
340+ width: 100%;
341+ height: 100%;
342+ display: flex;
343+ justify-content: center;
344+}
345+
346+.mono {
347+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
348+ monospace;
349+}
350+
351+.link-alt-adj,
352+.link-alt-adj:visited,
353+.link-alt-adj:visited:hover,
354+.link-alt-adj:hover {
355+ color: var(--link-color);
356+ text-decoration: none;
357+}
358+
359+.link-alt-adj:visited:hover,
360+.link-alt-adj:hover {
361+ text-decoration: underline;
362+}
363+
364+.link-alt-hover,
365+.link-alt-hover:visited,
366+.link-alt-hover:visited:hover,
367+.link-alt-hover:hover {
368+ color: var(--hover);
369+ text-decoration: none;
370+}
371+
372+.link-alt-hover:visited:hover,
373+.link-alt-hover:hover {
374+ text-decoration: underline;
375+}
376+
377+.link-alt,
378+.link-alt:visited,
379+.link-alt:visited:hover,
380+.link-alt:hover {
381+ color: var(--white);
382+ text-decoration: none;
383+}
384+
385+.link-alt:visited:hover,
386+.link-alt:hover {
387+ text-decoration: underline;
388+}
389+
390+.text-3xl {
391+ font-size: 2.5rem;
392+}
393+
394+.text-2xl {
395+ font-size: 1.9rem;
396+ line-height: 1.15;
397+}
398+
399+.text-xl {
400+ font-size: 1.55rem;
401+ line-height: 1.15;
402+}
403+
404+.text-lg {
405+ font-size: 1.35rem;
406+ line-height: 1.15;
407+}
408+
409+.text-md {
410+ font-size: 1.15rem;
411+ line-height: 1.15;
412+}
413+
414+.text-sm {
415+ font-size: 0.875rem;
416+}
417+
418+.text-xs {
419+ font-size: 0.775rem;
420+}
421+
422+.cursor-pointer {
423+ cursor: pointer;
424+}
425+
426+.w-full {
427+ width: 100%;
428+}
429+
430+.h-full {
431+ height: 100%;
432+}
433+
434+.border {
435+ border: 2px solid var(--grey-light);
436+}
437+
438+.text-left {
439+ text-align: left;
440+}
441+
442+.text-center {
443+ text-align: center;
444+}
445+
446+.text-underline {
447+ border-bottom: 3px solid var(--text-color);
448+ padding-bottom: 3px;
449+}
450+
451+.text-hdr {
452+ color: var(--hover);
453+}
454+
455+.text-underline-hdr {
456+ border-bottom: 3px solid var(--hover);
457+ padding-bottom: 3px;
458+}
459+
460+.font-bold {
461+ font-weight: bold;
462+}
463+
464+.font-italic {
465+ font-style: italic;
466+}
467+
468+.inline {
469+ display: inline;
470+}
471+
472+.inline-block {
473+ display: inline-block;
474+}
475+
476+.max-w-half {
477+ max-width: 50%;
478+}
479+
480+.h-screen {
481+ height: 100vh;
482+}
483+
484+.w-screen {
485+ width: 100vw;
486+}
487+
488+.flex {
489+ display: flex;
490+}
491+
492+.flex-col {
493+ flex-direction: column;
494+}
495+
496+.items-center {
497+ align-items: center;
498+}
499+
500+.m-0 {
501+ margin: 0;
502+}
503+
504+.mt {
505+ margin-top: 0.5rem;
506+}
507+
508+.mt-2 {
509+ margin-top: 1rem;
510+}
511+
512+.mt-4 {
513+ margin-top: 2rem;
514+}
515+
516+.mt-8 {
517+ margin-top: 4rem;
518+}
519+
520+.mb {
521+ margin-bottom: 0.5rem;
522+}
523+
524+.mb-2 {
525+ margin-bottom: 1rem;
526+}
527+
528+.mb-4 {
529+ margin-bottom: 2rem;
530+}
531+
532+.mb-8 {
533+ margin-bottom: 4rem;
534+}
535+
536+.mb-16 {
537+ margin-bottom: 8rem;
538+}
539+
540+.mr {
541+ margin-right: 0.5rem;
542+}
543+
544+.ml-sm {
545+ margin-left: 0.25rem;
546+}
547+
548+.ml {
549+ margin-left: 0.5rem;
550+}
551+
552+.pt-0 {
553+ padding-top: 0;
554+}
555+
556+.my {
557+ margin-top: 0.5rem;
558+ margin-bottom: 0.5rem;
559+}
560+
561+.my-2 {
562+ margin-top: 1rem;
563+ margin-bottom: 1rem;
564+}
565+
566+.my-4 {
567+ margin-top: 2rem;
568+ margin-bottom: 2rem;
569+}
570+
571+.my-8 {
572+ margin-top: 4rem;
573+ margin-bottom: 4rem;
574+}
575+
576+.mx {
577+ margin-left: 0.5rem;
578+ margin-right: 0.5rem;
579+}
580+
581+.mx-2 {
582+ margin-left: 1rem;
583+ margin-right: 1rem;
584+}
585+
586+.m-1 {
587+ margin: 0.5rem;
588+}
589+
590+.p-1 {
591+ padding: 0.5rem;
592+}
593+
594+.p-0 {
595+ padding: 0;
596+}
597+
598+.px-2 {
599+ padding-left: 1rem;
600+ padding-right: 1rem;
601+}
602+
603+.px-4 {
604+ padding-left: 2rem;
605+ padding-right: 2rem;
606+}
607+
608+.py {
609+ padding-top: 0.5rem;
610+ padding-bottom: 0.5rem;
611+}
612+
613+.py-2 {
614+ padding-top: 1rem;
615+ padding-bottom: 1rem;
616+}
617+
618+.py-4 {
619+ padding-top: 2rem;
620+ padding-bottom: 2rem;
621+}
622+
623+.py-8 {
624+ padding-top: 4rem;
625+ padding-bottom: 4rem;
626+}
627+
628+.justify-between {
629+ justify-content: space-between;
630+}
631+
632+.justify-center {
633+ justify-content: center;
634+}
635+
636+.gap {
637+ gap: 0.5rem;
638+}
639+
640+.gap-2 {
641+ gap: 1rem;
642+}
643+
644+.group {
645+ display: flex;
646+ flex-direction: column;
647+ gap: 0.5rem;
648+}
649+
650+.group-2 {
651+ display: flex;
652+ flex-direction: column;
653+ gap: 1rem;
654+}
655+
656+.group-h {
657+ display: flex;
658+ gap: 0.5rem;
659+ align-items: center;
660+}
661+
662+.flex-1 {
663+ flex: 1;
664+}
665+
666+.items-end {
667+ align-items: end;
668+}
669+
670+.items-start {
671+ align-items: start;
672+}
673+
674+.justify-end {
675+ justify-content: end;
676+}
677+
678+.font-grey-light {
679+ color: var(--grey-light);
680+}
681+
682+.hidden {
683+ display: none;
684+}
685+
686+.align-right {
687+ text-align: right;
688+}
689+
690+/* ==== MARKDOWN ==== */
691+
692+.md h1,
693+.md h2,
694+.md h3,
695+.md h4 {
696+ padding: 0;
697+ margin: 1.5rem 0 0.9rem 0;
698+ font-weight: bold;
699+}
700+
701+.md h1 a,
702+.md h2 a,
703+.md h3 a,
704+.md h4 a {
705+ color: var(--grey-light);
706+ text-decoration: none;
707+}
708+
709+.md h1 {
710+ font-size: 1.6rem;
711+ line-height: 1.15;
712+ border-bottom: 2px solid var(--grey);
713+ padding-bottom: 0.7rem;
714+}
715+
716+.md h2 {
717+ font-size: 1.3rem;
718+ line-height: 1.15;
719+ color: var(--white-dark);
720+}
721+
722+.md h3 {
723+ font-size: 1.2rem;
724+ color: var(--white-dark);
725+}
726+
727+.md h4 {
728+ font-size: 1rem;
729+ color: var(--white-dark);
730+}
731+
732+/* ==== HELPERS ==== */
733+
734+.logo-header {
735+ line-height: 1;
736+ display: inline-block;
737+ background-color: #FF79C6;
738+ background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
739+ color: transparent;
740+ background-clip: text;
741+ border: 3px solid #FF79C6;
742+ padding: 8px 10px 10px 10px;
743+ border-radius: 10px;
744+ box-shadow: 0px 5px 0px 0px var(--shadow);
745+ background-size: 100%;
746+ -webkit-background-clip: text;
747+ -moz-background-clip: text;
748+ -webkit-text-fill-color: transparent;
749+ -moz-text-fill-color: transparent;
750+}
751+
752+.btn {
753+ border: 2px solid var(--link-color);
754+ color: var(--link-color);
755+ padding: 0.4rem 1rem;
756+ font-weight: bold;
757+ display: inline-block;
758+}
759+
760+.btn-link,
761+.btn-link:visited {
762+ border: 2px solid var(--link-color);
763+ color: var(--link-color);
764+ padding: 0.4rem 1rem;
765+ text-decoration: none;
766+ font-weight: bold;
767+ display: inline-block;
768+}
769+
770+.btn-link:visited:hover,
771+.btn-link:hover {
772+ border: 2px solid var(--hover);
773+}
774+
775+.btn-link-alt,
776+.btn-link-alt:visited {
777+ border: 2px solid var(--white);
778+ color: var(--white);
779+}
780+
781+.box {
782+ border: 2px solid var(--grey-light);
783+ padding: 0.5rem 0.75rem;
784+}
785+
786+.box-sm {
787+ border: 2px solid var(--grey-light);
788+ padding: 0.15rem 0.35rem;
789+}
790+
791+.box-alert {
792+ border: 2px solid var(--hover);
793+ padding: 0.5rem 0.75rem;
794+}
795+
796+.box-sm-alert {
797+ border: 2px solid var(--hover);
798+ padding: 0.15rem 0.35rem;
799+}
800+
801+.list-none {
802+ list-style-type: none;
803+}
804+
805+.list-disc {
806+ list-style-type: disc;
807+}
808+
809+.list-decimal {
810+ list-style-type: decimal;
811+}
812+
813+.pill {
814+ border: 1px solid var(--link-color);
815+ color: var(--link-color);
816+}
817+
818+.pill-alert {
819+ border: 1px solid var(--hover);
820+ color: var(--hover);
821+}
822+
823+.pill-info {
824+ border: 1px solid var(--visited);
825+ color: var(--visited);
826+}
827+
828+@media only screen and (max-width: 40em) {
829+ body {
830+ padding: 0 1rem;
831+ }
832+
833+ header {
834+ margin: 0;
835+ }
836+
837+ .flex-collapse {
838+ flex-direction: column;
839+ }
840+}
841diff --git a/static/vars.css b/static/vars.css
842new file mode 100644
843index 0000000..6300534
844--- /dev/null
845+++ b/static/vars.css
846@@ -0,0 +1,18 @@
847+:root {
848+ --main-hue: 250;
849+ --white: #f2f2f2;
850+ --white-light: #f2f2f2;
851+ --white-dark: #e8e8e8;
852+ --code: #414558;
853+ --pre: #252525;
854+ --bg-color: #282a36;
855+ --text-color: #f2f2f2;
856+ --link-color: #8be9fd;
857+ --visited: #bd93f9;
858+ --blockquote: #bd93f9;
859+ --blockquote-bg: #353548;
860+ --hover: #ff80bf;
861+ --grey: #414558;
862+ --grey-light: #6a708e;
863+ --shadow: #252525;
864+}
865diff --git a/tmpl/base.html b/tmpl/base.html
866index a656cd5..b5f5fb3 100644
867--- a/tmpl/base.html
868+++ b/tmpl/base.html
869@@ -9,8 +9,9 @@
870 <meta name="keywords" content="git, collaboration, patch, requests" />
871 {{template "meta" .}}
872
873- <link rel="stylesheet" href="https://pico.sh/smol.css" />
874- <link rel="stylesheet" href="https://pico.sh/syntax.css" />
875+ <link rel="stylesheet" href="/static/smol.css" />
876+ <link rel="stylesheet" href="/static/vars.css" />
877+ <link rel="stylesheet" href="/syntax.css" />
878 </head>
879 <body class="container">{{template "body" .}}</body>
880 </html>
881diff --git a/web.go b/web.go
882index 4e782c8..99bfec6 100644
883--- a/web.go
884+++ b/web.go
885@@ -6,8 +6,12 @@ import (
886 "embed"
887 "fmt"
888 "html/template"
889+ "io"
890+ "io/fs"
891 "log/slog"
892+ "mime"
893 "net/http"
894+ "os"
895 "path/filepath"
896 "slices"
897 "strconv"
898@@ -23,6 +27,9 @@ import (
899 //go:embed tmpl/*
900 var tmplFS embed.FS
901
902+//go:embed static/*
903+var staticFS embed.FS
904+
905 type WebCtx struct {
906 Pr *PrCmd
907 Backend *Backend
908@@ -618,13 +625,68 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
909 }
910 }
911
912+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
913+ return func(w http.ResponseWriter, r *http.Request) {
914+ web, err := getWebCtx(r)
915+ if err != nil {
916+ w.WriteHeader(http.StatusUnprocessableEntity)
917+ return
918+ }
919+ logger := web.Logger
920+
921+ file := r.PathValue("file")
922+ logger.Info("serving file", "file", file, "fs", staticfs)
923+ reader, err := staticfs.Open(file)
924+ if err != nil {
925+ logger.Error(err.Error())
926+ http.Error(w, "file not found", 404)
927+ return
928+ }
929+ contents, err := io.ReadAll(reader)
930+ if err != nil {
931+ logger.Error(err.Error())
932+ http.Error(w, "file not found", 404)
933+ return
934+ }
935+ contentType := mime.TypeByExtension(filepath.Ext(file))
936+ if contentType == "" {
937+ contentType = http.DetectContentType(contents)
938+ }
939+ w.Header().Add("Content-Type", contentType)
940+
941+ _, err = w.Write(contents)
942+ if err != nil {
943+ logger.Error(err.Error())
944+ http.Error(w, "server error", 500)
945+ return
946+ }
947+ }
948+}
949+
950+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
951+ dir := filepath.Join(datadir, dirName)
952+ _, err := os.Stat(dir)
953+ if err == nil {
954+ logger.Info("found folder in data_dir", "dir", dir)
955+ return os.DirFS(dir), nil
956+ }
957+
958+ logger.Info("using embeded folder", "dir", dir)
959+ fsys, err := fs.Sub(ffs, dirName)
960+ if err != nil {
961+ return nil, err
962+ }
963+
964+ return fsys, nil
965+}
966+
967 func StartWebServer(cfg *GitCfg) {
968 addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
969
970 dbpath := filepath.Join(cfg.DataDir, "pr.db")
971 dbh, err := Open(dbpath, cfg.Logger)
972 if err != nil {
973- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
974+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
975 }
976
977 be := &Backend{
978@@ -659,6 +721,11 @@ func StartWebServer(cfg *GitCfg) {
979 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
980 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
981 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
982+ filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
983+ if err != nil {
984+ panic(err)
985+ }
986+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
987
988 cfg.Logger.Info("starting web server", "addr", addr)
989 err = http.ListenAndServe(addr, nil)
990
991base-commit: 02ac4fd774be1b1ccf643a8a769248c0bf5ab6be
992--
9932.45.2
994
review: typo and future enhancement comment
jolheiser <git@jolheiser.com>
Signed-off-by: jolheiser <git@jolheiser.com>
web.go | 3 +++ 1 file changed, 3 insertions(+)
1From da1730ff6885e95d558ba15cbc689b8dc50e4eae Mon Sep 17 00:00:00 2001
2From: jolheiser <git@jolheiser.com>
3Date: Fri, 19 Jul 2024 10:40:15 -0500
4Subject: [PATCH 2/3] review: typo and future enhancement comment
5
6Signed-off-by: jolheiser <git@jolheiser.com>
7---
8 web.go | 3 +++
9 1 file changed, 3 insertions(+)
10
11diff --git a/web.go b/web.go
12index 99bfec6..022c259 100644
13--- a/web.go
14+++ b/web.go
15@@ -663,6 +663,9 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
16 }
17 }
18
19+// review(jolheiser):
20+// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
21+// embeded -> embedded
22 func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
23 dir := filepath.Join(datadir, dirName)
24 _, err := os.Stat(dir)
25--
262.45.2
27
refactor: per-file override for static folder
Eric Bower <me@erock.io>
web.go | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-)
1From c038404dd712c5c04dc9825daffc3d9fc0d2ae68 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Fri, 19 Jul 2024 13:43:12 -0400
4Subject: [PATCH 3/3] refactor: per-file override for static folder
5
6---
7 web.go | 42 +++++++++++++++++++++++++++---------------
8 1 file changed, 27 insertions(+), 15 deletions(-)
9
10diff --git a/web.go b/web.go
11index 022c259..9419dea 100644
12--- a/web.go
13+++ b/web.go
14@@ -28,7 +28,7 @@ import (
15 var tmplFS embed.FS
16
17 //go:embed static/*
18-var staticFS embed.FS
19+var embedStaticFS embed.FS
20
21 type WebCtx struct {
22 Pr *PrCmd
23@@ -625,7 +625,7 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
24 }
25 }
26
27-func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
28+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
29 return func(w http.ResponseWriter, r *http.Request) {
30 web, err := getWebCtx(r)
31 if err != nil {
32@@ -635,13 +635,26 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
33 logger := web.Logger
34
35 file := r.PathValue("file")
36- logger.Info("serving file", "file", file, "fs", staticfs)
37- reader, err := staticfs.Open(file)
38+
39+ logger.Info("serving file", "file", file)
40+ // merging both embedded fs and whatever user provides
41+ var reader fs.File
42+ if userfs == nil {
43+ reader, err = embedfs.Open(file)
44+ } else {
45+ reader, err = userfs.Open(file)
46+ if err != nil {
47+ // serve embeded static folder
48+ reader, err = embedfs.Open(file)
49+ }
50+ }
51+
52 if err != nil {
53 logger.Error(err.Error())
54 http.Error(w, "file not found", 404)
55 return
56 }
57+
58 contents, err := io.ReadAll(reader)
59 if err != nil {
60 logger.Error(err.Error())
61@@ -663,23 +676,20 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
62 }
63 }
64
65-// review(jolheiser):
66-// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
67-// embeded -> embedded
68-func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
69+func getUserDefinedFS(datadir, dirName string) fs.FS {
70 dir := filepath.Join(datadir, dirName)
71 _, err := os.Stat(dir)
72- if err == nil {
73- logger.Info("found folder in data_dir", "dir", dir)
74- return os.DirFS(dir), nil
75+ if err != nil {
76+ return nil
77 }
78+ return os.DirFS(dir)
79+}
80
81- logger.Info("using embeded folder", "dir", dir)
82+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
83 fsys, err := fs.Sub(ffs, dirName)
84 if err != nil {
85 return nil, err
86 }
87-
88 return fsys, nil
89 }
90
91@@ -724,11 +734,13 @@ func StartWebServer(cfg *GitCfg) {
92 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
93 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
94 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
95- filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
96+ embedFS, err := getEmbedFS(embedStaticFS, "static")
97 if err != nil {
98 panic(err)
99 }
100- http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
101+ userFS := getUserDefinedFS(cfg.DataDir, "static")
102+
103+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
104
105 cfg.Logger.Info("starting web server", "addr", addr)
106 err = http.ListenAndServe(addr, nil)
107--
1082.45.2
109