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 531 // singular because it's always going to be just one 532 VerifiedCommit commitverify.VerifiedCommits 533 534 types.RepoCommitResponse 535} 536 537func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 538 params.Active = "overview" 539 return p.executeRepo("repo/commit", w, params) 540} 541 542type RepoTreeParams struct { 543 LoggedInUser *oauth.User 544 RepoInfo repoinfo.RepoInfo 545 Active string 546 BreadCrumbs [][]string 547 BaseTreeLink string 548 BaseBlobLink string 549 types.RepoTreeResponse 550} 551 552type RepoTreeStats struct { 553 NumFolders uint64 554 NumFiles uint64 555} 556 557func (r RepoTreeParams) TreeStats() RepoTreeStats { 558 numFolders, numFiles := 0, 0 559 for _, f := range r.Files { 560 if !f.IsFile { 561 numFolders += 1 562 } else if f.IsFile { 563 numFiles += 1 564 } 565 } 566 567 return RepoTreeStats{ 568 NumFolders: uint64(numFolders), 569 NumFiles: uint64(numFiles), 570 } 571} 572 573func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 574 params.Active = "overview" 575 return p.execute("repo/tree", w, params) 576} 577 578type RepoBranchesParams struct { 579 LoggedInUser *oauth.User 580 RepoInfo repoinfo.RepoInfo 581 Active string 582 types.RepoBranchesResponse 583} 584 585func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 586 params.Active = "overview" 587 return p.executeRepo("repo/branches", w, params) 588} 589 590type RepoTagsParams struct { 591 LoggedInUser *oauth.User 592 RepoInfo repoinfo.RepoInfo 593 Active string 594 types.RepoTagsResponse 595 ArtifactMap map[plumbing.Hash][]db.Artifact 596 DanglingArtifacts []db.Artifact 597} 598 599func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 600 params.Active = "overview" 601 return p.executeRepo("repo/tags", w, params) 602} 603 604type RepoArtifactParams struct { 605 LoggedInUser *oauth.User 606 RepoInfo repoinfo.RepoInfo 607 Artifact db.Artifact 608} 609 610func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 611 return p.executePlain("repo/fragments/artifact", w, params) 612} 613 614type RepoBlobParams struct { 615 LoggedInUser *oauth.User 616 RepoInfo repoinfo.RepoInfo 617 Active string 618 BreadCrumbs [][]string 619 ShowRendered bool 620 RenderToggle bool 621 RenderedContents template.HTML 622 types.RepoBlobResponse 623} 624 625func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 626 var style *chroma.Style = styles.Get("catpuccin-latte") 627 628 if params.ShowRendered { 629 switch markup.GetFormat(params.Path) { 630 case markup.FormatMarkdown: 631 p.rctx.RepoInfo = params.RepoInfo 632 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 633 htmlString := p.rctx.RenderMarkdown(params.Contents) 634 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 635 } 636 } 637 638 if params.Lines < 5000 { 639 c := params.Contents 640 formatter := chromahtml.New( 641 chromahtml.InlineCode(false), 642 chromahtml.WithLineNumbers(true), 643 chromahtml.WithLinkableLineNumbers(true, "L"), 644 chromahtml.Standalone(false), 645 chromahtml.WithClasses(true), 646 ) 647 648 lexer := lexers.Get(filepath.Base(params.Path)) 649 if lexer == nil { 650 lexer = lexers.Fallback 651 } 652 653 iterator, err := lexer.Tokenise(nil, c) 654 if err != nil { 655 return fmt.Errorf("chroma tokenize: %w", err) 656 } 657 658 var code bytes.Buffer 659 err = formatter.Format(&code, style, iterator) 660 if err != nil { 661 return fmt.Errorf("chroma format: %w", err) 662 } 663 664 params.Contents = code.String() 665 } 666 667 params.Active = "overview" 668 return p.executeRepo("repo/blob", w, params) 669} 670 671type Collaborator struct { 672 Did string 673 Handle string 674 Role string 675} 676 677type RepoSettingsParams struct { 678 LoggedInUser *oauth.User 679 RepoInfo repoinfo.RepoInfo 680 Collaborators []Collaborator 681 Active string 682 Branches []types.Branch 683 Spindles []string 684 CurrentSpindle string 685 // TODO: use repoinfo.roles 686 IsCollaboratorInviteAllowed bool 687} 688 689func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 690 params.Active = "settings" 691 return p.executeRepo("repo/settings", w, params) 692} 693 694type RepoIssuesParams struct { 695 LoggedInUser *oauth.User 696 RepoInfo repoinfo.RepoInfo 697 Active string 698 Issues []db.Issue 699 DidHandleMap map[string]string 700 Page pagination.Page 701 FilteringByOpen bool 702} 703 704func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 705 params.Active = "issues" 706 return p.executeRepo("repo/issues/issues", w, params) 707} 708 709type RepoSingleIssueParams struct { 710 LoggedInUser *oauth.User 711 RepoInfo repoinfo.RepoInfo 712 Active string 713 Issue db.Issue 714 Comments []db.Comment 715 IssueOwnerHandle string 716 DidHandleMap map[string]string 717 718 OrderedReactionKinds []db.ReactionKind 719 Reactions map[db.ReactionKind]int 720 UserReacted map[db.ReactionKind]bool 721 722 State string 723} 724 725type ThreadReactionFragmentParams struct { 726 ThreadAt syntax.ATURI 727 Kind db.ReactionKind 728 Count int 729 IsReacted bool 730} 731 732func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 733 return p.executePlain("repo/fragments/reaction", w, params) 734} 735 736func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 737 params.Active = "issues" 738 if params.Issue.Open { 739 params.State = "open" 740 } else { 741 params.State = "closed" 742 } 743 return p.execute("repo/issues/issue", w, params) 744} 745 746type RepoNewIssueParams struct { 747 LoggedInUser *oauth.User 748 RepoInfo repoinfo.RepoInfo 749 Active string 750} 751 752func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 753 params.Active = "issues" 754 return p.executeRepo("repo/issues/new", w, params) 755} 756 757type EditIssueCommentParams struct { 758 LoggedInUser *oauth.User 759 RepoInfo repoinfo.RepoInfo 760 Issue *db.Issue 761 Comment *db.Comment 762} 763 764func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 765 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 766} 767 768type SingleIssueCommentParams struct { 769 LoggedInUser *oauth.User 770 DidHandleMap map[string]string 771 RepoInfo repoinfo.RepoInfo 772 Issue *db.Issue 773 Comment *db.Comment 774} 775 776func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 777 return p.executePlain("repo/issues/fragments/issueComment", w, params) 778} 779 780type RepoNewPullParams struct { 781 LoggedInUser *oauth.User 782 RepoInfo repoinfo.RepoInfo 783 Branches []types.Branch 784 Strategy string 785 SourceBranch string 786 TargetBranch string 787 Title string 788 Body string 789 Active string 790} 791 792func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 793 params.Active = "pulls" 794 return p.executeRepo("repo/pulls/new", w, params) 795} 796 797type RepoPullsParams struct { 798 LoggedInUser *oauth.User 799 RepoInfo repoinfo.RepoInfo 800 Pulls []*db.Pull 801 Active string 802 DidHandleMap map[string]string 803 FilteringBy db.PullState 804 Stacks map[string]db.Stack 805} 806 807func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 808 params.Active = "pulls" 809 return p.executeRepo("repo/pulls/pulls", w, params) 810} 811 812type ResubmitResult uint64 813 814const ( 815 ShouldResubmit ResubmitResult = iota 816 ShouldNotResubmit 817 Unknown 818) 819 820func (r ResubmitResult) Yes() bool { 821 return r == ShouldResubmit 822} 823func (r ResubmitResult) No() bool { 824 return r == ShouldNotResubmit 825} 826func (r ResubmitResult) Unknown() bool { 827 return r == Unknown 828} 829 830type RepoSinglePullParams struct { 831 LoggedInUser *oauth.User 832 RepoInfo repoinfo.RepoInfo 833 Active string 834 DidHandleMap map[string]string 835 Pull *db.Pull 836 Stack db.Stack 837 AbandonedPulls []*db.Pull 838 MergeCheck types.MergeCheckResponse 839 ResubmitCheck ResubmitResult 840 Pipelines map[string]db.Pipeline 841 842 OrderedReactionKinds []db.ReactionKind 843 Reactions map[db.ReactionKind]int 844 UserReacted map[db.ReactionKind]bool 845} 846 847func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 848 params.Active = "pulls" 849 return p.executeRepo("repo/pulls/pull", w, params) 850} 851 852type RepoPullPatchParams struct { 853 LoggedInUser *oauth.User 854 DidHandleMap map[string]string 855 RepoInfo repoinfo.RepoInfo 856 Pull *db.Pull 857 Stack db.Stack 858 Diff *types.NiceDiff 859 Round int 860 Submission *db.PullSubmission 861 OrderedReactionKinds []db.ReactionKind 862} 863 864// this name is a mouthful 865func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 866 return p.execute("repo/pulls/patch", w, params) 867} 868 869type RepoPullInterdiffParams struct { 870 LoggedInUser *oauth.User 871 DidHandleMap map[string]string 872 RepoInfo repoinfo.RepoInfo 873 Pull *db.Pull 874 Round int 875 Interdiff *patchutil.InterdiffResult 876 OrderedReactionKinds []db.ReactionKind 877} 878 879// this name is a mouthful 880func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 881 return p.execute("repo/pulls/interdiff", w, params) 882} 883 884type PullPatchUploadParams struct { 885 RepoInfo repoinfo.RepoInfo 886} 887 888func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 889 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 890} 891 892type PullCompareBranchesParams struct { 893 RepoInfo repoinfo.RepoInfo 894 Branches []types.Branch 895 SourceBranch string 896} 897 898func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 899 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 900} 901 902type PullCompareForkParams struct { 903 RepoInfo repoinfo.RepoInfo 904 Forks []db.Repo 905 Selected string 906} 907 908func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 909 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 910} 911 912type PullCompareForkBranchesParams struct { 913 RepoInfo repoinfo.RepoInfo 914 SourceBranches []types.Branch 915 TargetBranches []types.Branch 916} 917 918func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 919 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 920} 921 922type PullResubmitParams struct { 923 LoggedInUser *oauth.User 924 RepoInfo repoinfo.RepoInfo 925 Pull *db.Pull 926 SubmissionId int 927} 928 929func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 930 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 931} 932 933type PullActionsParams struct { 934 LoggedInUser *oauth.User 935 RepoInfo repoinfo.RepoInfo 936 Pull *db.Pull 937 RoundNumber int 938 MergeCheck types.MergeCheckResponse 939 ResubmitCheck ResubmitResult 940 Stack db.Stack 941} 942 943func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 944 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 945} 946 947type PullNewCommentParams struct { 948 LoggedInUser *oauth.User 949 RepoInfo repoinfo.RepoInfo 950 Pull *db.Pull 951 RoundNumber int 952} 953 954func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 955 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 956} 957 958type RepoCompareParams struct { 959 LoggedInUser *oauth.User 960 RepoInfo repoinfo.RepoInfo 961 Forks []db.Repo 962 Branches []types.Branch 963 Tags []*types.TagReference 964 Base string 965 Head string 966 Diff *types.NiceDiff 967 968 Active string 969} 970 971func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 972 params.Active = "overview" 973 return p.executeRepo("repo/compare/compare", w, params) 974} 975 976type RepoCompareNewParams struct { 977 LoggedInUser *oauth.User 978 RepoInfo repoinfo.RepoInfo 979 Forks []db.Repo 980 Branches []types.Branch 981 Tags []*types.TagReference 982 Base string 983 Head string 984 985 Active string 986} 987 988func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 989 params.Active = "overview" 990 return p.executeRepo("repo/compare/new", w, params) 991} 992 993type RepoCompareAllowPullParams struct { 994 LoggedInUser *oauth.User 995 RepoInfo repoinfo.RepoInfo 996 Base string 997 Head string 998} 999 1000func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1001 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1002} 1003 1004type RepoCompareDiffParams struct { 1005 LoggedInUser *oauth.User 1006 RepoInfo repoinfo.RepoInfo 1007 Diff types.NiceDiff 1008} 1009 1010func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1011 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1012} 1013 1014type PipelinesParams struct { 1015 LoggedInUser *oauth.User 1016 RepoInfo repoinfo.RepoInfo 1017 Pipelines []db.Pipeline 1018 Active string 1019} 1020 1021func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1022 params.Active = "pipelines" 1023 return p.executeRepo("repo/pipelines/pipelines", w, params) 1024} 1025 1026type LogBlockParams struct { 1027 Id int 1028 Name string 1029 Command string 1030 Collapsed bool 1031} 1032 1033func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1034 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1035} 1036 1037type LogLineParams struct { 1038 Id int 1039 Content string 1040} 1041 1042func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1043 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1044} 1045 1046type WorkflowParams struct { 1047 LoggedInUser *oauth.User 1048 RepoInfo repoinfo.RepoInfo 1049 Pipeline db.Pipeline 1050 Workflow string 1051 LogUrl string 1052 Active string 1053} 1054 1055func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1056 params.Active = "pipelines" 1057 return p.executeRepo("repo/pipelines/workflow", w, params) 1058} 1059 1060func (p *Pages) Static() http.Handler { 1061 if p.dev { 1062 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1063 } 1064 1065 sub, err := fs.Sub(Files, "static") 1066 if err != nil { 1067 log.Fatalf("no static dir found? that's crazy: %v", err) 1068 } 1069 // Custom handler to apply Cache-Control headers for font files 1070 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1071} 1072 1073func Cache(h http.Handler) http.Handler { 1074 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1075 path := strings.Split(r.URL.Path, "?")[0] 1076 1077 if strings.HasSuffix(path, ".css") { 1078 // on day for css files 1079 w.Header().Set("Cache-Control", "public, max-age=86400") 1080 } else { 1081 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1082 } 1083 h.ServeHTTP(w, r) 1084 }) 1085} 1086 1087func CssContentHash() string { 1088 cssFile, err := Files.Open("static/tw.css") 1089 if err != nil { 1090 log.Printf("Error opening CSS file: %v", err) 1091 return "" 1092 } 1093 defer cssFile.Close() 1094 1095 hasher := sha256.New() 1096 if _, err := io.Copy(hasher, cssFile); err != nil { 1097 log.Printf("Error hashing CSS file: %v", err) 1098 return "" 1099 } 1100 1101 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1102} 1103 1104func (p *Pages) Error500(w io.Writer) error { 1105 return p.execute("errors/500", w, nil) 1106} 1107 1108func (p *Pages) Error404(w io.Writer) error { 1109 return p.execute("errors/404", w, nil) 1110} 1111 1112func (p *Pages) Error503(w io.Writer) error { 1113 return p.execute("errors/503", w, nil) 1114}