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 db.Spindle
305}
306
307func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
308 return p.executePlain("spindles/fragments/spindleListing", w, params)
309}
310
311type SpindleDashboardParams struct {
312 LoggedInUser *oauth.User
313 Spindle db.Spindle
314 Members []string
315 Repos map[string][]db.Repo
316 DidHandleMap map[string]string
317}
318
319func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
320 return p.execute("spindles/dashboard", w, params)
321}
322
323type NewRepoParams struct {
324 LoggedInUser *oauth.User
325 Knots []string
326}
327
328func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
329 return p.execute("repo/new", w, params)
330}
331
332type ForkRepoParams struct {
333 LoggedInUser *oauth.User
334 Knots []string
335 RepoInfo repoinfo.RepoInfo
336}
337
338func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
339 return p.execute("repo/fork", w, params)
340}
341
342type ProfilePageParams struct {
343 LoggedInUser *oauth.User
344 Repos []db.Repo
345 CollaboratingRepos []db.Repo
346 ProfileTimeline *db.ProfileTimeline
347 Card ProfileCard
348 Punchcard db.Punchcard
349
350 DidHandleMap map[string]string
351}
352
353type ProfileCard struct {
354 UserDid string
355 UserHandle string
356 FollowStatus db.FollowStatus
357 AvatarUri string
358 Followers int
359 Following int
360
361 Profile *db.Profile
362}
363
364func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
365 return p.execute("user/profile", w, params)
366}
367
368type ReposPageParams struct {
369 LoggedInUser *oauth.User
370 Repos []db.Repo
371 Card ProfileCard
372
373 DidHandleMap map[string]string
374}
375
376func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
377 return p.execute("user/repos", w, params)
378}
379
380type FollowFragmentParams struct {
381 UserDid string
382 FollowStatus db.FollowStatus
383}
384
385func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
386 return p.executePlain("user/fragments/follow", w, params)
387}
388
389type EditBioParams struct {
390 LoggedInUser *oauth.User
391 Profile *db.Profile
392}
393
394func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
395 return p.executePlain("user/fragments/editBio", w, params)
396}
397
398type EditPinsParams struct {
399 LoggedInUser *oauth.User
400 Profile *db.Profile
401 AllRepos []PinnedRepo
402 DidHandleMap map[string]string
403}
404
405type PinnedRepo struct {
406 IsPinned bool
407 db.Repo
408}
409
410func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
411 return p.executePlain("user/fragments/editPins", w, params)
412}
413
414type RepoActionsFragmentParams struct {
415 IsStarred bool
416 RepoAt syntax.ATURI
417 Stats db.RepoStats
418}
419
420func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error {
421 return p.executePlain("repo/fragments/repoActions", w, params)
422}
423
424type RepoDescriptionParams struct {
425 RepoInfo repoinfo.RepoInfo
426}
427
428func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
429 return p.executePlain("repo/fragments/editRepoDescription", w, params)
430}
431
432func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
433 return p.executePlain("repo/fragments/repoDescription", w, params)
434}
435
436type RepoIndexParams struct {
437 LoggedInUser *oauth.User
438 RepoInfo repoinfo.RepoInfo
439 Active string
440 TagMap map[string][]string
441 CommitsTrunc []*object.Commit
442 TagsTrunc []*types.TagReference
443 BranchesTrunc []types.Branch
444 ForkInfo *types.ForkInfo
445 HTMLReadme template.HTML
446 Raw bool
447 EmailToDidOrHandle map[string]string
448 VerifiedCommits commitverify.VerifiedCommits
449 Languages *types.RepoLanguageResponse
450 Pipelines map[string]db.Pipeline
451 types.RepoIndexResponse
452}
453
454func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
455 params.Active = "overview"
456 if params.IsEmpty {
457 return p.executeRepo("repo/empty", w, params)
458 }
459
460 p.rctx.RepoInfo = params.RepoInfo
461 p.rctx.RendererType = markup.RendererTypeRepoMarkdown
462
463 if params.ReadmeFileName != "" {
464 var htmlString string
465 ext := filepath.Ext(params.ReadmeFileName)
466 switch ext {
467 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd":
468 htmlString = p.rctx.RenderMarkdown(params.Readme)
469 params.Raw = false
470 params.HTMLReadme = template.HTML(p.rctx.Sanitize(htmlString))
471 default:
472 htmlString = string(params.Readme)
473 params.Raw = true
474 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString))
475 }
476 }
477
478 return p.executeRepo("repo/index", w, params)
479}
480
481type RepoLogParams struct {
482 LoggedInUser *oauth.User
483 RepoInfo repoinfo.RepoInfo
484 TagMap map[string][]string
485 types.RepoLogResponse
486 Active string
487 EmailToDidOrHandle map[string]string
488 VerifiedCommits commitverify.VerifiedCommits
489 Pipelines map[string]db.Pipeline
490}
491
492func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
493 params.Active = "overview"
494 return p.executeRepo("repo/log", w, params)
495}
496
497type RepoCommitParams struct {
498 LoggedInUser *oauth.User
499 RepoInfo repoinfo.RepoInfo
500 Active string
501 EmailToDidOrHandle map[string]string
502 Pipeline *db.Pipeline
503
504 // singular because it's always going to be just one
505 VerifiedCommit commitverify.VerifiedCommits
506
507 types.RepoCommitResponse
508}
509
510func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
511 params.Active = "overview"
512 return p.executeRepo("repo/commit", w, params)
513}
514
515type RepoTreeParams struct {
516 LoggedInUser *oauth.User
517 RepoInfo repoinfo.RepoInfo
518 Active string
519 BreadCrumbs [][]string
520 BaseTreeLink string
521 BaseBlobLink string
522 types.RepoTreeResponse
523}
524
525type RepoTreeStats struct {
526 NumFolders uint64
527 NumFiles uint64
528}
529
530func (r RepoTreeParams) TreeStats() RepoTreeStats {
531 numFolders, numFiles := 0, 0
532 for _, f := range r.Files {
533 if !f.IsFile {
534 numFolders += 1
535 } else if f.IsFile {
536 numFiles += 1
537 }
538 }
539
540 return RepoTreeStats{
541 NumFolders: uint64(numFolders),
542 NumFiles: uint64(numFiles),
543 }
544}
545
546func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
547 params.Active = "overview"
548 return p.execute("repo/tree", w, params)
549}
550
551type RepoBranchesParams struct {
552 LoggedInUser *oauth.User
553 RepoInfo repoinfo.RepoInfo
554 Active string
555 types.RepoBranchesResponse
556}
557
558func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
559 params.Active = "overview"
560 return p.executeRepo("repo/branches", w, params)
561}
562
563type RepoTagsParams struct {
564 LoggedInUser *oauth.User
565 RepoInfo repoinfo.RepoInfo
566 Active string
567 types.RepoTagsResponse
568 ArtifactMap map[plumbing.Hash][]db.Artifact
569 DanglingArtifacts []db.Artifact
570}
571
572func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
573 params.Active = "overview"
574 return p.executeRepo("repo/tags", w, params)
575}
576
577type RepoArtifactParams struct {
578 LoggedInUser *oauth.User
579 RepoInfo repoinfo.RepoInfo
580 Artifact db.Artifact
581}
582
583func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
584 return p.executePlain("repo/fragments/artifact", w, params)
585}
586
587type RepoBlobParams struct {
588 LoggedInUser *oauth.User
589 RepoInfo repoinfo.RepoInfo
590 Active string
591 BreadCrumbs [][]string
592 ShowRendered bool
593 RenderToggle bool
594 RenderedContents template.HTML
595 types.RepoBlobResponse
596}
597
598func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
599 var style *chroma.Style = styles.Get("catpuccin-latte")
600
601 if params.ShowRendered {
602 switch markup.GetFormat(params.Path) {
603 case markup.FormatMarkdown:
604 p.rctx.RepoInfo = params.RepoInfo
605 p.rctx.RendererType = markup.RendererTypeRepoMarkdown
606 htmlString := p.rctx.RenderMarkdown(params.Contents)
607 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString))
608 }
609 }
610
611 if params.Lines < 5000 {
612 c := params.Contents
613 formatter := chromahtml.New(
614 chromahtml.InlineCode(false),
615 chromahtml.WithLineNumbers(true),
616 chromahtml.WithLinkableLineNumbers(true, "L"),
617 chromahtml.Standalone(false),
618 chromahtml.WithClasses(true),
619 )
620
621 lexer := lexers.Get(filepath.Base(params.Path))
622 if lexer == nil {
623 lexer = lexers.Fallback
624 }
625
626 iterator, err := lexer.Tokenise(nil, c)
627 if err != nil {
628 return fmt.Errorf("chroma tokenize: %w", err)
629 }
630
631 var code bytes.Buffer
632 err = formatter.Format(&code, style, iterator)
633 if err != nil {
634 return fmt.Errorf("chroma format: %w", err)
635 }
636
637 params.Contents = code.String()
638 }
639
640 params.Active = "overview"
641 return p.executeRepo("repo/blob", w, params)
642}
643
644type Collaborator struct {
645 Did string
646 Handle string
647 Role string
648}
649
650type RepoSettingsParams struct {
651 LoggedInUser *oauth.User
652 RepoInfo repoinfo.RepoInfo
653 Collaborators []Collaborator
654 Active string
655 Branches []types.Branch
656 Spindles []string
657 CurrentSpindle string
658 // TODO: use repoinfo.roles
659 IsCollaboratorInviteAllowed bool
660}
661
662func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
663 params.Active = "settings"
664 return p.executeRepo("repo/settings", w, params)
665}
666
667type RepoIssuesParams struct {
668 LoggedInUser *oauth.User
669 RepoInfo repoinfo.RepoInfo
670 Active string
671 Issues []db.Issue
672 DidHandleMap map[string]string
673 Page pagination.Page
674 FilteringByOpen bool
675}
676
677func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
678 params.Active = "issues"
679 return p.executeRepo("repo/issues/issues", w, params)
680}
681
682type RepoSingleIssueParams struct {
683 LoggedInUser *oauth.User
684 RepoInfo repoinfo.RepoInfo
685 Active string
686 Issue db.Issue
687 Comments []db.Comment
688 IssueOwnerHandle string
689 DidHandleMap map[string]string
690
691 State string
692}
693
694func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
695 params.Active = "issues"
696 if params.Issue.Open {
697 params.State = "open"
698 } else {
699 params.State = "closed"
700 }
701 return p.execute("repo/issues/issue", w, params)
702}
703
704type RepoNewIssueParams struct {
705 LoggedInUser *oauth.User
706 RepoInfo repoinfo.RepoInfo
707 Active string
708}
709
710func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
711 params.Active = "issues"
712 return p.executeRepo("repo/issues/new", w, params)
713}
714
715type EditIssueCommentParams struct {
716 LoggedInUser *oauth.User
717 RepoInfo repoinfo.RepoInfo
718 Issue *db.Issue
719 Comment *db.Comment
720}
721
722func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
723 return p.executePlain("repo/issues/fragments/editIssueComment", w, params)
724}
725
726type SingleIssueCommentParams struct {
727 LoggedInUser *oauth.User
728 DidHandleMap map[string]string
729 RepoInfo repoinfo.RepoInfo
730 Issue *db.Issue
731 Comment *db.Comment
732}
733
734func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error {
735 return p.executePlain("repo/issues/fragments/issueComment", w, params)
736}
737
738type RepoNewPullParams struct {
739 LoggedInUser *oauth.User
740 RepoInfo repoinfo.RepoInfo
741 Branches []types.Branch
742 Strategy string
743 SourceBranch string
744 TargetBranch string
745 Title string
746 Body string
747 Active string
748}
749
750func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
751 params.Active = "pulls"
752 return p.executeRepo("repo/pulls/new", w, params)
753}
754
755type RepoPullsParams struct {
756 LoggedInUser *oauth.User
757 RepoInfo repoinfo.RepoInfo
758 Pulls []*db.Pull
759 Active string
760 DidHandleMap map[string]string
761 FilteringBy db.PullState
762 Stacks map[string]db.Stack
763}
764
765func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
766 params.Active = "pulls"
767 return p.executeRepo("repo/pulls/pulls", w, params)
768}
769
770type ResubmitResult uint64
771
772const (
773 ShouldResubmit ResubmitResult = iota
774 ShouldNotResubmit
775 Unknown
776)
777
778func (r ResubmitResult) Yes() bool {
779 return r == ShouldResubmit
780}
781func (r ResubmitResult) No() bool {
782 return r == ShouldNotResubmit
783}
784func (r ResubmitResult) Unknown() bool {
785 return r == Unknown
786}
787
788type RepoSinglePullParams struct {
789 LoggedInUser *oauth.User
790 RepoInfo repoinfo.RepoInfo
791 Active string
792 DidHandleMap map[string]string
793 Pull *db.Pull
794 Stack db.Stack
795 AbandonedPulls []*db.Pull
796 MergeCheck types.MergeCheckResponse
797 ResubmitCheck ResubmitResult
798}
799
800func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
801 params.Active = "pulls"
802 return p.executeRepo("repo/pulls/pull", w, params)
803}
804
805type RepoPullPatchParams struct {
806 LoggedInUser *oauth.User
807 DidHandleMap map[string]string
808 RepoInfo repoinfo.RepoInfo
809 Pull *db.Pull
810 Stack db.Stack
811 Diff *types.NiceDiff
812 Round int
813 Submission *db.PullSubmission
814}
815
816// this name is a mouthful
817func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error {
818 return p.execute("repo/pulls/patch", w, params)
819}
820
821type RepoPullInterdiffParams struct {
822 LoggedInUser *oauth.User
823 DidHandleMap map[string]string
824 RepoInfo repoinfo.RepoInfo
825 Pull *db.Pull
826 Round int
827 Interdiff *patchutil.InterdiffResult
828}
829
830// this name is a mouthful
831func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error {
832 return p.execute("repo/pulls/interdiff", w, params)
833}
834
835type PullPatchUploadParams struct {
836 RepoInfo repoinfo.RepoInfo
837}
838
839func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {
840 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params)
841}
842
843type PullCompareBranchesParams struct {
844 RepoInfo repoinfo.RepoInfo
845 Branches []types.Branch
846 SourceBranch string
847}
848
849func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error {
850 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params)
851}
852
853type PullCompareForkParams struct {
854 RepoInfo repoinfo.RepoInfo
855 Forks []db.Repo
856 Selected string
857}
858
859func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error {
860 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params)
861}
862
863type PullCompareForkBranchesParams struct {
864 RepoInfo repoinfo.RepoInfo
865 SourceBranches []types.Branch
866 TargetBranches []types.Branch
867}
868
869func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error {
870 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params)
871}
872
873type PullResubmitParams struct {
874 LoggedInUser *oauth.User
875 RepoInfo repoinfo.RepoInfo
876 Pull *db.Pull
877 SubmissionId int
878}
879
880func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
881 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
882}
883
884type PullActionsParams struct {
885 LoggedInUser *oauth.User
886 RepoInfo repoinfo.RepoInfo
887 Pull *db.Pull
888 RoundNumber int
889 MergeCheck types.MergeCheckResponse
890 ResubmitCheck ResubmitResult
891 Stack db.Stack
892}
893
894func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
895 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
896}
897
898type PullNewCommentParams struct {
899 LoggedInUser *oauth.User
900 RepoInfo repoinfo.RepoInfo
901 Pull *db.Pull
902 RoundNumber int
903}
904
905func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
906 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
907}
908
909type RepoCompareParams struct {
910 LoggedInUser *oauth.User
911 RepoInfo repoinfo.RepoInfo
912 Forks []db.Repo
913 Branches []types.Branch
914 Tags []*types.TagReference
915 Base string
916 Head string
917 Diff *types.NiceDiff
918
919 Active string
920}
921
922func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
923 params.Active = "overview"
924 return p.executeRepo("repo/compare/compare", w, params)
925}
926
927type RepoCompareNewParams struct {
928 LoggedInUser *oauth.User
929 RepoInfo repoinfo.RepoInfo
930 Forks []db.Repo
931 Branches []types.Branch
932 Tags []*types.TagReference
933 Base string
934 Head string
935
936 Active string
937}
938
939func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
940 params.Active = "overview"
941 return p.executeRepo("repo/compare/new", w, params)
942}
943
944type RepoCompareAllowPullParams struct {
945 LoggedInUser *oauth.User
946 RepoInfo repoinfo.RepoInfo
947 Base string
948 Head string
949}
950
951func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
952 return p.executePlain("repo/fragments/compareAllowPull", w, params)
953}
954
955type RepoCompareDiffParams struct {
956 LoggedInUser *oauth.User
957 RepoInfo repoinfo.RepoInfo
958 Diff types.NiceDiff
959}
960
961func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
962 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
963}
964
965type PipelinesParams struct {
966 LoggedInUser *oauth.User
967 RepoInfo repoinfo.RepoInfo
968 Pipelines []db.Pipeline
969 Active string
970}
971
972func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
973 params.Active = "pipelines"
974 return p.executeRepo("repo/pipelines/pipelines", w, params)
975}
976
977type LogBlockParams struct {
978 Id int
979 Name string
980 Command string
981 Collapsed bool
982}
983
984func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
985 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
986}
987
988type LogLineParams struct {
989 Id int
990 Content string
991}
992
993func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
994 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
995}
996
997type WorkflowParams struct {
998 LoggedInUser *oauth.User
999 RepoInfo repoinfo.RepoInfo
1000 Pipeline db.Pipeline
1001 Workflow string
1002 LogUrl string
1003 Active string
1004}
1005
1006func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1007 params.Active = "pipelines"
1008 return p.executeRepo("repo/pipelines/workflow", w, params)
1009}
1010
1011func (p *Pages) Static() http.Handler {
1012 if p.dev {
1013 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1014 }
1015
1016 sub, err := fs.Sub(Files, "static")
1017 if err != nil {
1018 log.Fatalf("no static dir found? that's crazy: %v", err)
1019 }
1020 // Custom handler to apply Cache-Control headers for font files
1021 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1022}
1023
1024func Cache(h http.Handler) http.Handler {
1025 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1026 path := strings.Split(r.URL.Path, "?")[0]
1027
1028 if strings.HasSuffix(path, ".css") {
1029 // on day for css files
1030 w.Header().Set("Cache-Control", "public, max-age=86400")
1031 } else {
1032 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1033 }
1034 h.ServeHTTP(w, r)
1035 })
1036}
1037
1038func CssContentHash() string {
1039 cssFile, err := Files.Open("static/tw.css")
1040 if err != nil {
1041 log.Printf("Error opening CSS file: %v", err)
1042 return ""
1043 }
1044 defer cssFile.Close()
1045
1046 hasher := sha256.New()
1047 if _, err := io.Copy(hasher, cssFile); err != nil {
1048 log.Printf("Error hashing CSS file: %v", err)
1049 return ""
1050 }
1051
1052 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1053}
1054
1055func (p *Pages) Error500(w io.Writer) error {
1056 return p.execute("errors/500", w, nil)
1057}
1058
1059func (p *Pages) Error404(w io.Writer) error {
1060 return p.execute("errors/404", w, nil)
1061}
1062
1063func (p *Pages) Error503(w io.Writer) error {
1064 return p.execute("errors/503", w, nil)
1065}