this repo has no description
1package pages
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "embed"
7 "encoding/hex"
8 "fmt"
9 "html/template"
10 "io"
11 "io/fs"
12 "log"
13 "net/http"
14 "os"
15 "path/filepath"
16 "strings"
17
18 "tangled.sh/tangled.sh/core/appview/commitverify"
19 "tangled.sh/tangled.sh/core/appview/config"
20 "tangled.sh/tangled.sh/core/appview/db"
21 "tangled.sh/tangled.sh/core/appview/oauth"
22 "tangled.sh/tangled.sh/core/appview/pages/markup"
23 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
24 "tangled.sh/tangled.sh/core/appview/pagination"
25 "tangled.sh/tangled.sh/core/patchutil"
26 "tangled.sh/tangled.sh/core/types"
27
28 "github.com/alecthomas/chroma/v2"
29 chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
30 "github.com/alecthomas/chroma/v2/lexers"
31 "github.com/alecthomas/chroma/v2/styles"
32 "github.com/bluesky-social/indigo/atproto/syntax"
33 "github.com/go-git/go-git/v5/plumbing"
34 "github.com/go-git/go-git/v5/plumbing/object"
35 "github.com/microcosm-cc/bluemonday"
36)
37
38//go:embed templates/* static
39var Files embed.FS
40
41type Pages struct {
42 t map[string]*template.Template
43 dev bool
44 embedFS embed.FS
45 templateDir string // Path to templates on disk for dev mode
46 rctx *markup.RenderContext
47}
48
49func NewPages(config *config.Config) *Pages {
50 // initialized with safe defaults, can be overriden per use
51 rctx := &markup.RenderContext{
52 IsDev: config.Core.Dev,
53 CamoUrl: config.Camo.Host,
54 CamoSecret: config.Camo.SharedSecret,
55 }
56
57 p := &Pages{
58 t: make(map[string]*template.Template),
59 dev: config.Core.Dev,
60 embedFS: Files,
61 rctx: rctx,
62 templateDir: "appview/pages",
63 }
64
65 // Initial load of all templates
66 p.loadAllTemplates()
67
68 return p
69}
70
71func (p *Pages) loadAllTemplates() {
72 templates := make(map[string]*template.Template)
73 var fragmentPaths []string
74
75 // Use embedded FS for initial loading
76 // First, collect all fragment paths
77 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
78 if err != nil {
79 return err
80 }
81 if d.IsDir() {
82 return nil
83 }
84 if !strings.HasSuffix(path, ".html") {
85 return nil
86 }
87 if !strings.Contains(path, "fragments/") {
88 return nil
89 }
90 name := strings.TrimPrefix(path, "templates/")
91 name = strings.TrimSuffix(name, ".html")
92 tmpl, err := template.New(name).
93 Funcs(funcMap()).
94 ParseFS(p.embedFS, path)
95 if err != nil {
96 log.Fatalf("setting up fragment: %v", err)
97 }
98 templates[name] = tmpl
99 fragmentPaths = append(fragmentPaths, path)
100 log.Printf("loaded fragment: %s", name)
101 return nil
102 })
103 if err != nil {
104 log.Fatalf("walking template dir for fragments: %v", err)
105 }
106
107 // Then walk through and setup the rest of the templates
108 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
109 if err != nil {
110 return err
111 }
112 if d.IsDir() {
113 return nil
114 }
115 if !strings.HasSuffix(path, "html") {
116 return nil
117 }
118 // Skip fragments as they've already been loaded
119 if strings.Contains(path, "fragments/") {
120 return nil
121 }
122 // Skip layouts
123 if strings.Contains(path, "layouts/") {
124 return nil
125 }
126 name := strings.TrimPrefix(path, "templates/")
127 name = strings.TrimSuffix(name, ".html")
128 // Add the page template on top of the base
129 allPaths := []string{}
130 allPaths = append(allPaths, "templates/layouts/*.html")
131 allPaths = append(allPaths, fragmentPaths...)
132 allPaths = append(allPaths, path)
133 tmpl, err := template.New(name).
134 Funcs(funcMap()).
135 ParseFS(p.embedFS, allPaths...)
136 if err != nil {
137 return fmt.Errorf("setting up template: %w", err)
138 }
139 templates[name] = tmpl
140 log.Printf("loaded template: %s", name)
141 return nil
142 })
143 if err != nil {
144 log.Fatalf("walking template dir: %v", err)
145 }
146
147 log.Printf("total templates loaded: %d", len(templates))
148 p.t = templates
149}
150
151// loadTemplateFromDisk loads a template from the filesystem in dev mode
152func (p *Pages) loadTemplateFromDisk(name string) error {
153 if !p.dev {
154 return nil
155 }
156
157 log.Printf("reloading template from disk: %s", name)
158
159 // Find all fragments first
160 var fragmentPaths []string
161 err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error {
162 if err != nil {
163 return err
164 }
165 if d.IsDir() {
166 return nil
167 }
168 if !strings.HasSuffix(path, ".html") {
169 return nil
170 }
171 if !strings.Contains(path, "fragments/") {
172 return nil
173 }
174 fragmentPaths = append(fragmentPaths, path)
175 return nil
176 })
177 if err != nil {
178 return fmt.Errorf("walking disk template dir for fragments: %w", err)
179 }
180
181 // Find the template path on disk
182 templatePath := filepath.Join(p.templateDir, "templates", name+".html")
183 if _, err := os.Stat(templatePath); os.IsNotExist(err) {
184 return fmt.Errorf("template not found on disk: %s", name)
185 }
186
187 // Create a new template
188 tmpl := template.New(name).Funcs(funcMap())
189
190 // Parse layouts
191 layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html")
192 layouts, err := filepath.Glob(layoutGlob)
193 if err != nil {
194 return fmt.Errorf("finding layout templates: %w", err)
195 }
196
197 // Create paths for parsing
198 allFiles := append(layouts, fragmentPaths...)
199 allFiles = append(allFiles, templatePath)
200
201 // Parse all templates
202 tmpl, err = tmpl.ParseFiles(allFiles...)
203 if err != nil {
204 return fmt.Errorf("parsing template files: %w", err)
205 }
206
207 // Update the template in the map
208 p.t[name] = tmpl
209 log.Printf("template reloaded from disk: %s", name)
210 return nil
211}
212
213func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error {
214 // In dev mode, reload the template from disk before executing
215 if p.dev {
216 if err := p.loadTemplateFromDisk(templateName); err != nil {
217 log.Printf("warning: failed to reload template %s from disk: %v", templateName, err)
218 // Continue with the existing template
219 }
220 }
221
222 tmpl, exists := p.t[templateName]
223 if !exists {
224 return fmt.Errorf("template not found: %s", templateName)
225 }
226
227 if base == "" {
228 return tmpl.Execute(w, params)
229 } else {
230 return tmpl.ExecuteTemplate(w, base, params)
231 }
232}
233
234func (p *Pages) execute(name string, w io.Writer, params any) error {
235 return p.executeOrReload(name, w, "layouts/base", params)
236}
237
238func (p *Pages) executePlain(name string, w io.Writer, params any) error {
239 return p.executeOrReload(name, w, "", params)
240}
241
242func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
243 return p.executeOrReload(name, w, "layouts/repobase", params)
244}
245
246type LoginParams struct {
247}
248
249func (p *Pages) Login(w io.Writer, params LoginParams) error {
250 return p.executePlain("user/login", w, params)
251}
252
253type TimelineParams struct {
254 LoggedInUser *oauth.User
255 Timeline []db.TimelineEvent
256 DidHandleMap map[string]string
257}
258
259func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
260 return p.execute("timeline", w, params)
261}
262
263type SettingsParams struct {
264 LoggedInUser *oauth.User
265 PubKeys []db.PublicKey
266 Emails []db.Email
267}
268
269func (p *Pages) Settings(w io.Writer, params SettingsParams) error {
270 return p.execute("settings", w, params)
271}
272
273type KnotsParams struct {
274 LoggedInUser *oauth.User
275 Registrations []db.Registration
276}
277
278func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
279 return p.execute("knots", w, params)
280}
281
282type KnotParams struct {
283 LoggedInUser *oauth.User
284 DidHandleMap map[string]string
285 Registration *db.Registration
286 Members []string
287 IsOwner bool
288}
289
290func (p *Pages) Knot(w io.Writer, params KnotParams) error {
291 return p.execute("knot", w, params)
292}
293
294type SpindlesParams struct {
295 LoggedInUser *oauth.User
296 Spindles []db.Spindle
297}
298
299func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
300 return p.execute("spindles/index", w, params)
301}
302
303type SpindleListingParams struct {
304 LoggedInUser *oauth.User
305 Spindle db.Spindle
306}
307
308func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
309 return p.execute("spindles/fragments/spindleListing", w, params)
310}
311
312type NewRepoParams struct {
313 LoggedInUser *oauth.User
314 Knots []string
315}
316
317func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
318 return p.execute("repo/new", w, params)
319}
320
321type ForkRepoParams struct {
322 LoggedInUser *oauth.User
323 Knots []string
324 RepoInfo repoinfo.RepoInfo
325}
326
327func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
328 return p.execute("repo/fork", w, params)
329}
330
331type ProfilePageParams struct {
332 LoggedInUser *oauth.User
333 Repos []db.Repo
334 CollaboratingRepos []db.Repo
335 ProfileTimeline *db.ProfileTimeline
336 Card ProfileCard
337 Punchcard db.Punchcard
338
339 DidHandleMap map[string]string
340}
341
342type ProfileCard struct {
343 UserDid string
344 UserHandle string
345 FollowStatus db.FollowStatus
346 AvatarUri string
347 Followers int
348 Following int
349
350 Profile *db.Profile
351}
352
353func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
354 return p.execute("user/profile", w, params)
355}
356
357type ReposPageParams struct {
358 LoggedInUser *oauth.User
359 Repos []db.Repo
360 Card ProfileCard
361
362 DidHandleMap map[string]string
363}
364
365func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
366 return p.execute("user/repos", w, params)
367}
368
369type FollowFragmentParams struct {
370 UserDid string
371 FollowStatus db.FollowStatus
372}
373
374func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
375 return p.executePlain("user/fragments/follow", w, params)
376}
377
378type EditBioParams struct {
379 LoggedInUser *oauth.User
380 Profile *db.Profile
381}
382
383func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
384 return p.executePlain("user/fragments/editBio", w, params)
385}
386
387type EditPinsParams struct {
388 LoggedInUser *oauth.User
389 Profile *db.Profile
390 AllRepos []PinnedRepo
391 DidHandleMap map[string]string
392}
393
394type PinnedRepo struct {
395 IsPinned bool
396 db.Repo
397}
398
399func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
400 return p.executePlain("user/fragments/editPins", w, params)
401}
402
403type RepoActionsFragmentParams struct {
404 IsStarred bool
405 RepoAt syntax.ATURI
406 Stats db.RepoStats
407}
408
409func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error {
410 return p.executePlain("repo/fragments/repoActions", w, params)
411}
412
413type RepoDescriptionParams struct {
414 RepoInfo repoinfo.RepoInfo
415}
416
417func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
418 return p.executePlain("repo/fragments/editRepoDescription", w, params)
419}
420
421func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
422 return p.executePlain("repo/fragments/repoDescription", w, params)
423}
424
425type RepoIndexParams struct {
426 LoggedInUser *oauth.User
427 RepoInfo repoinfo.RepoInfo
428 Active string
429 TagMap map[string][]string
430 CommitsTrunc []*object.Commit
431 TagsTrunc []*types.TagReference
432 BranchesTrunc []types.Branch
433 ForkInfo *types.ForkInfo
434 HTMLReadme template.HTML
435 Raw bool
436 EmailToDidOrHandle map[string]string
437 VerifiedCommits commitverify.VerifiedCommits
438 Languages *types.RepoLanguageResponse
439 Pipelines map[string]db.Pipeline
440 types.RepoIndexResponse
441}
442
443func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
444 params.Active = "overview"
445 if params.IsEmpty {
446 return p.executeRepo("repo/empty", w, params)
447 }
448
449 p.rctx.RepoInfo = params.RepoInfo
450 p.rctx.RendererType = markup.RendererTypeRepoMarkdown
451
452 if params.ReadmeFileName != "" {
453 var htmlString string
454 ext := filepath.Ext(params.ReadmeFileName)
455 switch ext {
456 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd":
457 htmlString = p.rctx.RenderMarkdown(params.Readme)
458 params.Raw = false
459 params.HTMLReadme = template.HTML(p.rctx.Sanitize(htmlString))
460 default:
461 htmlString = string(params.Readme)
462 params.Raw = true
463 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString))
464 }
465 }
466
467 return p.executeRepo("repo/index", w, params)
468}
469
470type RepoLogParams struct {
471 LoggedInUser *oauth.User
472 RepoInfo repoinfo.RepoInfo
473 TagMap map[string][]string
474 types.RepoLogResponse
475 Active string
476 EmailToDidOrHandle map[string]string
477 VerifiedCommits commitverify.VerifiedCommits
478 Pipelines map[string]db.Pipeline
479}
480
481func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
482 params.Active = "overview"
483 return p.executeRepo("repo/log", w, params)
484}
485
486type RepoCommitParams struct {
487 LoggedInUser *oauth.User
488 RepoInfo repoinfo.RepoInfo
489 Active string
490 EmailToDidOrHandle map[string]string
491 Pipeline *db.Pipeline
492
493 // singular because it's always going to be just one
494 VerifiedCommit commitverify.VerifiedCommits
495
496 types.RepoCommitResponse
497}
498
499func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
500 params.Active = "overview"
501 return p.executeRepo("repo/commit", w, params)
502}
503
504type RepoTreeParams struct {
505 LoggedInUser *oauth.User
506 RepoInfo repoinfo.RepoInfo
507 Active string
508 BreadCrumbs [][]string
509 BaseTreeLink string
510 BaseBlobLink string
511 types.RepoTreeResponse
512}
513
514type RepoTreeStats struct {
515 NumFolders uint64
516 NumFiles uint64
517}
518
519func (r RepoTreeParams) TreeStats() RepoTreeStats {
520 numFolders, numFiles := 0, 0
521 for _, f := range r.Files {
522 if !f.IsFile {
523 numFolders += 1
524 } else if f.IsFile {
525 numFiles += 1
526 }
527 }
528
529 return RepoTreeStats{
530 NumFolders: uint64(numFolders),
531 NumFiles: uint64(numFiles),
532 }
533}
534
535func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
536 params.Active = "overview"
537 return p.execute("repo/tree", w, params)
538}
539
540type RepoBranchesParams struct {
541 LoggedInUser *oauth.User
542 RepoInfo repoinfo.RepoInfo
543 Active string
544 types.RepoBranchesResponse
545}
546
547func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
548 params.Active = "overview"
549 return p.executeRepo("repo/branches", w, params)
550}
551
552type RepoTagsParams struct {
553 LoggedInUser *oauth.User
554 RepoInfo repoinfo.RepoInfo
555 Active string
556 types.RepoTagsResponse
557 ArtifactMap map[plumbing.Hash][]db.Artifact
558 DanglingArtifacts []db.Artifact
559}
560
561func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
562 params.Active = "overview"
563 return p.executeRepo("repo/tags", w, params)
564}
565
566type RepoArtifactParams struct {
567 LoggedInUser *oauth.User
568 RepoInfo repoinfo.RepoInfo
569 Artifact db.Artifact
570}
571
572func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
573 return p.executePlain("repo/fragments/artifact", w, params)
574}
575
576type RepoBlobParams struct {
577 LoggedInUser *oauth.User
578 RepoInfo repoinfo.RepoInfo
579 Active string
580 BreadCrumbs [][]string
581 ShowRendered bool
582 RenderToggle bool
583 RenderedContents template.HTML
584 types.RepoBlobResponse
585}
586
587func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
588 var style *chroma.Style = styles.Get("catpuccin-latte")
589
590 if params.ShowRendered {
591 switch markup.GetFormat(params.Path) {
592 case markup.FormatMarkdown:
593 p.rctx.RepoInfo = params.RepoInfo
594 p.rctx.RendererType = markup.RendererTypeRepoMarkdown
595 htmlString := p.rctx.RenderMarkdown(params.Contents)
596 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString))
597 }
598 }
599
600 if params.Lines < 5000 {
601 c := params.Contents
602 formatter := chromahtml.New(
603 chromahtml.InlineCode(false),
604 chromahtml.WithLineNumbers(true),
605 chromahtml.WithLinkableLineNumbers(true, "L"),
606 chromahtml.Standalone(false),
607 chromahtml.WithClasses(true),
608 )
609
610 lexer := lexers.Get(filepath.Base(params.Path))
611 if lexer == nil {
612 lexer = lexers.Fallback
613 }
614
615 iterator, err := lexer.Tokenise(nil, c)
616 if err != nil {
617 return fmt.Errorf("chroma tokenize: %w", err)
618 }
619
620 var code bytes.Buffer
621 err = formatter.Format(&code, style, iterator)
622 if err != nil {
623 return fmt.Errorf("chroma format: %w", err)
624 }
625
626 params.Contents = code.String()
627 }
628
629 params.Active = "overview"
630 return p.executeRepo("repo/blob", w, params)
631}
632
633type Collaborator struct {
634 Did string
635 Handle string
636 Role string
637}
638
639type RepoSettingsParams struct {
640 LoggedInUser *oauth.User
641 RepoInfo repoinfo.RepoInfo
642 Collaborators []Collaborator
643 Active string
644 Branches []types.Branch
645 Spindles []string
646 CurrentSpindle string
647 // TODO: use repoinfo.roles
648 IsCollaboratorInviteAllowed bool
649}
650
651func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
652 params.Active = "settings"
653 return p.executeRepo("repo/settings", w, params)
654}
655
656type RepoIssuesParams struct {
657 LoggedInUser *oauth.User
658 RepoInfo repoinfo.RepoInfo
659 Active string
660 Issues []db.Issue
661 DidHandleMap map[string]string
662 Page pagination.Page
663 FilteringByOpen bool
664}
665
666func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
667 params.Active = "issues"
668 return p.executeRepo("repo/issues/issues", w, params)
669}
670
671type RepoSingleIssueParams struct {
672 LoggedInUser *oauth.User
673 RepoInfo repoinfo.RepoInfo
674 Active string
675 Issue db.Issue
676 Comments []db.Comment
677 IssueOwnerHandle string
678 DidHandleMap map[string]string
679
680 State string
681}
682
683func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
684 params.Active = "issues"
685 if params.Issue.Open {
686 params.State = "open"
687 } else {
688 params.State = "closed"
689 }
690 return p.execute("repo/issues/issue", w, params)
691}
692
693type RepoNewIssueParams struct {
694 LoggedInUser *oauth.User
695 RepoInfo repoinfo.RepoInfo
696 Active string
697}
698
699func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
700 params.Active = "issues"
701 return p.executeRepo("repo/issues/new", w, params)
702}
703
704type EditIssueCommentParams struct {
705 LoggedInUser *oauth.User
706 RepoInfo repoinfo.RepoInfo
707 Issue *db.Issue
708 Comment *db.Comment
709}
710
711func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
712 return p.executePlain("repo/issues/fragments/editIssueComment", w, params)
713}
714
715type SingleIssueCommentParams struct {
716 LoggedInUser *oauth.User
717 DidHandleMap map[string]string
718 RepoInfo repoinfo.RepoInfo
719 Issue *db.Issue
720 Comment *db.Comment
721}
722
723func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error {
724 return p.executePlain("repo/issues/fragments/issueComment", w, params)
725}
726
727type RepoNewPullParams struct {
728 LoggedInUser *oauth.User
729 RepoInfo repoinfo.RepoInfo
730 Branches []types.Branch
731 Strategy string
732 SourceBranch string
733 TargetBranch string
734 Title string
735 Body string
736 Active string
737}
738
739func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
740 params.Active = "pulls"
741 return p.executeRepo("repo/pulls/new", w, params)
742}
743
744type RepoPullsParams struct {
745 LoggedInUser *oauth.User
746 RepoInfo repoinfo.RepoInfo
747 Pulls []*db.Pull
748 Active string
749 DidHandleMap map[string]string
750 FilteringBy db.PullState
751 Stacks map[string]db.Stack
752}
753
754func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
755 params.Active = "pulls"
756 return p.executeRepo("repo/pulls/pulls", w, params)
757}
758
759type ResubmitResult uint64
760
761const (
762 ShouldResubmit ResubmitResult = iota
763 ShouldNotResubmit
764 Unknown
765)
766
767func (r ResubmitResult) Yes() bool {
768 return r == ShouldResubmit
769}
770func (r ResubmitResult) No() bool {
771 return r == ShouldNotResubmit
772}
773func (r ResubmitResult) Unknown() bool {
774 return r == Unknown
775}
776
777type RepoSinglePullParams struct {
778 LoggedInUser *oauth.User
779 RepoInfo repoinfo.RepoInfo
780 Active string
781 DidHandleMap map[string]string
782 Pull *db.Pull
783 Stack db.Stack
784 AbandonedPulls []*db.Pull
785 MergeCheck types.MergeCheckResponse
786 ResubmitCheck ResubmitResult
787}
788
789func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
790 params.Active = "pulls"
791 return p.executeRepo("repo/pulls/pull", w, params)
792}
793
794type RepoPullPatchParams struct {
795 LoggedInUser *oauth.User
796 DidHandleMap map[string]string
797 RepoInfo repoinfo.RepoInfo
798 Pull *db.Pull
799 Stack db.Stack
800 Diff *types.NiceDiff
801 Round int
802 Submission *db.PullSubmission
803}
804
805// this name is a mouthful
806func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error {
807 return p.execute("repo/pulls/patch", w, params)
808}
809
810type RepoPullInterdiffParams struct {
811 LoggedInUser *oauth.User
812 DidHandleMap map[string]string
813 RepoInfo repoinfo.RepoInfo
814 Pull *db.Pull
815 Round int
816 Interdiff *patchutil.InterdiffResult
817}
818
819// this name is a mouthful
820func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error {
821 return p.execute("repo/pulls/interdiff", w, params)
822}
823
824type PullPatchUploadParams struct {
825 RepoInfo repoinfo.RepoInfo
826}
827
828func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {
829 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params)
830}
831
832type PullCompareBranchesParams struct {
833 RepoInfo repoinfo.RepoInfo
834 Branches []types.Branch
835 SourceBranch string
836}
837
838func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error {
839 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params)
840}
841
842type PullCompareForkParams struct {
843 RepoInfo repoinfo.RepoInfo
844 Forks []db.Repo
845 Selected string
846}
847
848func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error {
849 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params)
850}
851
852type PullCompareForkBranchesParams struct {
853 RepoInfo repoinfo.RepoInfo
854 SourceBranches []types.Branch
855 TargetBranches []types.Branch
856}
857
858func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error {
859 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params)
860}
861
862type PullResubmitParams struct {
863 LoggedInUser *oauth.User
864 RepoInfo repoinfo.RepoInfo
865 Pull *db.Pull
866 SubmissionId int
867}
868
869func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
870 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
871}
872
873type PullActionsParams struct {
874 LoggedInUser *oauth.User
875 RepoInfo repoinfo.RepoInfo
876 Pull *db.Pull
877 RoundNumber int
878 MergeCheck types.MergeCheckResponse
879 ResubmitCheck ResubmitResult
880 Stack db.Stack
881}
882
883func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
884 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
885}
886
887type PullNewCommentParams struct {
888 LoggedInUser *oauth.User
889 RepoInfo repoinfo.RepoInfo
890 Pull *db.Pull
891 RoundNumber int
892}
893
894func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
895 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
896}
897
898type RepoCompareParams struct {
899 LoggedInUser *oauth.User
900 RepoInfo repoinfo.RepoInfo
901 Forks []db.Repo
902 Branches []types.Branch
903 Tags []*types.TagReference
904 Base string
905 Head string
906 Diff *types.NiceDiff
907
908 Active string
909}
910
911func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
912 params.Active = "overview"
913 return p.executeRepo("repo/compare/compare", w, params)
914}
915
916type RepoCompareNewParams struct {
917 LoggedInUser *oauth.User
918 RepoInfo repoinfo.RepoInfo
919 Forks []db.Repo
920 Branches []types.Branch
921 Tags []*types.TagReference
922 Base string
923 Head string
924
925 Active string
926}
927
928func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
929 params.Active = "overview"
930 return p.executeRepo("repo/compare/new", w, params)
931}
932
933type RepoCompareAllowPullParams struct {
934 LoggedInUser *oauth.User
935 RepoInfo repoinfo.RepoInfo
936 Base string
937 Head string
938}
939
940func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
941 return p.executePlain("repo/fragments/compareAllowPull", w, params)
942}
943
944type RepoCompareDiffParams struct {
945 LoggedInUser *oauth.User
946 RepoInfo repoinfo.RepoInfo
947 Diff types.NiceDiff
948}
949
950func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
951 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
952}
953
954type PipelinesParams struct {
955 LoggedInUser *oauth.User
956 RepoInfo repoinfo.RepoInfo
957 Pipelines []db.Pipeline
958 Active string
959}
960
961func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
962 params.Active = "pipelines"
963 return p.executeRepo("repo/pipelines/pipelines", w, params)
964}
965
966type WorkflowParams struct {
967 LoggedInUser *oauth.User
968 RepoInfo repoinfo.RepoInfo
969 Pipeline db.Pipeline
970 Workflow string
971 Active string
972}
973
974func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
975 params.Active = "pipelines"
976 return p.executeRepo("repo/pipelines/workflow", w, params)
977}
978
979func (p *Pages) Static() http.Handler {
980 if p.dev {
981 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
982 }
983
984 sub, err := fs.Sub(Files, "static")
985 if err != nil {
986 log.Fatalf("no static dir found? that's crazy: %v", err)
987 }
988 // Custom handler to apply Cache-Control headers for font files
989 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
990}
991
992func Cache(h http.Handler) http.Handler {
993 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
994 path := strings.Split(r.URL.Path, "?")[0]
995
996 if strings.HasSuffix(path, ".css") {
997 // on day for css files
998 w.Header().Set("Cache-Control", "public, max-age=86400")
999 } else {
1000 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1001 }
1002 h.ServeHTTP(w, r)
1003 })
1004}
1005
1006func CssContentHash() string {
1007 cssFile, err := Files.Open("static/tw.css")
1008 if err != nil {
1009 log.Printf("Error opening CSS file: %v", err)
1010 return ""
1011 }
1012 defer cssFile.Close()
1013
1014 hasher := sha256.New()
1015 if _, err := io.Copy(hasher, cssFile); err != nil {
1016 log.Printf("Error hashing CSS file: %v", err)
1017 return ""
1018 }
1019
1020 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1021}
1022
1023func (p *Pages) Error500(w io.Writer) error {
1024 return p.execute("errors/500", w, nil)
1025}
1026
1027func (p *Pages) Error404(w io.Writer) error {
1028 return p.execute("errors/404", w, nil)
1029}
1030
1031func (p *Pages) Error503(w io.Writer) error {
1032 return p.execute("errors/503", w, nil)
1033}