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" 16 "path/filepath" 17 "slices" 18 "strings" 19 20 "tangled.sh/tangled.sh/core/appview/auth" 21 "tangled.sh/tangled.sh/core/appview/db" 22 "tangled.sh/tangled.sh/core/appview/pages/markup" 23 "tangled.sh/tangled.sh/core/appview/pagination" 24 "tangled.sh/tangled.sh/core/appview/state/userutil" 25 "tangled.sh/tangled.sh/core/patchutil" 26 "tangled.sh/tangled.sh/core/types" 27 28 "github.com/alecthomas/chroma/v2" 29 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 30 "github.com/alecthomas/chroma/v2/lexers" 31 "github.com/alecthomas/chroma/v2/styles" 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 "github.com/go-git/go-git/v5/plumbing/object" 34 "github.com/microcosm-cc/bluemonday" 35) 36 37//go:embed templates/* static 38var Files embed.FS 39 40type Pages struct { 41 t map[string]*template.Template 42 dev bool 43 embedFS embed.FS 44 templateDir string // Path to templates on disk for dev mode 45} 46 47func NewPages(dev bool) *Pages { 48 p := &Pages{ 49 t: make(map[string]*template.Template), 50 dev: dev, 51 embedFS: Files, 52 templateDir: "appview/pages", 53 } 54 55 // Initial load of all templates 56 p.loadAllTemplates() 57 58 return p 59} 60 61func (p *Pages) loadAllTemplates() { 62 templates := make(map[string]*template.Template) 63 var fragmentPaths []string 64 65 // Use embedded FS for initial loading 66 // First, collect all fragment paths 67 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 68 if err != nil { 69 return err 70 } 71 if d.IsDir() { 72 return nil 73 } 74 if !strings.HasSuffix(path, ".html") { 75 return nil 76 } 77 if !strings.Contains(path, "fragments/") { 78 return nil 79 } 80 name := strings.TrimPrefix(path, "templates/") 81 name = strings.TrimSuffix(name, ".html") 82 tmpl, err := template.New(name). 83 Funcs(funcMap()). 84 ParseFS(p.embedFS, path) 85 if err != nil { 86 log.Fatalf("setting up fragment: %v", err) 87 } 88 templates[name] = tmpl 89 fragmentPaths = append(fragmentPaths, path) 90 log.Printf("loaded fragment: %s", name) 91 return nil 92 }) 93 if err != nil { 94 log.Fatalf("walking template dir for fragments: %v", err) 95 } 96 97 // Then walk through and setup the rest of the templates 98 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 99 if err != nil { 100 return err 101 } 102 if d.IsDir() { 103 return nil 104 } 105 if !strings.HasSuffix(path, "html") { 106 return nil 107 } 108 // Skip fragments as they've already been loaded 109 if strings.Contains(path, "fragments/") { 110 return nil 111 } 112 // Skip layouts 113 if strings.Contains(path, "layouts/") { 114 return nil 115 } 116 name := strings.TrimPrefix(path, "templates/") 117 name = strings.TrimSuffix(name, ".html") 118 // Add the page template on top of the base 119 allPaths := []string{} 120 allPaths = append(allPaths, "templates/layouts/*.html") 121 allPaths = append(allPaths, fragmentPaths...) 122 allPaths = append(allPaths, path) 123 tmpl, err := template.New(name). 124 Funcs(funcMap()). 125 ParseFS(p.embedFS, allPaths...) 126 if err != nil { 127 return fmt.Errorf("setting up template: %w", err) 128 } 129 templates[name] = tmpl 130 log.Printf("loaded template: %s", name) 131 return nil 132 }) 133 if err != nil { 134 log.Fatalf("walking template dir: %v", err) 135 } 136 137 log.Printf("total templates loaded: %d", len(templates)) 138 p.t = templates 139} 140 141// loadTemplateFromDisk loads a template from the filesystem in dev mode 142func (p *Pages) loadTemplateFromDisk(name string) error { 143 if !p.dev { 144 return nil 145 } 146 147 log.Printf("reloading template from disk: %s", name) 148 149 // Find all fragments first 150 var fragmentPaths []string 151 err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 152 if err != nil { 153 return err 154 } 155 if d.IsDir() { 156 return nil 157 } 158 if !strings.HasSuffix(path, ".html") { 159 return nil 160 } 161 if !strings.Contains(path, "fragments/") { 162 return nil 163 } 164 fragmentPaths = append(fragmentPaths, path) 165 return nil 166 }) 167 if err != nil { 168 return fmt.Errorf("walking disk template dir for fragments: %w", err) 169 } 170 171 // Find the template path on disk 172 templatePath := filepath.Join(p.templateDir, "templates", name+".html") 173 if _, err := os.Stat(templatePath); os.IsNotExist(err) { 174 return fmt.Errorf("template not found on disk: %s", name) 175 } 176 177 // Create a new template 178 tmpl := template.New(name).Funcs(funcMap()) 179 180 // Parse layouts 181 layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 182 layouts, err := filepath.Glob(layoutGlob) 183 if err != nil { 184 return fmt.Errorf("finding layout templates: %w", err) 185 } 186 187 // Create paths for parsing 188 allFiles := append(layouts, fragmentPaths...) 189 allFiles = append(allFiles, templatePath) 190 191 // Parse all templates 192 tmpl, err = tmpl.ParseFiles(allFiles...) 193 if err != nil { 194 return fmt.Errorf("parsing template files: %w", err) 195 } 196 197 // Update the template in the map 198 p.t[name] = tmpl 199 log.Printf("template reloaded from disk: %s", name) 200 return nil 201} 202 203func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 204 // In dev mode, reload the template from disk before executing 205 if p.dev { 206 if err := p.loadTemplateFromDisk(templateName); err != nil { 207 log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 208 // Continue with the existing template 209 } 210 } 211 212 tmpl, exists := p.t[templateName] 213 if !exists { 214 return fmt.Errorf("template not found: %s", templateName) 215 } 216 217 if base == "" { 218 return tmpl.Execute(w, params) 219 } else { 220 return tmpl.ExecuteTemplate(w, base, params) 221 } 222} 223 224func (p *Pages) execute(name string, w io.Writer, params any) error { 225 return p.executeOrReload(name, w, "layouts/base", params) 226} 227 228func (p *Pages) executePlain(name string, w io.Writer, params any) error { 229 return p.executeOrReload(name, w, "", params) 230} 231 232func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 233 return p.executeOrReload(name, w, "layouts/repobase", params) 234} 235 236type LoginParams struct { 237} 238 239func (p *Pages) Login(w io.Writer, params LoginParams) error { 240 return p.executePlain("user/login", w, params) 241} 242 243type TimelineParams struct { 244 LoggedInUser *auth.User 245 Timeline []db.TimelineEvent 246 DidHandleMap map[string]string 247} 248 249func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 250 return p.execute("timeline", w, params) 251} 252 253type SettingsParams struct { 254 LoggedInUser *auth.User 255 PubKeys []db.PublicKey 256 Emails []db.Email 257} 258 259func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 260 return p.execute("settings", w, params) 261} 262 263type KnotsParams struct { 264 LoggedInUser *auth.User 265 Registrations []db.Registration 266} 267 268func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 269 return p.execute("knots", w, params) 270} 271 272type KnotParams struct { 273 LoggedInUser *auth.User 274 DidHandleMap map[string]string 275 Registration *db.Registration 276 Members []string 277 IsOwner bool 278} 279 280func (p *Pages) Knot(w io.Writer, params KnotParams) error { 281 return p.execute("knot", w, params) 282} 283 284type NewRepoParams struct { 285 LoggedInUser *auth.User 286 Knots []string 287} 288 289func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 290 return p.execute("repo/new", w, params) 291} 292 293type ForkRepoParams struct { 294 LoggedInUser *auth.User 295 Knots []string 296 RepoInfo RepoInfo 297} 298 299func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 300 return p.execute("repo/fork", w, params) 301} 302 303type ProfilePageParams struct { 304 LoggedInUser *auth.User 305 UserDid string 306 UserHandle string 307 Repos []db.Repo 308 CollaboratingRepos []db.Repo 309 ProfileStats ProfileStats 310 FollowStatus db.FollowStatus 311 AvatarUri string 312 ProfileTimeline *db.ProfileTimeline 313 314 DidHandleMap map[string]string 315} 316 317type ProfileStats struct { 318 Followers int 319 Following int 320} 321 322func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 323 return p.execute("user/profile", w, params) 324} 325 326type FollowFragmentParams struct { 327 UserDid string 328 FollowStatus db.FollowStatus 329} 330 331func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 332 return p.executePlain("user/fragments/follow", w, params) 333} 334 335type RepoActionsFragmentParams struct { 336 IsStarred bool 337 RepoAt syntax.ATURI 338 Stats db.RepoStats 339} 340 341func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 342 return p.executePlain("repo/fragments/repoActions", w, params) 343} 344 345type RepoDescriptionParams struct { 346 RepoInfo RepoInfo 347} 348 349func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 350 return p.executePlain("repo/fragments/editRepoDescription", w, params) 351} 352 353func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 354 return p.executePlain("repo/fragments/repoDescription", w, params) 355} 356 357type RepoInfo struct { 358 Name string 359 OwnerDid string 360 OwnerHandle string 361 Description string 362 Knot string 363 RepoAt syntax.ATURI 364 IsStarred bool 365 Stats db.RepoStats 366 Roles RolesInRepo 367 Source *db.Repo 368 SourceHandle string 369 Ref string 370 DisableFork bool 371} 372 373type RolesInRepo struct { 374 Roles []string 375} 376 377func (r RolesInRepo) SettingsAllowed() bool { 378 return slices.Contains(r.Roles, "repo:settings") 379} 380 381func (r RolesInRepo) CollaboratorInviteAllowed() bool { 382 return slices.Contains(r.Roles, "repo:invite") 383} 384 385func (r RolesInRepo) RepoDeleteAllowed() bool { 386 return slices.Contains(r.Roles, "repo:delete") 387} 388 389func (r RolesInRepo) IsOwner() bool { 390 return slices.Contains(r.Roles, "repo:owner") 391} 392 393func (r RolesInRepo) IsCollaborator() bool { 394 return slices.Contains(r.Roles, "repo:collaborator") 395} 396 397func (r RolesInRepo) IsPushAllowed() bool { 398 return slices.Contains(r.Roles, "repo:push") 399} 400 401func (r RepoInfo) OwnerWithAt() string { 402 if r.OwnerHandle != "" { 403 return fmt.Sprintf("@%s", r.OwnerHandle) 404 } else { 405 return r.OwnerDid 406 } 407} 408 409func (r RepoInfo) FullName() string { 410 return path.Join(r.OwnerWithAt(), r.Name) 411} 412 413func (r RepoInfo) OwnerWithoutAt() string { 414 if strings.HasPrefix(r.OwnerWithAt(), "@") { 415 return strings.TrimPrefix(r.OwnerWithAt(), "@") 416 } else { 417 return userutil.FlattenDid(r.OwnerDid) 418 } 419} 420 421func (r RepoInfo) FullNameWithoutAt() string { 422 return path.Join(r.OwnerWithoutAt(), r.Name) 423} 424 425func (r RepoInfo) GetTabs() [][]string { 426 tabs := [][]string{ 427 {"overview", "/", "square-chart-gantt"}, 428 {"issues", "/issues", "circle-dot"}, 429 {"pulls", "/pulls", "git-pull-request"}, 430 } 431 432 if r.Roles.SettingsAllowed() { 433 tabs = append(tabs, []string{"settings", "/settings", "cog"}) 434 } 435 436 return tabs 437} 438 439// each tab on a repo could have some metadata: 440// 441// issues -> number of open issues etc. 442// settings -> a warning icon to setup branch protection? idk 443// 444// we gather these bits of info here, because go templates 445// are difficult to program in 446func (r RepoInfo) TabMetadata() map[string]any { 447 meta := make(map[string]any) 448 449 if r.Stats.PullCount.Open > 0 { 450 meta["pulls"] = r.Stats.PullCount.Open 451 } 452 453 if r.Stats.IssueCount.Open > 0 { 454 meta["issues"] = r.Stats.IssueCount.Open 455 } 456 457 // more stuff? 458 459 return meta 460} 461 462type RepoIndexParams struct { 463 LoggedInUser *auth.User 464 RepoInfo RepoInfo 465 Active string 466 TagMap map[string][]string 467 CommitsTrunc []*object.Commit 468 TagsTrunc []*types.TagReference 469 BranchesTrunc []types.Branch 470 types.RepoIndexResponse 471 HTMLReadme template.HTML 472 Raw bool 473 EmailToDidOrHandle map[string]string 474} 475 476func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 477 params.Active = "overview" 478 if params.IsEmpty { 479 return p.executeRepo("repo/empty", w, params) 480 } 481 482 rctx := markup.RenderContext{ 483 Ref: params.RepoInfo.Ref, 484 FullRepoName: params.RepoInfo.FullName(), 485 } 486 487 if params.ReadmeFileName != "" { 488 var htmlString string 489 ext := filepath.Ext(params.ReadmeFileName) 490 switch ext { 491 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 492 htmlString = rctx.RenderMarkdown(params.Readme) 493 params.Raw = false 494 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 495 default: 496 htmlString = string(params.Readme) 497 params.Raw = true 498 params.HTMLReadme = template.HTML(bluemonday.NewPolicy().Sanitize(htmlString)) 499 } 500 } 501 502 return p.executeRepo("repo/index", w, params) 503} 504 505type RepoLogParams struct { 506 LoggedInUser *auth.User 507 RepoInfo RepoInfo 508 TagMap map[string][]string 509 types.RepoLogResponse 510 Active string 511 EmailToDidOrHandle map[string]string 512} 513 514func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 515 params.Active = "overview" 516 return p.executeRepo("repo/log", w, params) 517} 518 519type RepoCommitParams struct { 520 LoggedInUser *auth.User 521 RepoInfo RepoInfo 522 Active string 523 EmailToDidOrHandle map[string]string 524 525 types.RepoCommitResponse 526} 527 528func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 529 params.Active = "overview" 530 return p.executeRepo("repo/commit", w, params) 531} 532 533type RepoTreeParams struct { 534 LoggedInUser *auth.User 535 RepoInfo RepoInfo 536 Active string 537 BreadCrumbs [][]string 538 BaseTreeLink string 539 BaseBlobLink string 540 types.RepoTreeResponse 541} 542 543type RepoTreeStats struct { 544 NumFolders uint64 545 NumFiles uint64 546} 547 548func (r RepoTreeParams) TreeStats() RepoTreeStats { 549 numFolders, numFiles := 0, 0 550 for _, f := range r.Files { 551 if !f.IsFile { 552 numFolders += 1 553 } else if f.IsFile { 554 numFiles += 1 555 } 556 } 557 558 return RepoTreeStats{ 559 NumFolders: uint64(numFolders), 560 NumFiles: uint64(numFiles), 561 } 562} 563 564func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 565 params.Active = "overview" 566 return p.execute("repo/tree", w, params) 567} 568 569type RepoBranchesParams struct { 570 LoggedInUser *auth.User 571 RepoInfo RepoInfo 572 Active string 573 types.RepoBranchesResponse 574} 575 576func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 577 params.Active = "overview" 578 return p.executeRepo("repo/branches", w, params) 579} 580 581type RepoTagsParams struct { 582 LoggedInUser *auth.User 583 RepoInfo RepoInfo 584 Active string 585 types.RepoTagsResponse 586} 587 588func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 589 params.Active = "overview" 590 return p.executeRepo("repo/tags", w, params) 591} 592 593type RepoBlobParams struct { 594 LoggedInUser *auth.User 595 RepoInfo RepoInfo 596 Active string 597 BreadCrumbs [][]string 598 ShowRendered bool 599 RenderToggle bool 600 RenderedContents template.HTML 601 types.RepoBlobResponse 602} 603 604func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 605 var style *chroma.Style = styles.Get("catpuccin-latte") 606 607 if params.ShowRendered { 608 switch markup.GetFormat(params.Path) { 609 case markup.FormatMarkdown: 610 rctx := markup.RenderContext{ 611 Ref: params.RepoInfo.Ref, 612 FullRepoName: params.RepoInfo.FullName(), 613 RendererType: markup.RendererTypeRepoMarkdown, 614 } 615 params.RenderedContents = template.HTML(rctx.RenderMarkdown(params.Contents)) 616 } 617 } 618 619 if params.Lines < 5000 { 620 c := params.Contents 621 formatter := chromahtml.New( 622 chromahtml.InlineCode(false), 623 chromahtml.WithLineNumbers(true), 624 chromahtml.WithLinkableLineNumbers(true, "L"), 625 chromahtml.Standalone(false), 626 chromahtml.WithClasses(true), 627 ) 628 629 lexer := lexers.Get(filepath.Base(params.Path)) 630 if lexer == nil { 631 lexer = lexers.Fallback 632 } 633 634 iterator, err := lexer.Tokenise(nil, c) 635 if err != nil { 636 return fmt.Errorf("chroma tokenize: %w", err) 637 } 638 639 var code bytes.Buffer 640 err = formatter.Format(&code, style, iterator) 641 if err != nil { 642 return fmt.Errorf("chroma format: %w", err) 643 } 644 645 params.Contents = code.String() 646 } 647 648 params.Active = "overview" 649 return p.executeRepo("repo/blob", w, params) 650} 651 652type Collaborator struct { 653 Did string 654 Handle string 655 Role string 656} 657 658type RepoSettingsParams struct { 659 LoggedInUser *auth.User 660 RepoInfo RepoInfo 661 Collaborators []Collaborator 662 Active string 663 Branches []string 664 DefaultBranch string 665 // TODO: use repoinfo.roles 666 IsCollaboratorInviteAllowed bool 667} 668 669func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 670 params.Active = "settings" 671 return p.executeRepo("repo/settings", w, params) 672} 673 674type RepoIssuesParams struct { 675 LoggedInUser *auth.User 676 RepoInfo RepoInfo 677 Active string 678 Issues []db.Issue 679 DidHandleMap map[string]string 680 Page pagination.Page 681 FilteringByOpen bool 682} 683 684func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 685 params.Active = "issues" 686 return p.executeRepo("repo/issues/issues", w, params) 687} 688 689type RepoSingleIssueParams struct { 690 LoggedInUser *auth.User 691 RepoInfo RepoInfo 692 Active string 693 Issue db.Issue 694 Comments []db.Comment 695 IssueOwnerHandle string 696 DidHandleMap map[string]string 697 698 State string 699} 700 701func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 702 params.Active = "issues" 703 if params.Issue.Open { 704 params.State = "open" 705 } else { 706 params.State = "closed" 707 } 708 return p.execute("repo/issues/issue", w, params) 709} 710 711type RepoNewIssueParams struct { 712 LoggedInUser *auth.User 713 RepoInfo RepoInfo 714 Active string 715} 716 717func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 718 params.Active = "issues" 719 return p.executeRepo("repo/issues/new", w, params) 720} 721 722type EditIssueCommentParams struct { 723 LoggedInUser *auth.User 724 RepoInfo RepoInfo 725 Issue *db.Issue 726 Comment *db.Comment 727} 728 729func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 730 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 731} 732 733type SingleIssueCommentParams struct { 734 LoggedInUser *auth.User 735 DidHandleMap map[string]string 736 RepoInfo RepoInfo 737 Issue *db.Issue 738 Comment *db.Comment 739} 740 741func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 742 return p.executePlain("repo/issues/fragments/issueComment", w, params) 743} 744 745type RepoNewPullParams struct { 746 LoggedInUser *auth.User 747 RepoInfo RepoInfo 748 Branches []types.Branch 749 Active string 750} 751 752func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 753 params.Active = "pulls" 754 return p.executeRepo("repo/pulls/new", w, params) 755} 756 757type RepoPullsParams struct { 758 LoggedInUser *auth.User 759 RepoInfo RepoInfo 760 Pulls []*db.Pull 761 Active string 762 DidHandleMap map[string]string 763 FilteringBy db.PullState 764} 765 766func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 767 params.Active = "pulls" 768 return p.executeRepo("repo/pulls/pulls", w, params) 769} 770 771type ResubmitResult uint64 772 773const ( 774 ShouldResubmit ResubmitResult = iota 775 ShouldNotResubmit 776 Unknown 777) 778 779func (r ResubmitResult) Yes() bool { 780 return r == ShouldResubmit 781} 782func (r ResubmitResult) No() bool { 783 return r == ShouldNotResubmit 784} 785func (r ResubmitResult) Unknown() bool { 786 return r == Unknown 787} 788 789type RepoSinglePullParams struct { 790 LoggedInUser *auth.User 791 RepoInfo RepoInfo 792 Active string 793 DidHandleMap map[string]string 794 Pull *db.Pull 795 MergeCheck types.MergeCheckResponse 796 ResubmitCheck ResubmitResult 797} 798 799func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 800 params.Active = "pulls" 801 return p.executeRepo("repo/pulls/pull", w, params) 802} 803 804type RepoPullPatchParams struct { 805 LoggedInUser *auth.User 806 DidHandleMap map[string]string 807 RepoInfo RepoInfo 808 Pull *db.Pull 809 Diff *types.NiceDiff 810 Round int 811 Submission *db.PullSubmission 812} 813 814// this name is a mouthful 815func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 816 return p.execute("repo/pulls/patch", w, params) 817} 818 819type RepoPullInterdiffParams struct { 820 LoggedInUser *auth.User 821 DidHandleMap map[string]string 822 RepoInfo RepoInfo 823 Pull *db.Pull 824 Round int 825 Interdiff *patchutil.InterdiffResult 826} 827 828// this name is a mouthful 829func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 830 return p.execute("repo/pulls/interdiff", w, params) 831} 832 833type PullPatchUploadParams struct { 834 RepoInfo RepoInfo 835} 836 837func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 838 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 839} 840 841type PullCompareBranchesParams struct { 842 RepoInfo RepoInfo 843 Branches []types.Branch 844} 845 846func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 847 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 848} 849 850type PullCompareForkParams struct { 851 RepoInfo RepoInfo 852 Forks []db.Repo 853} 854 855func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 856 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 857} 858 859type PullCompareForkBranchesParams struct { 860 RepoInfo RepoInfo 861 SourceBranches []types.Branch 862 TargetBranches []types.Branch 863} 864 865func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 866 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 867} 868 869type PullResubmitParams struct { 870 LoggedInUser *auth.User 871 RepoInfo RepoInfo 872 Pull *db.Pull 873 SubmissionId int 874} 875 876func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 877 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 878} 879 880type PullActionsParams struct { 881 LoggedInUser *auth.User 882 RepoInfo RepoInfo 883 Pull *db.Pull 884 RoundNumber int 885 MergeCheck types.MergeCheckResponse 886 ResubmitCheck ResubmitResult 887} 888 889func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 890 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 891} 892 893type PullNewCommentParams struct { 894 LoggedInUser *auth.User 895 RepoInfo RepoInfo 896 Pull *db.Pull 897 RoundNumber int 898} 899 900func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 901 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 902} 903 904func (p *Pages) Static() http.Handler { 905 if p.dev { 906 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 907 } 908 909 sub, err := fs.Sub(Files, "static") 910 if err != nil { 911 log.Fatalf("no static dir found? that's crazy: %v", err) 912 } 913 // Custom handler to apply Cache-Control headers for font files 914 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 915} 916 917func Cache(h http.Handler) http.Handler { 918 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 919 path := strings.Split(r.URL.Path, "?")[0] 920 921 if strings.HasSuffix(path, ".css") { 922 // on day for css files 923 w.Header().Set("Cache-Control", "public, max-age=86400") 924 } else { 925 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 926 } 927 h.ServeHTTP(w, r) 928 }) 929} 930 931func CssContentHash() string { 932 cssFile, err := Files.Open("static/tw.css") 933 if err != nil { 934 log.Printf("Error opening CSS file: %v", err) 935 return "" 936 } 937 defer cssFile.Close() 938 939 hasher := sha256.New() 940 if _, err := io.Copy(hasher, cssFile); err != nil { 941 log.Printf("Error hashing CSS file: %v", err) 942 return "" 943 } 944 945 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 946} 947 948func (p *Pages) Error500(w io.Writer) error { 949 return p.execute("errors/500", w, nil) 950} 951 952func (p *Pages) Error404(w io.Writer) error { 953 return p.execute("errors/404", w, nil) 954} 955 956func (p *Pages) Error503(w io.Writer) error { 957 return p.execute("errors/503", w, nil) 958}