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