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