forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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 Unsupported bool 628 IsImage bool 629 IsVideo bool 630 ContentSrc string 631 BreadCrumbs [][]string 632 ShowRendered bool 633 RenderToggle bool 634 RenderedContents template.HTML 635 types.RepoBlobResponse 636} 637 638func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 639 var style *chroma.Style = styles.Get("catpuccin-latte") 640 641 if params.ShowRendered { 642 switch markup.GetFormat(params.Path) { 643 case markup.FormatMarkdown: 644 p.rctx.RepoInfo = params.RepoInfo 645 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 646 htmlString := p.rctx.RenderMarkdown(params.Contents) 647 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 648 } 649 } 650 651 if params.Lines < 5000 { 652 c := params.Contents 653 formatter := chromahtml.New( 654 chromahtml.InlineCode(false), 655 chromahtml.WithLineNumbers(true), 656 chromahtml.WithLinkableLineNumbers(true, "L"), 657 chromahtml.Standalone(false), 658 chromahtml.WithClasses(true), 659 ) 660 661 lexer := lexers.Get(filepath.Base(params.Path)) 662 if lexer == nil { 663 lexer = lexers.Fallback 664 } 665 666 iterator, err := lexer.Tokenise(nil, c) 667 if err != nil { 668 return fmt.Errorf("chroma tokenize: %w", err) 669 } 670 671 var code bytes.Buffer 672 err = formatter.Format(&code, style, iterator) 673 if err != nil { 674 return fmt.Errorf("chroma format: %w", err) 675 } 676 677 params.Contents = code.String() 678 } 679 680 params.Active = "overview" 681 return p.executeRepo("repo/blob", w, params) 682} 683 684type Collaborator struct { 685 Did string 686 Handle string 687 Role string 688} 689 690type RepoSettingsParams struct { 691 LoggedInUser *oauth.User 692 RepoInfo repoinfo.RepoInfo 693 Collaborators []Collaborator 694 Active string 695 Branches []types.Branch 696 Spindles []string 697 CurrentSpindle string 698 Secrets []*tangled.RepoListSecrets_Secret 699 700 // TODO: use repoinfo.roles 701 IsCollaboratorInviteAllowed bool 702} 703 704func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 705 params.Active = "settings" 706 return p.executeRepo("repo/settings", w, params) 707} 708 709type RepoGeneralSettingsParams struct { 710 LoggedInUser *oauth.User 711 RepoInfo repoinfo.RepoInfo 712 Active string 713 Tabs []map[string]any 714 Tab string 715 Branches []types.Branch 716} 717 718func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 719 params.Active = "settings" 720 return p.executeRepo("repo/settings/general", w, params) 721} 722 723type RepoAccessSettingsParams struct { 724 LoggedInUser *oauth.User 725 RepoInfo repoinfo.RepoInfo 726 Active string 727 Tabs []map[string]any 728 Tab string 729 Collaborators []Collaborator 730} 731 732func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 733 params.Active = "settings" 734 return p.executeRepo("repo/settings/access", w, params) 735} 736 737type RepoPipelineSettingsParams struct { 738 LoggedInUser *oauth.User 739 RepoInfo repoinfo.RepoInfo 740 Active string 741 Tabs []map[string]any 742 Tab string 743 Spindles []string 744 CurrentSpindle string 745 Secrets []map[string]any 746} 747 748func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 749 params.Active = "settings" 750 return p.executeRepo("repo/settings/pipelines", w, params) 751} 752 753type RepoIssuesParams struct { 754 LoggedInUser *oauth.User 755 RepoInfo repoinfo.RepoInfo 756 Active string 757 Issues []db.Issue 758 DidHandleMap map[string]string 759 Page pagination.Page 760 FilteringByOpen bool 761} 762 763func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 764 params.Active = "issues" 765 return p.executeRepo("repo/issues/issues", w, params) 766} 767 768type RepoSingleIssueParams struct { 769 LoggedInUser *oauth.User 770 RepoInfo repoinfo.RepoInfo 771 Active string 772 Issue db.Issue 773 Comments []db.Comment 774 IssueOwnerHandle string 775 DidHandleMap map[string]string 776 777 OrderedReactionKinds []db.ReactionKind 778 Reactions map[db.ReactionKind]int 779 UserReacted map[db.ReactionKind]bool 780 781 State string 782} 783 784type ThreadReactionFragmentParams struct { 785 ThreadAt syntax.ATURI 786 Kind db.ReactionKind 787 Count int 788 IsReacted bool 789} 790 791func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 792 return p.executePlain("repo/fragments/reaction", w, params) 793} 794 795func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 796 params.Active = "issues" 797 if params.Issue.Open { 798 params.State = "open" 799 } else { 800 params.State = "closed" 801 } 802 return p.execute("repo/issues/issue", w, params) 803} 804 805type RepoNewIssueParams struct { 806 LoggedInUser *oauth.User 807 RepoInfo repoinfo.RepoInfo 808 Active string 809} 810 811func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 812 params.Active = "issues" 813 return p.executeRepo("repo/issues/new", w, params) 814} 815 816type EditIssueCommentParams struct { 817 LoggedInUser *oauth.User 818 RepoInfo repoinfo.RepoInfo 819 Issue *db.Issue 820 Comment *db.Comment 821} 822 823func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 824 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 825} 826 827type SingleIssueCommentParams struct { 828 LoggedInUser *oauth.User 829 DidHandleMap map[string]string 830 RepoInfo repoinfo.RepoInfo 831 Issue *db.Issue 832 Comment *db.Comment 833} 834 835func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 836 return p.executePlain("repo/issues/fragments/issueComment", w, params) 837} 838 839type RepoNewPullParams struct { 840 LoggedInUser *oauth.User 841 RepoInfo repoinfo.RepoInfo 842 Branches []types.Branch 843 Strategy string 844 SourceBranch string 845 TargetBranch string 846 Title string 847 Body string 848 Active string 849} 850 851func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 852 params.Active = "pulls" 853 return p.executeRepo("repo/pulls/new", w, params) 854} 855 856type RepoPullsParams struct { 857 LoggedInUser *oauth.User 858 RepoInfo repoinfo.RepoInfo 859 Pulls []*db.Pull 860 Active string 861 DidHandleMap map[string]string 862 FilteringBy db.PullState 863 Stacks map[string]db.Stack 864 Pipelines map[string]db.Pipeline 865} 866 867func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 868 params.Active = "pulls" 869 return p.executeRepo("repo/pulls/pulls", w, params) 870} 871 872type ResubmitResult uint64 873 874const ( 875 ShouldResubmit ResubmitResult = iota 876 ShouldNotResubmit 877 Unknown 878) 879 880func (r ResubmitResult) Yes() bool { 881 return r == ShouldResubmit 882} 883func (r ResubmitResult) No() bool { 884 return r == ShouldNotResubmit 885} 886func (r ResubmitResult) Unknown() bool { 887 return r == Unknown 888} 889 890type RepoSinglePullParams struct { 891 LoggedInUser *oauth.User 892 RepoInfo repoinfo.RepoInfo 893 Active string 894 DidHandleMap map[string]string 895 Pull *db.Pull 896 Stack db.Stack 897 AbandonedPulls []*db.Pull 898 MergeCheck types.MergeCheckResponse 899 ResubmitCheck ResubmitResult 900 Pipelines map[string]db.Pipeline 901 902 OrderedReactionKinds []db.ReactionKind 903 Reactions map[db.ReactionKind]int 904 UserReacted map[db.ReactionKind]bool 905} 906 907func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 908 params.Active = "pulls" 909 return p.executeRepo("repo/pulls/pull", w, params) 910} 911 912type RepoPullPatchParams struct { 913 LoggedInUser *oauth.User 914 DidHandleMap map[string]string 915 RepoInfo repoinfo.RepoInfo 916 Pull *db.Pull 917 Stack db.Stack 918 Diff *types.NiceDiff 919 Round int 920 Submission *db.PullSubmission 921 OrderedReactionKinds []db.ReactionKind 922 DiffOpts types.DiffOpts 923} 924 925// this name is a mouthful 926func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 927 return p.execute("repo/pulls/patch", w, params) 928} 929 930type RepoPullInterdiffParams struct { 931 LoggedInUser *oauth.User 932 DidHandleMap map[string]string 933 RepoInfo repoinfo.RepoInfo 934 Pull *db.Pull 935 Round int 936 Interdiff *patchutil.InterdiffResult 937 OrderedReactionKinds []db.ReactionKind 938 DiffOpts types.DiffOpts 939} 940 941// this name is a mouthful 942func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 943 return p.execute("repo/pulls/interdiff", w, params) 944} 945 946type PullPatchUploadParams struct { 947 RepoInfo repoinfo.RepoInfo 948} 949 950func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 951 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 952} 953 954type PullCompareBranchesParams struct { 955 RepoInfo repoinfo.RepoInfo 956 Branches []types.Branch 957 SourceBranch string 958} 959 960func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 961 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 962} 963 964type PullCompareForkParams struct { 965 RepoInfo repoinfo.RepoInfo 966 Forks []db.Repo 967 Selected string 968} 969 970func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 971 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 972} 973 974type PullCompareForkBranchesParams struct { 975 RepoInfo repoinfo.RepoInfo 976 SourceBranches []types.Branch 977 TargetBranches []types.Branch 978} 979 980func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 981 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 982} 983 984type PullResubmitParams struct { 985 LoggedInUser *oauth.User 986 RepoInfo repoinfo.RepoInfo 987 Pull *db.Pull 988 SubmissionId int 989} 990 991func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 992 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 993} 994 995type PullActionsParams struct { 996 LoggedInUser *oauth.User 997 RepoInfo repoinfo.RepoInfo 998 Pull *db.Pull 999 RoundNumber int 1000 MergeCheck types.MergeCheckResponse 1001 ResubmitCheck ResubmitResult 1002 Stack db.Stack 1003} 1004 1005func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1006 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1007} 1008 1009type PullNewCommentParams struct { 1010 LoggedInUser *oauth.User 1011 RepoInfo repoinfo.RepoInfo 1012 Pull *db.Pull 1013 RoundNumber int 1014} 1015 1016func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1017 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1018} 1019 1020type RepoCompareParams struct { 1021 LoggedInUser *oauth.User 1022 RepoInfo repoinfo.RepoInfo 1023 Forks []db.Repo 1024 Branches []types.Branch 1025 Tags []*types.TagReference 1026 Base string 1027 Head string 1028 Diff *types.NiceDiff 1029 DiffOpts types.DiffOpts 1030 1031 Active string 1032} 1033 1034func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1035 params.Active = "overview" 1036 return p.executeRepo("repo/compare/compare", w, params) 1037} 1038 1039type RepoCompareNewParams struct { 1040 LoggedInUser *oauth.User 1041 RepoInfo repoinfo.RepoInfo 1042 Forks []db.Repo 1043 Branches []types.Branch 1044 Tags []*types.TagReference 1045 Base string 1046 Head string 1047 1048 Active string 1049} 1050 1051func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1052 params.Active = "overview" 1053 return p.executeRepo("repo/compare/new", w, params) 1054} 1055 1056type RepoCompareAllowPullParams struct { 1057 LoggedInUser *oauth.User 1058 RepoInfo repoinfo.RepoInfo 1059 Base string 1060 Head string 1061} 1062 1063func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1064 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1065} 1066 1067type RepoCompareDiffParams struct { 1068 LoggedInUser *oauth.User 1069 RepoInfo repoinfo.RepoInfo 1070 Diff types.NiceDiff 1071} 1072 1073func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1074 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1075} 1076 1077type PipelinesParams struct { 1078 LoggedInUser *oauth.User 1079 RepoInfo repoinfo.RepoInfo 1080 Pipelines []db.Pipeline 1081 Active string 1082} 1083 1084func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1085 params.Active = "pipelines" 1086 return p.executeRepo("repo/pipelines/pipelines", w, params) 1087} 1088 1089type LogBlockParams struct { 1090 Id int 1091 Name string 1092 Command string 1093 Collapsed bool 1094} 1095 1096func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1097 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1098} 1099 1100type LogLineParams struct { 1101 Id int 1102 Content string 1103} 1104 1105func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1106 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1107} 1108 1109type WorkflowParams struct { 1110 LoggedInUser *oauth.User 1111 RepoInfo repoinfo.RepoInfo 1112 Pipeline db.Pipeline 1113 Workflow string 1114 LogUrl string 1115 Active string 1116} 1117 1118func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1119 params.Active = "pipelines" 1120 return p.executeRepo("repo/pipelines/workflow", w, params) 1121} 1122 1123func (p *Pages) Static() http.Handler { 1124 if p.dev { 1125 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1126 } 1127 1128 sub, err := fs.Sub(Files, "static") 1129 if err != nil { 1130 log.Fatalf("no static dir found? that's crazy: %v", err) 1131 } 1132 // Custom handler to apply Cache-Control headers for font files 1133 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1134} 1135 1136func Cache(h http.Handler) http.Handler { 1137 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1138 path := strings.Split(r.URL.Path, "?")[0] 1139 1140 if strings.HasSuffix(path, ".css") { 1141 // on day for css files 1142 w.Header().Set("Cache-Control", "public, max-age=86400") 1143 } else { 1144 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1145 } 1146 h.ServeHTTP(w, r) 1147 }) 1148} 1149 1150func CssContentHash() string { 1151 cssFile, err := Files.Open("static/tw.css") 1152 if err != nil { 1153 log.Printf("Error opening CSS file: %v", err) 1154 return "" 1155 } 1156 defer cssFile.Close() 1157 1158 hasher := sha256.New() 1159 if _, err := io.Copy(hasher, cssFile); err != nil { 1160 log.Printf("Error hashing CSS file: %v", err) 1161 return "" 1162 } 1163 1164 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1165} 1166 1167func (p *Pages) Error500(w io.Writer) error { 1168 return p.execute("errors/500", w, nil) 1169} 1170 1171func (p *Pages) Error404(w io.Writer) error { 1172 return p.execute("errors/404", w, nil) 1173} 1174 1175func (p *Pages) Error503(w io.Writer) error { 1176 return p.execute("errors/503", w, nil) 1177}