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