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