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