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