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