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