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