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[plumbing.Hash]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}
479
480func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
481 params.Active = "overview"
482 return p.executeRepo("repo/log", w, params)
483}
484
485type RepoCommitParams struct {
486 LoggedInUser *oauth.User
487 RepoInfo repoinfo.RepoInfo
488 Active string
489 EmailToDidOrHandle map[string]string
490
491 // singular because it's always going to be just one
492 VerifiedCommit commitverify.VerifiedCommits
493
494 types.RepoCommitResponse
495}
496
497func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
498 params.Active = "overview"
499 return p.executeRepo("repo/commit", w, params)
500}
501
502type RepoTreeParams struct {
503 LoggedInUser *oauth.User
504 RepoInfo repoinfo.RepoInfo
505 Active string
506 BreadCrumbs [][]string
507 BaseTreeLink string
508 BaseBlobLink string
509 types.RepoTreeResponse
510}
511
512type RepoTreeStats struct {
513 NumFolders uint64
514 NumFiles uint64
515}
516
517func (r RepoTreeParams) TreeStats() RepoTreeStats {
518 numFolders, numFiles := 0, 0
519 for _, f := range r.Files {
520 if !f.IsFile {
521 numFolders += 1
522 } else if f.IsFile {
523 numFiles += 1
524 }
525 }
526
527 return RepoTreeStats{
528 NumFolders: uint64(numFolders),
529 NumFiles: uint64(numFiles),
530 }
531}
532
533func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
534 params.Active = "overview"
535 return p.execute("repo/tree", w, params)
536}
537
538type RepoBranchesParams struct {
539 LoggedInUser *oauth.User
540 RepoInfo repoinfo.RepoInfo
541 Active string
542 types.RepoBranchesResponse
543}
544
545func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
546 params.Active = "overview"
547 return p.executeRepo("repo/branches", w, params)
548}
549
550type RepoTagsParams struct {
551 LoggedInUser *oauth.User
552 RepoInfo repoinfo.RepoInfo
553 Active string
554 types.RepoTagsResponse
555 ArtifactMap map[plumbing.Hash][]db.Artifact
556 DanglingArtifacts []db.Artifact
557}
558
559func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
560 params.Active = "overview"
561 return p.executeRepo("repo/tags", w, params)
562}
563
564type RepoArtifactParams struct {
565 LoggedInUser *oauth.User
566 RepoInfo repoinfo.RepoInfo
567 Artifact db.Artifact
568}
569
570func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
571 return p.executePlain("repo/fragments/artifact", w, params)
572}
573
574type RepoBlobParams struct {
575 LoggedInUser *oauth.User
576 RepoInfo repoinfo.RepoInfo
577 Active string
578 BreadCrumbs [][]string
579 ShowRendered bool
580 RenderToggle bool
581 RenderedContents template.HTML
582 types.RepoBlobResponse
583}
584
585func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
586 var style *chroma.Style = styles.Get("catpuccin-latte")
587
588 if params.ShowRendered {
589 switch markup.GetFormat(params.Path) {
590 case markup.FormatMarkdown:
591 p.rctx.RepoInfo = params.RepoInfo
592 p.rctx.RendererType = markup.RendererTypeRepoMarkdown
593 htmlString := p.rctx.RenderMarkdown(params.Contents)
594 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString))
595 }
596 }
597
598 if params.Lines < 5000 {
599 c := params.Contents
600 formatter := chromahtml.New(
601 chromahtml.InlineCode(false),
602 chromahtml.WithLineNumbers(true),
603 chromahtml.WithLinkableLineNumbers(true, "L"),
604 chromahtml.Standalone(false),
605 chromahtml.WithClasses(true),
606 )
607
608 lexer := lexers.Get(filepath.Base(params.Path))
609 if lexer == nil {
610 lexer = lexers.Fallback
611 }
612
613 iterator, err := lexer.Tokenise(nil, c)
614 if err != nil {
615 return fmt.Errorf("chroma tokenize: %w", err)
616 }
617
618 var code bytes.Buffer
619 err = formatter.Format(&code, style, iterator)
620 if err != nil {
621 return fmt.Errorf("chroma format: %w", err)
622 }
623
624 params.Contents = code.String()
625 }
626
627 params.Active = "overview"
628 return p.executeRepo("repo/blob", w, params)
629}
630
631type Collaborator struct {
632 Did string
633 Handle string
634 Role string
635}
636
637type RepoSettingsParams struct {
638 LoggedInUser *oauth.User
639 RepoInfo repoinfo.RepoInfo
640 Collaborators []Collaborator
641 Active string
642 Branches []types.Branch
643 Spindles []string
644 CurrentSpindle string
645 // TODO: use repoinfo.roles
646 IsCollaboratorInviteAllowed bool
647}
648
649func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
650 params.Active = "settings"
651 return p.executeRepo("repo/settings", w, params)
652}
653
654type RepoIssuesParams struct {
655 LoggedInUser *oauth.User
656 RepoInfo repoinfo.RepoInfo
657 Active string
658 Issues []db.Issue
659 DidHandleMap map[string]string
660 Page pagination.Page
661 FilteringByOpen bool
662}
663
664func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
665 params.Active = "issues"
666 return p.executeRepo("repo/issues/issues", w, params)
667}
668
669type RepoSingleIssueParams struct {
670 LoggedInUser *oauth.User
671 RepoInfo repoinfo.RepoInfo
672 Active string
673 Issue db.Issue
674 Comments []db.Comment
675 IssueOwnerHandle string
676 DidHandleMap map[string]string
677
678 State string
679}
680
681func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
682 params.Active = "issues"
683 if params.Issue.Open {
684 params.State = "open"
685 } else {
686 params.State = "closed"
687 }
688 return p.execute("repo/issues/issue", w, params)
689}
690
691type RepoNewIssueParams struct {
692 LoggedInUser *oauth.User
693 RepoInfo repoinfo.RepoInfo
694 Active string
695}
696
697func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
698 params.Active = "issues"
699 return p.executeRepo("repo/issues/new", w, params)
700}
701
702type EditIssueCommentParams struct {
703 LoggedInUser *oauth.User
704 RepoInfo repoinfo.RepoInfo
705 Issue *db.Issue
706 Comment *db.Comment
707}
708
709func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
710 return p.executePlain("repo/issues/fragments/editIssueComment", w, params)
711}
712
713type SingleIssueCommentParams struct {
714 LoggedInUser *oauth.User
715 DidHandleMap map[string]string
716 RepoInfo repoinfo.RepoInfo
717 Issue *db.Issue
718 Comment *db.Comment
719}
720
721func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error {
722 return p.executePlain("repo/issues/fragments/issueComment", w, params)
723}
724
725type RepoNewPullParams struct {
726 LoggedInUser *oauth.User
727 RepoInfo repoinfo.RepoInfo
728 Branches []types.Branch
729 Strategy string
730 SourceBranch string
731 TargetBranch string
732 Title string
733 Body string
734 Active string
735}
736
737func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
738 params.Active = "pulls"
739 return p.executeRepo("repo/pulls/new", w, params)
740}
741
742type RepoPullsParams struct {
743 LoggedInUser *oauth.User
744 RepoInfo repoinfo.RepoInfo
745 Pulls []*db.Pull
746 Active string
747 DidHandleMap map[string]string
748 FilteringBy db.PullState
749 Stacks map[string]db.Stack
750}
751
752func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
753 params.Active = "pulls"
754 return p.executeRepo("repo/pulls/pulls", w, params)
755}
756
757type ResubmitResult uint64
758
759const (
760 ShouldResubmit ResubmitResult = iota
761 ShouldNotResubmit
762 Unknown
763)
764
765func (r ResubmitResult) Yes() bool {
766 return r == ShouldResubmit
767}
768func (r ResubmitResult) No() bool {
769 return r == ShouldNotResubmit
770}
771func (r ResubmitResult) Unknown() bool {
772 return r == Unknown
773}
774
775type RepoSinglePullParams struct {
776 LoggedInUser *oauth.User
777 RepoInfo repoinfo.RepoInfo
778 Active string
779 DidHandleMap map[string]string
780 Pull *db.Pull
781 Stack db.Stack
782 AbandonedPulls []*db.Pull
783 MergeCheck types.MergeCheckResponse
784 ResubmitCheck ResubmitResult
785}
786
787func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
788 params.Active = "pulls"
789 return p.executeRepo("repo/pulls/pull", w, params)
790}
791
792type RepoPullPatchParams struct {
793 LoggedInUser *oauth.User
794 DidHandleMap map[string]string
795 RepoInfo repoinfo.RepoInfo
796 Pull *db.Pull
797 Stack db.Stack
798 Diff *types.NiceDiff
799 Round int
800 Submission *db.PullSubmission
801}
802
803// this name is a mouthful
804func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error {
805 return p.execute("repo/pulls/patch", w, params)
806}
807
808type RepoPullInterdiffParams struct {
809 LoggedInUser *oauth.User
810 DidHandleMap map[string]string
811 RepoInfo repoinfo.RepoInfo
812 Pull *db.Pull
813 Round int
814 Interdiff *patchutil.InterdiffResult
815}
816
817// this name is a mouthful
818func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error {
819 return p.execute("repo/pulls/interdiff", w, params)
820}
821
822type PullPatchUploadParams struct {
823 RepoInfo repoinfo.RepoInfo
824}
825
826func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {
827 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params)
828}
829
830type PullCompareBranchesParams struct {
831 RepoInfo repoinfo.RepoInfo
832 Branches []types.Branch
833 SourceBranch string
834}
835
836func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error {
837 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params)
838}
839
840type PullCompareForkParams struct {
841 RepoInfo repoinfo.RepoInfo
842 Forks []db.Repo
843 Selected string
844}
845
846func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error {
847 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params)
848}
849
850type PullCompareForkBranchesParams struct {
851 RepoInfo repoinfo.RepoInfo
852 SourceBranches []types.Branch
853 TargetBranches []types.Branch
854}
855
856func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error {
857 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params)
858}
859
860type PullResubmitParams struct {
861 LoggedInUser *oauth.User
862 RepoInfo repoinfo.RepoInfo
863 Pull *db.Pull
864 SubmissionId int
865}
866
867func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
868 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
869}
870
871type PullActionsParams struct {
872 LoggedInUser *oauth.User
873 RepoInfo repoinfo.RepoInfo
874 Pull *db.Pull
875 RoundNumber int
876 MergeCheck types.MergeCheckResponse
877 ResubmitCheck ResubmitResult
878 Stack db.Stack
879}
880
881func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
882 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
883}
884
885type PullNewCommentParams struct {
886 LoggedInUser *oauth.User
887 RepoInfo repoinfo.RepoInfo
888 Pull *db.Pull
889 RoundNumber int
890}
891
892func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
893 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
894}
895
896type RepoCompareParams struct {
897 LoggedInUser *oauth.User
898 RepoInfo repoinfo.RepoInfo
899 Forks []db.Repo
900 Branches []types.Branch
901 Tags []*types.TagReference
902 Base string
903 Head string
904 Diff *types.NiceDiff
905
906 Active string
907}
908
909func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
910 params.Active = "overview"
911 return p.executeRepo("repo/compare/compare", w, params)
912}
913
914type RepoCompareNewParams struct {
915 LoggedInUser *oauth.User
916 RepoInfo repoinfo.RepoInfo
917 Forks []db.Repo
918 Branches []types.Branch
919 Tags []*types.TagReference
920 Base string
921 Head string
922
923 Active string
924}
925
926func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
927 params.Active = "overview"
928 return p.executeRepo("repo/compare/new", w, params)
929}
930
931type RepoCompareAllowPullParams struct {
932 LoggedInUser *oauth.User
933 RepoInfo repoinfo.RepoInfo
934 Base string
935 Head string
936}
937
938func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
939 return p.executePlain("repo/fragments/compareAllowPull", w, params)
940}
941
942type RepoCompareDiffParams struct {
943 LoggedInUser *oauth.User
944 RepoInfo repoinfo.RepoInfo
945 Diff types.NiceDiff
946}
947
948func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
949 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
950}
951
952func (p *Pages) Static() http.Handler {
953 if p.dev {
954 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
955 }
956
957 sub, err := fs.Sub(Files, "static")
958 if err != nil {
959 log.Fatalf("no static dir found? that's crazy: %v", err)
960 }
961 // Custom handler to apply Cache-Control headers for font files
962 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
963}
964
965func Cache(h http.Handler) http.Handler {
966 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
967 path := strings.Split(r.URL.Path, "?")[0]
968
969 if strings.HasSuffix(path, ".css") {
970 // on day for css files
971 w.Header().Set("Cache-Control", "public, max-age=86400")
972 } else {
973 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
974 }
975 h.ServeHTTP(w, r)
976 })
977}
978
979func CssContentHash() string {
980 cssFile, err := Files.Open("static/tw.css")
981 if err != nil {
982 log.Printf("Error opening CSS file: %v", err)
983 return ""
984 }
985 defer cssFile.Close()
986
987 hasher := sha256.New()
988 if _, err := io.Copy(hasher, cssFile); err != nil {
989 log.Printf("Error hashing CSS file: %v", err)
990 return ""
991 }
992
993 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
994}
995
996func (p *Pages) Error500(w io.Writer) error {
997 return p.execute("errors/500", w, nil)
998}
999
1000func (p *Pages) Error404(w io.Writer) error {
1001 return p.execute("errors/404", w, nil)
1002}
1003
1004func (p *Pages) Error503(w io.Writer) error {
1005 return p.execute("errors/503", w, nil)
1006}