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