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