forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+13 -3
appview/db/pulls.go
··· 122 return len(p.Submissions) - 1 123 } 124 125 - func (p *Pull) IsSameRepoBranch() bool { 126 if p.PullSource != nil { 127 if p.PullSource.RepoAt != nil { 128 return p.PullSource.RepoAt == &p.RepoAt ··· 134 return false 135 } 136 137 - func (p *Pull) IsPatch() bool { 138 - return p.PullSource == nil 139 } 140 141 func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
··· 122 return len(p.Submissions) - 1 123 } 124 125 + func (p *Pull) IsPatchBased() bool { 126 + return p.PullSource == nil 127 + } 128 + 129 + func (p *Pull) IsBranchBased() bool { 130 if p.PullSource != nil { 131 if p.PullSource.RepoAt != nil { 132 return p.PullSource.RepoAt == &p.RepoAt ··· 138 return false 139 } 140 141 + func (p *Pull) IsForkBased() bool { 142 + if p.PullSource != nil { 143 + if p.PullSource.RepoAt != nil { 144 + // make sure repos are different 145 + return p.PullSource.RepoAt != &p.RepoAt 146 + } 147 + } 148 + return false 149 } 150 151 func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
+4
appview/pages/funcmap.go
··· 33 "add": func(a, b int) int { 34 return a + b 35 }, 36 "sub": func(a, b int) int { 37 return a - b 38 },
··· 33 "add": func(a, b int) int { 34 return a + b 35 }, 36 + // the absolute state of go templates 37 + "add64": func(a, b int64) int64 { 38 + return a + b 39 + }, 40 "sub": func(a, b int) int { 41 return a - b 42 },
+80 -41
appview/pages/pages.go
··· 39 func NewPages() *Pages { 40 templates := make(map[string]*template.Template) 41 42 - // Walk through embedded templates directory and parse all .html files 43 err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 44 if err != nil { 45 return err 46 } 47 48 - if !d.IsDir() && strings.HasSuffix(path, ".html") { 49 - name := strings.TrimPrefix(path, "templates/") 50 - name = strings.TrimSuffix(name, ".html") 51 52 - // add fragments as templates 53 - if strings.HasPrefix(path, "templates/fragments/") { 54 - tmpl, err := template.New(name). 55 - Funcs(funcMap()). 56 - ParseFS(Files, path) 57 - if err != nil { 58 - return fmt.Errorf("setting up fragment: %w", err) 59 - } 60 61 - templates[name] = tmpl 62 - log.Printf("loaded fragment: %s", name) 63 - } 64 65 - // layouts and fragments are applied first 66 - if !strings.HasPrefix(path, "templates/layouts/") && 67 - !strings.HasPrefix(path, "templates/fragments/") { 68 - // Add the page template on top of the base 69 - tmpl, err := template.New(name). 70 - Funcs(funcMap()). 71 - ParseFS(Files, "templates/layouts/*.html", "templates/fragments/*.html", path) 72 - if err != nil { 73 - return fmt.Errorf("setting up template: %w", err) 74 - } 75 76 - templates[name] = tmpl 77 - log.Printf("loaded template: %s", name) 78 - } 79 80 return nil 81 } 82 return nil 83 }) 84 if err != nil { ··· 200 } 201 202 func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 203 - return p.executePlain("fragments/follow", w, params) 204 } 205 206 type RepoActionsFragmentParams struct { ··· 210 } 211 212 func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 213 - return p.executePlain("fragments/repoActions", w, params) 214 } 215 216 type RepoDescriptionParams struct { ··· 218 } 219 220 func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 221 - return p.executePlain("fragments/editRepoDescription", w, params) 222 } 223 224 func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 225 - return p.executePlain("fragments/repoDescription", w, params) 226 } 227 228 type RepoInfo struct { ··· 580 } 581 582 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 583 - return p.executePlain("fragments/editIssueComment", w, params) 584 } 585 586 type SingleIssueCommentParams struct { ··· 592 } 593 594 func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 595 - return p.executePlain("fragments/issueComment", w, params) 596 } 597 598 type RepoNewPullParams struct { ··· 675 } 676 677 func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 678 - return p.executePlain("fragments/pullPatchUpload", w, params) 679 } 680 681 type PullCompareBranchesParams struct { ··· 684 } 685 686 func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 687 - return p.executePlain("fragments/pullCompareBranches", w, params) 688 } 689 690 type PullCompareForkParams struct { ··· 693 } 694 695 func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 696 - return p.executePlain("fragments/pullCompareForks", w, params) 697 } 698 699 type PullCompareForkBranchesParams struct { ··· 703 } 704 705 func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 706 - return p.executePlain("fragments/pullCompareForksBranches", w, params) 707 } 708 709 type PullResubmitParams struct { ··· 714 } 715 716 func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 717 - return p.executePlain("fragments/pullResubmit", w, params) 718 } 719 720 type PullActionsParams struct { ··· 727 } 728 729 func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 730 - return p.executePlain("fragments/pullActions", w, params) 731 } 732 733 type PullNewCommentParams struct { ··· 738 } 739 740 func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 741 - return p.executePlain("fragments/pullNewComment", w, params) 742 } 743 744 func (p *Pages) Static() http.Handler {
··· 39 func NewPages() *Pages { 40 templates := make(map[string]*template.Template) 41 42 + var fragmentPaths []string 43 + // First, collect all fragment paths 44 err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 45 if err != nil { 46 return err 47 } 48 49 + if d.IsDir() { 50 + return nil 51 + } 52 + 53 + if !strings.HasSuffix(path, ".html") { 54 + return nil 55 + } 56 + 57 + if !strings.Contains(path, "fragments/") { 58 + return nil 59 + } 60 + 61 + name := strings.TrimPrefix(path, "templates/") 62 + name = strings.TrimSuffix(name, ".html") 63 + 64 + tmpl, err := template.New(name). 65 + Funcs(funcMap()). 66 + ParseFS(Files, path) 67 + if err != nil { 68 + log.Fatalf("setting up fragment: %v", err) 69 + } 70 + 71 + templates[name] = tmpl 72 + fragmentPaths = append(fragmentPaths, path) 73 + log.Printf("loaded fragment: %s", name) 74 + return nil 75 + }) 76 + if err != nil { 77 + log.Fatalf("walking template dir for fragments: %v", err) 78 + } 79 80 + // Then walk through and setup the rest of the templates 81 + err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 82 + if err != nil { 83 + return err 84 + } 85 86 + if d.IsDir() { 87 + return nil 88 + } 89 90 + if !strings.HasSuffix(path, "html") { 91 + return nil 92 + } 93 94 + // Skip fragments as they've already been loaded 95 + if strings.Contains(path, "fragments/") { 96 + return nil 97 + } 98 99 + // Skip layouts 100 + if strings.Contains(path, "layouts/") { 101 return nil 102 } 103 + 104 + name := strings.TrimPrefix(path, "templates/") 105 + name = strings.TrimSuffix(name, ".html") 106 + 107 + // Add the page template on top of the base 108 + allPaths := []string{} 109 + allPaths = append(allPaths, "templates/layouts/*.html") 110 + allPaths = append(allPaths, fragmentPaths...) 111 + allPaths = append(allPaths, path) 112 + tmpl, err := template.New(name). 113 + Funcs(funcMap()). 114 + ParseFS(Files, allPaths...) 115 + if err != nil { 116 + return fmt.Errorf("setting up template: %w", err) 117 + } 118 + 119 + templates[name] = tmpl 120 + log.Printf("loaded template: %s", name) 121 return nil 122 }) 123 if err != nil { ··· 239 } 240 241 func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 242 + return p.executePlain("user/fragments/follow", w, params) 243 } 244 245 type RepoActionsFragmentParams struct { ··· 249 } 250 251 func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 252 + return p.executePlain("repo/fragments/repoActions", w, params) 253 } 254 255 type RepoDescriptionParams struct { ··· 257 } 258 259 func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 260 + return p.executePlain("repo/fragments/editRepoDescription", w, params) 261 } 262 263 func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 264 + return p.executePlain("repo/fragments/repoDescription", w, params) 265 } 266 267 type RepoInfo struct { ··· 619 } 620 621 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 622 + return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 623 } 624 625 type SingleIssueCommentParams struct { ··· 631 } 632 633 func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 634 + return p.executePlain("repo/issues/fragments/issueComment", w, params) 635 } 636 637 type RepoNewPullParams struct { ··· 714 } 715 716 func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 717 + return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 718 } 719 720 type PullCompareBranchesParams struct { ··· 723 } 724 725 func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 726 + return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 727 } 728 729 type PullCompareForkParams struct { ··· 732 } 733 734 func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 735 + return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 736 } 737 738 type PullCompareForkBranchesParams struct { ··· 742 } 743 744 func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 745 + return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 746 } 747 748 type PullResubmitParams struct { ··· 753 } 754 755 func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 756 + return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 757 } 758 759 type PullActionsParams struct { ··· 766 } 767 768 func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 769 + return p.executePlain("repo/pulls/fragments/pullActions", w, params) 770 } 771 772 type PullNewCommentParams struct { ··· 777 } 778 779 func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 780 + return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 781 } 782 783 func (p *Pages) Static() http.Handler {
-33
appview/pages/templates/fragments/cloneInstructions.html
··· 1 - {{ define "fragments/cloneInstructions" }} 2 - <section class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4"> 3 - <div class="flex flex-col gap-2"> 4 - <strong>push</strong> 5 - <div class="md:pl-4 overflow-x-auto whitespace-nowrap"> 6 - <code class="dark:text-gray-100">git remote add origin git@{{.RepoInfo.Knot}}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code> 7 - </div> 8 - </div> 9 - 10 - <div class="flex flex-col gap-2"> 11 - <strong>clone</strong> 12 - <div class="md:pl-4 flex flex-col gap-2"> 13 - 14 - <div class="flex items-center gap-3"> 15 - <span class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white">HTTP</span> 16 - <div class="overflow-x-auto whitespace-nowrap flex-1"> 17 - <code class="dark:text-gray-100">git clone https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code> 18 - </div> 19 - </div> 20 - 21 - <div class="flex items-center gap-3"> 22 - <span class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white">SSH</span> 23 - <div class="overflow-x-auto whitespace-nowrap flex-1"> 24 - <code class="dark:text-gray-100">git clone git@{{.RepoInfo.Knot}}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code> 25 - </div> 26 - </div> 27 - </div> 28 - </div> 29 - 30 - 31 - <p class="py-2 text-gray-500 dark:text-gray-400">Note that for self-hosted knots, clone URLs may be different based on your setup.</p> 32 - </section> 33 - {{ end }}
···
-116
appview/pages/templates/fragments/diff.html
··· 1 - {{ define "fragments/diff" }} 2 - {{ $repo := index . 0 }} 3 - {{ $diff := index . 1 }} 4 - {{ $commit := $diff.Commit }} 5 - {{ $stat := $diff.Stat }} 6 - {{ $diff := $diff.Diff }} 7 - 8 - {{ $this := $commit.This }} 9 - {{ $parent := $commit.Parent }} 10 - 11 - {{ $last := sub (len $diff) 1 }} 12 - {{ range $idx, $hunk := $diff }} 13 - {{ with $hunk }} 14 - <section class="mt-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 15 - <div id="file-{{ .Name.New }}"> 16 - <div id="diff-file"> 17 - <details open> 18 - <summary class="list-none cursor-pointer sticky top-0"> 19 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 20 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto" style="direction: rtl;"> 21 - {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 22 - 23 - <div class="flex gap-2 items-center" style="direction: ltr;"> 24 - {{ if .IsNew }} 25 - <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span> 26 - {{ else if .IsDelete }} 27 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">DELETED</span> 28 - {{ else if .IsCopy }} 29 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">COPIED</span> 30 - {{ else if .IsRename }} 31 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">RENAMED</span> 32 - {{ else }} 33 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">MODIFIED</span> 34 - {{ end }} 35 - 36 - {{ if .IsDelete }} 37 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}> 38 - {{ .Name.Old }} 39 - </a> 40 - {{ else if (or .IsCopy .IsRename) }} 41 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $parent}}href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}"{{end}}> 42 - {{ .Name.Old }} 43 - </a> 44 - {{ i "arrow-right" "w-4 h-4" }} 45 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 46 - {{ .Name.New }} 47 - </a> 48 - {{ else }} 49 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 50 - {{ .Name.New }} 51 - </a> 52 - {{ end }} 53 - </div> 54 - </div> 55 - 56 - {{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }} 57 - <div id="right-side-items" class="p-2 flex items-center"> 58 - <a title="top of file" href="#file-{{ .Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a> 59 - {{ if gt $idx 0 }} 60 - {{ $prev := index $diff (sub $idx 1) }} 61 - <a title="previous file" href="#file-{{ $prev.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a> 62 - {{ end }} 63 - 64 - {{ if lt $idx $last }} 65 - {{ $next := index $diff (add $idx 1) }} 66 - <a title="next file" href="#file-{{ $next.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a> 67 - {{ end }} 68 - </div> 69 - 70 - </div> 71 - </summary> 72 - 73 - <div class="transition-all duration-700 ease-in-out"> 74 - {{ if .IsDelete }} 75 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 76 - This file has been deleted in this commit. 77 - </p> 78 - {{ else }} 79 - {{ if .IsBinary }} 80 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 81 - This is a binary file and will not be displayed. 82 - </p> 83 - {{ else }} 84 - <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{- .Header -}}</div>{{- range .Lines -}} 85 - {{- if eq .Op.String "+" -}} 86 - <div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full"> 87 - <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 88 - <div class="p-1 whitespace-pre">{{ .Line }}</div> 89 - </div> 90 - {{- end -}} 91 - {{- if eq .Op.String "-" -}} 92 - <div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full"> 93 - <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 94 - <div class="p-1 whitespace-pre">{{ .Line }}</div> 95 - </div> 96 - {{- end -}} 97 - {{- if eq .Op.String " " -}} 98 - <div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full"> 99 - <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 100 - <div class="p-1 whitespace-pre">{{ .Line }}</div> 101 - </div> 102 - {{- end -}} 103 - {{- end -}} 104 - {{- end -}}</div></div></pre> 105 - {{- end -}} 106 - {{ end }} 107 - </div> 108 - 109 - </details> 110 - 111 - </div> 112 - </div> 113 - </section> 114 - {{ end }} 115 - {{ end }} 116 - {{ end }}
···
-52
appview/pages/templates/fragments/editIssueComment.html
··· 1 - {{ define "fragments/editIssueComment" }} 2 - {{ with .Comment }} 3 - <div id="comment-container-{{.CommentId}}"> 4 - <div class="flex items-center gap-2 mb-2 text-gray-500 text-sm"> 5 - {{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }} 6 - <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 - 8 - <!-- show user "hats" --> 9 - {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 10 - {{ if $isIssueAuthor }} 11 - <span class="before:content-['ยท']"></span> 12 - <span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center"> 13 - author 14 - </span> 15 - {{ end }} 16 - 17 - <span class="before:content-['ยท']"></span> 18 - <a 19 - href="#{{ .CommentId }}" 20 - class="text-gray-500 hover:text-gray-500 hover:underline no-underline" 21 - id="{{ .CommentId }}"> 22 - {{ .Created | timeFmt }} 23 - </a> 24 - 25 - <button 26 - class="btn px-2 py-1 flex items-center gap-2 text-sm" 27 - hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 28 - hx-include="#edit-textarea-{{ .CommentId }}" 29 - hx-target="#comment-container-{{ .CommentId }}" 30 - hx-swap="outerHTML"> 31 - {{ i "check" "w-4 h-4" }} 32 - </button> 33 - <button 34 - class="btn px-2 py-1 flex items-center gap-2 text-sm" 35 - hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 36 - hx-target="#comment-container-{{ .CommentId }}" 37 - hx-swap="outerHTML"> 38 - {{ i "x" "w-4 h-4" }} 39 - </button> 40 - <span id="comment-{{.CommentId}}-status"></span> 41 - </div> 42 - 43 - <div> 44 - <textarea 45 - id="edit-textarea-{{ .CommentId }}" 46 - name="body" 47 - class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea> 48 - </div> 49 - </div> 50 - {{ end }} 51 - {{ end }} 52 -
···
-11
appview/pages/templates/fragments/editRepoDescription.html
··· 1 - {{ define "fragments/editRepoDescription" }} 2 - <form hx-put="/{{ .RepoInfo.FullName }}/description" hx-target="this" hx-swap="outerHTML" class="flex flex-wrap gap-2"> 3 - <input type="text" class="p-1" name="description" value="{{ .RepoInfo.Description }}"> 4 - <button type="submit" class="btn p-2 flex items-center gap-2 no-underline text-sm"> 5 - {{ i "check" "w-3 h-3" }} save 6 - </button> 7 - <button type="button" class="btn p-2 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description" > 8 - {{ i "x" "w-3 h-3" }} cancel 9 - </button> 10 - </form> 11 - {{ end }}
···
-17
appview/pages/templates/fragments/follow.html
··· 1 - {{ define "fragments/follow" }} 2 - <button id="followBtn" 3 - class="btn mt-2 w-full" 4 - 5 - {{ if eq .FollowStatus.String "IsNotFollowing" }} 6 - hx-post="/follow?subject={{.UserDid}}" 7 - {{ else }} 8 - hx-delete="/follow?subject={{.UserDid}}" 9 - {{ end }} 10 - 11 - hx-trigger="click" 12 - hx-target="#followBtn" 13 - hx-swap="outerHTML" 14 - > 15 - {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }} 16 - </button> 17 - {{ end }}
···
-60
appview/pages/templates/fragments/issueComment.html
··· 1 - {{ define "fragments/issueComment" }} 2 - {{ with .Comment }} 3 - <div id="comment-container-{{.CommentId}}"> 4 - <div class="flex items-center gap-2 mb-2 text-gray-500 text-sm"> 5 - {{ $owner := index $.DidHandleMap .OwnerDid }} 6 - <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 - 8 - <!-- show user "hats" --> 9 - {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 10 - {{ if $isIssueAuthor }} 11 - <span class="before:content-['ยท']"></span> 12 - <span class="rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 13 - author 14 - </span> 15 - {{ end }} 16 - 17 - <span class="before:content-['ยท']"></span> 18 - <a 19 - href="#{{ .CommentId }}" 20 - class="text-gray-500 hover:text-gray-500 hover:underline no-underline" 21 - id="{{ .CommentId }}"> 22 - {{ if .Deleted }} 23 - deleted {{ .Deleted | timeFmt }} 24 - {{ else if .Edited }} 25 - edited {{ .Edited | timeFmt }} 26 - {{ else }} 27 - {{ .Created | timeFmt }} 28 - {{ end }} 29 - </a> 30 - 31 - {{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }} 32 - {{ if and $isCommentOwner (not .Deleted) }} 33 - <button 34 - class="btn px-2 py-1 text-sm" 35 - hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 36 - hx-swap="outerHTML" 37 - hx-target="#comment-container-{{.CommentId}}" 38 - > 39 - {{ i "pencil" "w-4 h-4" }} 40 - </button> 41 - <button 42 - class="btn px-2 py-1 text-sm text-red-500" 43 - hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 44 - hx-confirm="Are you sure you want to delete your comment?" 45 - hx-swap="outerHTML" 46 - hx-target="#comment-container-{{.CommentId}}" 47 - > 48 - {{ i "trash-2" "w-4 h-4" }} 49 - </button> 50 - {{ end }} 51 - 52 - </div> 53 - {{ if not .Deleted }} 54 - <div class="prose dark:prose-invert"> 55 - {{ .Body | markdown }} 56 - </div> 57 - {{ end }} 58 - </div> 59 - {{ end }} 60 - {{ end }}
···
-91
appview/pages/templates/fragments/pullActions.html
··· 1 - {{ define "fragments/pullActions" }} 2 - {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 3 - {{ $roundNumber := .RoundNumber }} 4 - 5 - {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 6 - {{ $isMerged := .Pull.State.IsMerged }} 7 - {{ $isClosed := .Pull.State.IsClosed }} 8 - {{ $isOpen := .Pull.State.IsOpen }} 9 - {{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }} 10 - {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 11 - {{ $isLastRound := eq $roundNumber $lastIdx }} 12 - {{ $isSameRepoBranch := .Pull.IsSameRepoBranch }} 13 - {{ $isUpToDate := .ResubmitCheck.No }} 14 - <div class="relative w-fit"> 15 - <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 16 - <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2"> 17 - <button 18 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 19 - hx-target="#actions-{{$roundNumber}}" 20 - hx-swap="outerHtml" 21 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline"> 22 - {{ i "message-square-plus" "w-4 h-4" }} 23 - <span>comment</span> 24 - </button> 25 - {{ if and $isPushAllowed $isOpen $isLastRound }} 26 - {{ $disabled := "" }} 27 - {{ if $isConflicted }} 28 - {{ $disabled = "disabled" }} 29 - {{ end }} 30 - <button 31 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 32 - hx-swap="none" 33 - hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 34 - class="btn p-2 flex items-center gap-2" {{ $disabled }}> 35 - {{ i "git-merge" "w-4 h-4" }} 36 - <span>merge</span> 37 - </button> 38 - {{ end }} 39 - 40 - {{ if and $isPullAuthor $isOpen $isLastRound }} 41 - {{ $disabled := "" }} 42 - {{ if $isUpToDate }} 43 - {{ $disabled = "disabled" }} 44 - {{ end }} 45 - <button id="resubmitBtn" 46 - {{ if not .Pull.IsPatch }} 47 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 48 - {{ else }} 49 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 50 - hx-target="#actions-{{$roundNumber}}" 51 - hx-swap="outerHtml" 52 - {{ end }} 53 - 54 - hx-disabled-elt="#resubmitBtn" 55 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }} 56 - 57 - {{ if $disabled }} 58 - title="Update this branch to resubmit this pull request" 59 - {{ else }} 60 - title="Resubmit this pull request" 61 - {{ end }} 62 - > 63 - {{ i "rotate-ccw" "w-4 h-4" }} 64 - <span>resubmit</span> 65 - </button> 66 - {{ end }} 67 - 68 - {{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }} 69 - <button 70 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 71 - hx-swap="none" 72 - class="btn p-2 flex items-center gap-2"> 73 - {{ i "ban" "w-4 h-4" }} 74 - <span>close</span> 75 - </button> 76 - {{ end }} 77 - 78 - {{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }} 79 - <button 80 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 81 - hx-swap="none" 82 - class="btn p-2 flex items-center gap-2"> 83 - {{ i "circle-dot" "w-4 h-4" }} 84 - <span>reopen</span> 85 - </button> 86 - {{ end }} 87 - </div> 88 - </div> 89 - {{ end }} 90 - 91 -
···
-20
appview/pages/templates/fragments/pullCompareBranches.html
··· 1 - {{ define "fragments/pullCompareBranches" }} 2 - <div id="patch-upload"> 3 - <label for="targetBranch" class="dark:text-white" 4 - >select a branch</label 5 - > 6 - <div class="flex flex-wrap gap-2 items-center"> 7 - <select 8 - name="sourceBranch" 9 - class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 10 - > 11 - <option disabled selected>source branch</option> 12 - {{ range .Branches }} 13 - <option value="{{ .Reference.Name }}" class="py-1"> 14 - {{ .Reference.Name }} 15 - </option> 16 - {{ end }} 17 - </select> 18 - </div> 19 - </div> 20 - {{ end }}
···
-42
appview/pages/templates/fragments/pullCompareForks.html
··· 1 - {{ define "fragments/pullCompareForks" }} 2 - <div id="patch-upload"> 3 - <label for="forkSelect" class="dark:text-white" 4 - >select a fork to compare</label 5 - > 6 - <div class="flex flex-wrap gap-4 items-center mb-4"> 7 - <div class="flex flex-wrap gap-2 items-center"> 8 - <select 9 - id="forkSelect" 10 - name="fork" 11 - required 12 - class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 13 - hx-get="/{{ $.RepoInfo.FullName }}/pulls/new/fork-branches" 14 - hx-target="#branch-selection" 15 - hx-vals='{"fork": this.value}' 16 - hx-swap="innerHTML" 17 - onchange="document.getElementById('hiddenForkInput').value = this.value;" 18 - > 19 - <option disabled selected>select a fork</option> 20 - {{ range .Forks }} 21 - <option value="{{ .Name }}" class="py-1"> 22 - {{ .Name }} 23 - </option> 24 - {{ end }} 25 - </select> 26 - 27 - <input 28 - type="hidden" 29 - id="hiddenForkInput" 30 - name="fork" 31 - value="" 32 - /> 33 - </div> 34 - 35 - <div id="branch-selection"> 36 - <div class="text-sm text-gray-500 dark:text-gray-400"> 37 - Select a fork first to view available branches 38 - </div> 39 - </div> 40 - </div> 41 - </div> 42 - {{ end }}
···
-15
appview/pages/templates/fragments/pullCompareForksBranches.html
··· 1 - {{ define "fragments/pullCompareForksBranches" }} 2 - <div class="flex flex-wrap gap-2 items-center"> 3 - <select 4 - name="sourceBranch" 5 - class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 6 - > 7 - <option disabled selected>source branch</option> 8 - {{ range .SourceBranches }} 9 - <option value="{{ .Reference.Name }}" class="py-1"> 10 - {{ .Reference.Name }} 11 - </option> 12 - {{ end }} 13 - </select> 14 - </div> 15 - {{ end }}
···
-32
appview/pages/templates/fragments/pullNewComment.html
··· 1 - {{ define "fragments/pullNewComment" }} 2 - <div 3 - id="pull-comment-card-{{ .RoundNumber }}" 4 - class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 - <div class="text-sm text-gray-500 dark:text-gray-400"> 6 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 7 - </div> 8 - <form 9 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 10 - hx-swap="none" 11 - class="w-full flex flex-wrap gap-2"> 12 - <textarea 13 - name="body" 14 - class="w-full p-2 rounded border border-gray-200" 15 - placeholder="Add to the discussion..."></textarea> 16 - <button type="submit" class="btn flex items-center gap-2"> 17 - {{ i "message-square" "w-4 h-4" }} comment 18 - </button> 19 - <button 20 - type="button" 21 - class="btn flex items-center gap-2" 22 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 23 - hx-swap="outerHTML" 24 - hx-target="#pull-comment-card-{{ .RoundNumber }}"> 25 - {{ i "x" "w-4 h-4" }} 26 - <span>cancel</span> 27 - </button> 28 - <div id="pull-comment"></div> 29 - </form> 30 - </div> 31 - {{ end }} 32 -
···
-14
appview/pages/templates/fragments/pullPatchUpload.html
··· 1 - {{ define "fragments/pullPatchUpload" }} 2 - <div id="patch-upload"> 3 - <textarea 4 - name="patch" 5 - id="patch" 6 - rows="12" 7 - class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600" 8 - placeholder="diff --git a/file.txt b/file.txt 9 - index 1234567..abcdefg 100644 10 - --- a/file.txt 11 - +++ b/file.txt" 12 - ></textarea> 13 - </div> 14 - {{ end }}
···
-52
appview/pages/templates/fragments/pullResubmit.html
··· 1 - {{ define "fragments/pullResubmit" }} 2 - <div 3 - id="resubmit-pull-card" 4 - class="rounded relative border bg-amber-50 dark:bg-amber-900 border-amber-200 dark:border-amber-500 px-6 py-2"> 5 - 6 - <div class="flex items-center gap-2 text-amber-500 dark:text-amber-50"> 7 - {{ i "pencil" "w-4 h-4" }} 8 - <span class="font-medium">resubmit your patch</span> 9 - </div> 10 - 11 - <div class="mt-2 text-sm text-gray-700 dark:text-gray-200"> 12 - You can update this patch to address any reviews. 13 - This will begin a new round of reviews, 14 - but you'll still be able to view your previous submissions and feedback. 15 - </div> 16 - 17 - <div class="mt-4 flex flex-col"> 18 - <form 19 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 20 - hx-swap="none" 21 - class="w-full flex flex-wrap gap-2"> 22 - <textarea 23 - name="patch" 24 - class="w-full p-2 mb-2" 25 - placeholder="Paste your updated patch here." 26 - rows="15" 27 - >{{.Pull.LatestPatch}}</textarea> 28 - <button 29 - type="submit" 30 - class="btn flex items-center gap-2" 31 - {{ if or .Pull.State.IsClosed }} 32 - disabled 33 - {{ end }}> 34 - {{ i "rotate-ccw" "w-4 h-4" }} 35 - <span>resubmit</span> 36 - </button> 37 - <button 38 - type="button" 39 - class="btn flex items-center gap-2" 40 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .Pull.LastRoundNumber }}/actions" 41 - hx-swap="outerHTML" 42 - hx-target="#resubmit-pull-card"> 43 - {{ i "x" "w-4 h-4" }} 44 - <span>cancel</span> 45 - </button> 46 - </form> 47 - 48 - <div id="resubmit-error" class="error"></div> 49 - <div id="resubmit-success" class="success"></div> 50 - </div> 51 - </div> 52 - {{ end }}
···
-41
appview/pages/templates/fragments/repoActions.html
··· 1 - {{ define "fragments/repoActions" }} 2 - <div class="flex items-center gap-2 z-auto"> 3 - <button id="starBtn" 4 - class="btn disabled:opacity-50 disabled:cursor-not-allowed" 5 - 6 - {{ if .IsStarred }} 7 - hx-delete="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}" 8 - {{ else }} 9 - hx-post="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}" 10 - {{ end }} 11 - 12 - hx-trigger="click" 13 - hx-target="#starBtn" 14 - hx-swap="outerHTML" 15 - hx-disabled-elt="#starBtn" 16 - > 17 - <div class="flex gap-2 items-center"> 18 - {{ if .IsStarred }} 19 - {{ i "star" "w-4 h-4 fill-current" }} 20 - {{ else }} 21 - {{ i "star" "w-4 h-4" }} 22 - {{ end }} 23 - <span> 24 - {{ .Stats.StarCount }} 25 - </span> 26 - </div> 27 - </button> 28 - {{ if .DisableFork }} 29 - <button class="btn no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Empty repositories cannot be forked"> 30 - {{ i "git-fork" "w-4 h-4"}} 31 - fork 32 - </button> 33 - {{ else }} 34 - <a class="btn no-underline hover:no-underline flex items-center gap-2" href="/{{ .FullName }}/fork"> 35 - {{ i "git-fork" "w-4 h-4"}} 36 - fork 37 - </a> 38 - {{ end }} 39 - </div> 40 - {{ end }} 41 -
···
-15
appview/pages/templates/fragments/repoDescription.html
··· 1 - {{ define "fragments/repoDescription" }} 2 - <span id="repo-description" class="flex flex-wrap items-center gap-2" hx-target="this" hx-swap="outerHTML"> 3 - {{ if .RepoInfo.Description }} 4 - {{ .RepoInfo.Description }} 5 - {{ else }} 6 - <span class="italic">this repo has no description</span> 7 - {{ end }} 8 - 9 - {{ if .RepoInfo.Roles.IsOwner }} 10 - <button class="btn p-2 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description/edit"> 11 - {{ i "pencil" "w-3 h-3" }} edit 12 - </button> 13 - {{ end }} 14 - </span> 15 - {{ end }}
···
+2 -2
appview/pages/templates/layouts/repobase.html
··· 19 <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 20 </div> 21 22 - {{ template "fragments/repoActions" .RepoInfo }} 23 </div> 24 - {{ template "fragments/repoDescription" . }} 25 </section> 26 <section class="min-h-screen flex flex-col drop-shadow-sm"> 27 <nav class="w-full pl-4 overflow-auto">
··· 19 <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 20 </div> 21 22 + {{ template "repo/fragments/repoActions" .RepoInfo }} 23 </div> 24 + {{ template "repo/fragments/repoDescription" . }} 25 </section> 26 <section class="min-h-screen flex flex-col drop-shadow-sm"> 27 <nav class="w-full pl-4 overflow-auto">
+1 -1
appview/pages/templates/repo/blob.html
··· 60 {{ else }} 61 <div class="overflow-auto relative"> 62 {{ if .ShowRendered }} 63 - <div id="blob-contents" class="prose dark:prose-invert p-6">{{ .RenderedContents }}</div> 64 {{ else }} 65 <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ $.Contents | escapeHtml }}</div> 66 {{ end }}
··· 60 {{ else }} 61 <div class="overflow-auto relative"> 62 {{ if .ShowRendered }} 63 + <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 64 {{ else }} 65 <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ $.Contents | escapeHtml }}</div> 66 {{ end }}
+2 -21
appview/pages/templates/repo/commit.html
··· 4 5 {{ $repo := .RepoInfo.FullName }} 6 {{ $commit := .Diff.Commit }} 7 - {{ $stat := .Diff.Stat }} 8 - {{ $diff := .Diff.Diff }} 9 10 <section class="commit dark:text-white"> 11 <div id="commit-message"> ··· 13 <div> 14 <p class="pb-2">{{ index $messageParts 0 }}</p> 15 {{ if gt (len $messageParts) 1 }} 16 - <p class="mt-1 cursor-text pb-2 text-sm">{{ nl2br (unwrapText (index $messageParts 1)) }}</p> 17 {{ end }} 18 </div> 19 </div> ··· 29 {{ end }} 30 <span class="px-1 select-none before:content-['\00B7']"></span> 31 {{ timeFmt $commit.Author.When }} 32 - <span class="px-1 select-none before:content-['\00B7']"></span> 33 - <span>{{ $stat.FilesChanged }}</span> files <span class="font-mono">(+{{ $stat.Insertions }}, -{{ $stat.Deletions }})</span> 34 <span class="px-1 select-none before:content-['\00B7']"></span> 35 </p> 36 ··· 43 </p> 44 </div> 45 46 - <div class="diff-stat"> 47 - <br> 48 - <strong class="text-sm uppercase mb-4 dark:text-gray-200">Changed files</strong> 49 - <div class="overflow-x-auto"> 50 - {{ range $diff }} 51 - <ul class="dark:text-gray-200"> 52 - {{ if .IsDelete }} 53 - <li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li> 54 - {{ else }} 55 - <li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li> 56 - {{ end }} 57 - </ul> 58 - {{ end }} 59 - </div> 60 - </div> 61 </section> 62 63 {{end}} 64 65 {{ define "repoAfter" }} 66 - {{ template "fragments/diff" (list .RepoInfo.FullName .Diff) }} 67 {{end}}
··· 4 5 {{ $repo := .RepoInfo.FullName }} 6 {{ $commit := .Diff.Commit }} 7 8 <section class="commit dark:text-white"> 9 <div id="commit-message"> ··· 11 <div> 12 <p class="pb-2">{{ index $messageParts 0 }}</p> 13 {{ if gt (len $messageParts) 1 }} 14 + <p class="mt-1 cursor-text pb-2 text-sm">{{ nl2br (index $messageParts 1) }}</p> 15 {{ end }} 16 </div> 17 </div> ··· 27 {{ end }} 28 <span class="px-1 select-none before:content-['\00B7']"></span> 29 {{ timeFmt $commit.Author.When }} 30 <span class="px-1 select-none before:content-['\00B7']"></span> 31 </p> 32 ··· 39 </p> 40 </div> 41 42 </section> 43 44 {{end}} 45 46 {{ define "repoAfter" }} 47 + {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }} 48 {{end}}
+1 -1
appview/pages/templates/repo/empty.html
··· 9 {{ end }} 10 11 {{ define "repoAfter" }} 12 - {{ template "fragments/cloneInstructions" . }} 13 {{ end }}
··· 9 {{ end }} 10 11 {{ define "repoAfter" }} 12 + {{ template "repo/fragments/cloneInstructions" . }} 13 {{ end }}
+51
appview/pages/templates/repo/fragments/cloneInstructions.html
···
··· 1 + {{ define "repo/fragments/cloneInstructions" }} 2 + <section 3 + class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4" 4 + > 5 + <div class="flex flex-col gap-2"> 6 + <strong>push</strong> 7 + <div class="md:pl-4 overflow-x-auto whitespace-nowrap"> 8 + <code class="dark:text-gray-100" 9 + >git remote add origin 10 + git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 11 + > 12 + </div> 13 + </div> 14 + 15 + <div class="flex flex-col gap-2"> 16 + <strong>clone</strong> 17 + <div class="md:pl-4 flex flex-col gap-2"> 18 + <div class="flex items-center gap-3"> 19 + <span 20 + class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white" 21 + >HTTP</span 22 + > 23 + <div class="overflow-x-auto whitespace-nowrap flex-1"> 24 + <code class="dark:text-gray-100" 25 + >git clone 26 + https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code 27 + > 28 + </div> 29 + </div> 30 + 31 + <div class="flex items-center gap-3"> 32 + <span 33 + class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white" 34 + >SSH</span 35 + > 36 + <div class="overflow-x-auto whitespace-nowrap flex-1"> 37 + <code class="dark:text-gray-100" 38 + >git clone 39 + git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 40 + > 41 + </div> 42 + </div> 43 + </div> 44 + </div> 45 + 46 + <p class="py-2 text-gray-500 dark:text-gray-400"> 47 + Note that for self-hosted knots, clone URLs may be different based 48 + on your setup. 49 + </p> 50 + </section> 51 + {{ end }}
+175
appview/pages/templates/repo/fragments/diff.html
···
··· 1 + {{ define "repo/fragments/diff" }} 2 + {{ $repo := index . 0 }} 3 + {{ $diff := index . 1 }} 4 + {{ $commit := $diff.Commit }} 5 + {{ $stat := $diff.Stat }} 6 + {{ $diff := $diff.Diff }} 7 + 8 + {{ $this := $commit.This }} 9 + {{ $parent := $commit.Parent }} 10 + 11 + <section class="mt-6 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 12 + <div class="diff-stat"> 13 + <div class="flex gap-2 items-center"> 14 + <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong> 15 + {{ block "statPill" $stat }} {{ end }} 16 + </div> 17 + <div class="overflow-x-auto"> 18 + {{ range $diff }} 19 + <ul class="dark:text-gray-200"> 20 + {{ if .IsDelete }} 21 + <li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li> 22 + {{ else }} 23 + <li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li> 24 + {{ end }} 25 + </ul> 26 + {{ end }} 27 + </div> 28 + </div> 29 + </section> 30 + 31 + {{ $last := sub (len $diff) 1 }} 32 + {{ range $idx, $hunk := $diff }} 33 + {{ with $hunk }} 34 + <section class="mt-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 35 + <div id="file-{{ .Name.New }}"> 36 + <div id="diff-file"> 37 + <details open> 38 + <summary class="list-none cursor-pointer sticky top-0"> 39 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 40 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto" style="direction: rtl;"> 41 + {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 42 + 43 + <div class="flex gap-2 items-center" style="direction: ltr;"> 44 + {{ if .IsNew }} 45 + <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span> 46 + {{ else if .IsDelete }} 47 + <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">DELETED</span> 48 + {{ else if .IsCopy }} 49 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">COPIED</span> 50 + {{ else if .IsRename }} 51 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">RENAMED</span> 52 + {{ else }} 53 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">MODIFIED</span> 54 + {{ end }} 55 + 56 + {{ block "statPill" .Stats }} {{ end }} 57 + 58 + {{ if .IsDelete }} 59 + <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}> 60 + {{ .Name.Old }} 61 + </a> 62 + {{ else if (or .IsCopy .IsRename) }} 63 + <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $parent}}href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}"{{end}}> 64 + {{ .Name.Old }} 65 + </a> 66 + {{ i "arrow-right" "w-4 h-4" }} 67 + <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 68 + {{ .Name.New }} 69 + </a> 70 + {{ else }} 71 + <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 72 + {{ .Name.New }} 73 + </a> 74 + {{ end }} 75 + </div> 76 + </div> 77 + 78 + {{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }} 79 + <div id="right-side-items" class="p-2 flex items-center"> 80 + <a title="top of file" href="#file-{{ .Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a> 81 + {{ if gt $idx 0 }} 82 + {{ $prev := index $diff (sub $idx 1) }} 83 + <a title="previous file" href="#file-{{ $prev.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a> 84 + {{ end }} 85 + 86 + {{ if lt $idx $last }} 87 + {{ $next := index $diff (add $idx 1) }} 88 + <a title="next file" href="#file-{{ $next.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a> 89 + {{ end }} 90 + </div> 91 + 92 + </div> 93 + </summary> 94 + 95 + <div class="transition-all duration-700 ease-in-out"> 96 + {{ if .IsDelete }} 97 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 98 + This file has been deleted. 99 + </p> 100 + {{ else if .IsCopy }} 101 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 102 + This file has been copied. 103 + </p> 104 + {{ else if .IsRename }} 105 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 106 + This file has been renamed. 107 + </p> 108 + {{ else if .IsBinary }} 109 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 110 + This is a binary file and will not be displayed. 111 + </p> 112 + {{ else }} 113 + {{ $name := .Name.New }} 114 + <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 115 + {{- $oldStart := .OldPosition -}} 116 + {{- $newStart := .NewPosition -}} 117 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 scroll-mt-10 target:border target:border-amber-500 target:rounded " -}} 118 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 119 + {{- $lineNrSepStyle1 := "" -}} 120 + {{- $lineNrSepStyle2 := "pr-2" -}} 121 + {{- range .Lines -}} 122 + {{- if eq .Op.String "+" -}} 123 + <div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full items-center"> 124 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 125 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 126 + <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 127 + <div class="px-2">{{ .Line }}</div> 128 + </div> 129 + {{- $newStart = add64 $newStart 1 -}} 130 + {{- end -}} 131 + {{- if eq .Op.String "-" -}} 132 + <div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full items-center"> 133 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 134 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 135 + <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 136 + <div class="px-2">{{ .Line }}</div> 137 + </div> 138 + {{- $oldStart = add64 $oldStart 1 -}} 139 + {{- end -}} 140 + {{- if eq .Op.String " " -}} 141 + <div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full items-center"> 142 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 143 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 144 + <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 145 + <div class="px-2">{{ .Line }}</div> 146 + </div> 147 + {{- $newStart = add64 $newStart 1 -}} 148 + {{- $oldStart = add64 $oldStart 1 -}} 149 + {{- end -}} 150 + {{- end -}} 151 + {{- end -}}</div></div></pre> 152 + {{- end -}} 153 + </div> 154 + 155 + </details> 156 + 157 + </div> 158 + </div> 159 + </section> 160 + {{ end }} 161 + {{ end }} 162 + {{ end }} 163 + 164 + {{ define "statPill" }} 165 + <div class="flex items-center font-mono text-sm"> 166 + {{ if and .Insertions .Deletions }} 167 + <span class="rounded-l p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span> 168 + <span class="rounded-r p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span> 169 + {{ else if .Insertions }} 170 + <span class="rounded p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span> 171 + {{ else if .Deletions }} 172 + <span class="rounded p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span> 173 + {{ end }} 174 + </div> 175 + {{ end }}
+11
appview/pages/templates/repo/fragments/editRepoDescription.html
···
··· 1 + {{ define "repo/fragments/editRepoDescription" }} 2 + <form hx-put="/{{ .RepoInfo.FullName }}/description" hx-target="this" hx-swap="outerHTML" class="flex flex-wrap gap-2"> 3 + <input type="text" class="p-1" name="description" value="{{ .RepoInfo.Description }}"> 4 + <button type="submit" class="btn p-1 flex items-center gap-2 no-underline text-sm"> 5 + {{ i "check" "w-3 h-3" }} save 6 + </button> 7 + <button type="button" class="btn p-1 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description" > 8 + {{ i "x" "w-3 h-3" }} cancel 9 + </button> 10 + </form> 11 + {{ end }}
+47
appview/pages/templates/repo/fragments/repoActions.html
···
··· 1 + {{ define "repo/fragments/repoActions" }} 2 + <div class="flex items-center gap-2 z-auto"> 3 + <button 4 + id="starBtn" 5 + class="btn disabled:opacity-50 disabled:cursor-not-allowed" 6 + {{ if .IsStarred }} 7 + hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}" 8 + {{ else }} 9 + hx-post="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}" 10 + {{ end }} 11 + 12 + hx-trigger="click" 13 + hx-target="#starBtn" 14 + hx-swap="outerHTML" 15 + hx-disabled-elt="#starBtn" 16 + > 17 + <div class="flex gap-2 items-center"> 18 + {{ if .IsStarred }} 19 + {{ i "star" "w-4 h-4 fill-current" }} 20 + {{ else }} 21 + {{ i "star" "w-4 h-4" }} 22 + {{ end }} 23 + <span class="text-sm"> 24 + {{ .Stats.StarCount }} 25 + </span> 26 + </div> 27 + </button> 28 + {{ if .DisableFork }} 29 + <button 30 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" 31 + disabled 32 + title="Empty repositories cannot be forked" 33 + > 34 + {{ i "git-fork" "w-4 h-4" }} 35 + fork 36 + </button> 37 + {{ else }} 38 + <a 39 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2" 40 + href="/{{ .FullName }}/fork" 41 + > 42 + {{ i "git-fork" "w-4 h-4" }} 43 + fork 44 + </a> 45 + {{ end }} 46 + </div> 47 + {{ end }}
+15
appview/pages/templates/repo/fragments/repoDescription.html
···
··· 1 + {{ define "repo/fragments/repoDescription" }} 2 + <span id="repo-description" class="flex flex-wrap items-center gap-2 text-sm" hx-target="this" hx-swap="outerHTML"> 3 + {{ if .RepoInfo.Description }} 4 + {{ .RepoInfo.Description }} 5 + {{ else }} 6 + <span class="italic">this repo has no description</span> 7 + {{ end }} 8 + 9 + {{ if .RepoInfo.Roles.IsOwner }} 10 + <button class="flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description/edit"> 11 + {{ i "pencil" "w-3 h-3" }} 12 + </button> 13 + {{ end }} 14 + </span> 15 + {{ end }}
+207 -172
appview/pages/templates/repo/index.html
··· 1 {{ define "title" }}{{ .RepoInfo.FullName }} at {{ .Ref }}{{ end }} 2 3 - 4 {{ define "extrameta" }} 5 - <meta name="vcs:clone" content="https://tangled.sh/{{ .RepoInfo.FullName }}"/> 6 - <meta name="forge:summary" content="https://tangled.sh/{{ .RepoInfo.FullName }}"> 7 - <meta name="forge:dir" content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}"> 8 - <meta name="forge:file" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}"> 9 - <meta name="forge:line" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}"> 10 - <meta name="go-import" content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}"> 11 {{ end }} 12 - 13 14 {{ define "repoContent" }} 15 <main> 16 - {{ block "branchSelector" . }} {{ end }} 17 <div class="grid grid-cols-1 md:grid-cols-2 gap-2"> 18 - {{ block "fileTree" . }} {{ end }} 19 - {{ block "commitLog" . }} {{ end }} 20 </div> 21 </main> 22 {{ end }} 23 24 {{ define "branchSelector" }} 25 - <div class="flex justify-between pb-5"> 26 - <select 27 - onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 28 - class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 29 - > 30 - <optgroup label="branches" class="bold text-sm"> 31 - {{ range .Branches }} 32 - <option 33 - value="{{ .Reference.Name }}" 34 - class="py-1" 35 - {{ if eq .Reference.Name $.Ref }} 36 - selected 37 - {{ end }} 38 - > 39 - {{ .Reference.Name }} 40 - </option> 41 - {{ end }} 42 - </optgroup> 43 - <optgroup label="tags" class="bold text-sm"> 44 - {{ range .Tags }} 45 - <option 46 - value="{{ .Reference.Name }}" 47 - class="py-1" 48 - {{ if eq .Reference.Name $.Ref }} 49 - selected 50 - {{ end }} 51 - > 52 - {{ .Reference.Name }} 53 - </option> 54 - {{ else }} 55 - <option class="py-1" disabled>no tags found</option> 56 - {{ end }} 57 - </optgroup> 58 - </select> 59 - <a 60 - href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" 61 - class="ml-2 no-underline flex items-center gap-2 text-sm uppercase font-bold dark:text-white" 62 - > 63 - {{ i "logs" "w-4 h-4" }} 64 - {{ .TotalCommits }} 65 - {{ if eq .TotalCommits 1 }}commit{{ else }}commits{{ end }} 66 - </a> 67 - </div> 68 {{ end }} 69 70 {{ define "fileTree" }} 71 - <div id="file-tree" class="col-span-1 pr-2 md:border-r md:border-gray-200 dark:md:border-gray-700"> 72 - {{ $containerstyle := "py-1" }} 73 - {{ $linkstyle := "no-underline hover:underline dark:text-white" }} 74 75 - {{ range .Files }} 76 - {{ if not .IsFile }} 77 - <div class="{{ $containerstyle }}"> 78 - <div class="flex justify-between items-center"> 79 - <a 80 - href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref | urlquery }}/{{ .Name }}" 81 - class="{{ $linkstyle }}" 82 - > 83 - <div class="flex items-center gap-2"> 84 - {{ i "folder" "w-3 h-3 fill-current" }} 85 - {{ .Name }} 86 - </div> 87 - </a> 88 89 - <time class="text-xs text-gray-500 dark:text-gray-400" 90 - >{{ timeFmt .LastCommit.When }}</time 91 - > 92 </div> 93 - </div> 94 {{ end }} 95 - {{ end }} 96 97 - {{ range .Files }} 98 - {{ if .IsFile }} 99 - <div class="{{ $containerstyle }}"> 100 - <div class="flex justify-between items-center"> 101 - <a 102 - href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref | urlquery }}/{{ .Name }}" 103 - class="{{ $linkstyle }}" 104 - > 105 - <div class="flex items-center gap-2"> 106 - {{ i "file" "w-3 h-3" }}{{ .Name }} 107 - </div> 108 - </a> 109 110 - <time class="text-xs text-gray-500 dark:text-gray-400" 111 - >{{ timeFmt .LastCommit.When }}</time 112 - > 113 </div> 114 - </div> 115 {{ end }} 116 - {{ end }} 117 - </div> 118 {{ end }} 119 120 - 121 {{ define "commitLog" }} 122 - <div id="commit-log" class="hidden md:block md:col-span-1"> 123 - {{ range .Commits }} 124 - <div class="relative px-2 pb-8"> 125 - <div id="commit-message"> 126 - {{ $messageParts := splitN .Message "\n\n" 2 }} 127 - <div class="text-base cursor-pointer"> 128 - <div> 129 - <div> 130 - <a 131 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 132 - class="inline no-underline hover:underline dark:text-white" 133 - >{{ index $messageParts 0 }}</a 134 - > 135 - {{ if gt (len $messageParts) 1 }} 136 137 - <button 138 - class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 139 - hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 140 - > 141 - {{ i "ellipsis" "w-3 h-3" }} 142 - </button> 143 - {{ end }} 144 - </div> 145 - {{ if gt (len $messageParts) 1 }} 146 - <p 147 - class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 148 - > 149 - {{ nl2br (unwrapText (index $messageParts 1)) }} 150 - </p> 151 - {{ end }} 152 - </div> 153 - </div> 154 - </div> 155 156 - <div class="text-xs text-gray-500 dark:text-gray-400"> 157 - <span class="font-mono"> 158 - <a 159 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 160 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 161 - >{{ slice .Hash.String 0 8 }}</a 162 - > 163 - </span> 164 - <span 165 - class="mx-2 before:content-['ยท'] before:select-none" 166 - ></span> 167 - <span> 168 - {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 169 - <a 170 - href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ .Author.Email }}{{ end }}" 171 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 172 - >{{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ .Author.Name }}{{ end }}</a 173 - > 174 - </span> 175 - <div 176 - class="inline-block px-1 select-none after:content-['ยท']" 177 - ></div> 178 - <span>{{ timeFmt .Author.When }}</span> 179 - {{ $tagsForCommit := index $.TagMap .Hash.String }} 180 - {{ if gt (len $tagsForCommit) 0 }} 181 <div 182 class="inline-block px-1 select-none after:content-['ยท']" 183 ></div> 184 - {{ end }} 185 - {{ range $tagsForCommit }} 186 - <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 187 - {{ . }} 188 - </span> 189 - {{ end }} 190 - </div> 191 - </div> 192 - {{ end }} 193 - </div> 194 {{ end }} 195 - 196 197 {{ define "repoAfter" }} 198 {{- if .HTMLReadme }} 199 - <section class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }} prose dark:prose-invert dark:[&_pre]:bg-gray-900 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 dark:[&_pre]:border dark:[&_pre]:border-gray-700 {{ end }}"> 200 - <article class="{{ if .Raw }}whitespace-pre{{end}}"> 201 - {{ if .Raw }} 202 - <pre class="dark:bg-gray-900 dark:text-gray-200 dark:border dark:border-gray-700 dark:p-4 dark:rounded">{{ .HTMLReadme }}</pre> 203 - {{ else }} 204 - {{ .HTMLReadme }} 205 - {{ end }} 206 - </article> 207 - </section> 208 {{- end -}} 209 210 - {{ template "fragments/cloneInstructions" . }} 211 {{ end }}
··· 1 {{ define "title" }}{{ .RepoInfo.FullName }} at {{ .Ref }}{{ end }} 2 3 {{ define "extrameta" }} 4 + <meta 5 + name="vcs:clone" 6 + content="https://tangled.sh/{{ .RepoInfo.FullName }}" 7 + /> 8 + <meta 9 + name="forge:summary" 10 + content="https://tangled.sh/{{ .RepoInfo.FullName }}" 11 + /> 12 + <meta 13 + name="forge:dir" 14 + content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}" 15 + /> 16 + <meta 17 + name="forge:file" 18 + content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}" 19 + /> 20 + <meta 21 + name="forge:line" 22 + content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}" 23 + /> 24 + <meta 25 + name="go-import" 26 + content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}" 27 + /> 28 {{ end }} 29 30 {{ define "repoContent" }} 31 <main> 32 + {{ block "branchSelector" . }}{{ end }} 33 <div class="grid grid-cols-1 md:grid-cols-2 gap-2"> 34 + {{ block "fileTree" . }}{{ end }} 35 + {{ block "commitLog" . }}{{ end }} 36 </div> 37 </main> 38 {{ end }} 39 40 {{ define "branchSelector" }} 41 + <div class="flex justify-between pb-5"> 42 + <select 43 + onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 44 + class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 45 + > 46 + <optgroup label="branches" class="bold text-sm"> 47 + {{ range .Branches }} 48 + <option 49 + value="{{ .Reference.Name }}" 50 + class="py-1" 51 + {{ if eq .Reference.Name $.Ref }} 52 + selected 53 + {{ end }} 54 + > 55 + {{ .Reference.Name }} 56 + </option> 57 + {{ end }} 58 + </optgroup> 59 + <optgroup label="tags" class="bold text-sm"> 60 + {{ range .Tags }} 61 + <option 62 + value="{{ .Reference.Name }}" 63 + class="py-1" 64 + {{ if eq .Reference.Name $.Ref }} 65 + selected 66 + {{ end }} 67 + > 68 + {{ .Reference.Name }} 69 + </option> 70 + {{ else }} 71 + <option class="py-1" disabled>no tags found</option> 72 + {{ end }} 73 + </optgroup> 74 + </select> 75 + <a 76 + href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" 77 + class="ml-2 no-underline flex items-center gap-2 text-sm uppercase font-bold dark:text-white" 78 + > 79 + {{ i "logs" "w-4 h-4" }} 80 + {{ .TotalCommits }} 81 + {{ if eq .TotalCommits 1 }}commit{{ else }}commits{{ end }} 82 + </a> 83 + </div> 84 {{ end }} 85 86 {{ define "fileTree" }} 87 + <div 88 + id="file-tree" 89 + class="col-span-1 pr-2 md:border-r md:border-gray-200 dark:md:border-gray-700" 90 + > 91 + {{ $containerstyle := "py-1" }} 92 + {{ $linkstyle := "no-underline hover:underline dark:text-white" }} 93 94 + {{ range .Files }} 95 + {{ if not .IsFile }} 96 + <div class="{{ $containerstyle }}"> 97 + <div class="flex justify-between items-center"> 98 + <a 99 + href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref | urlquery }}/{{ .Name }}" 100 + class="{{ $linkstyle }}" 101 + > 102 + <div class="flex items-center gap-2"> 103 + {{ i "folder" "w-3 h-3 fill-current" }} 104 + {{ .Name }} 105 + </div> 106 + </a> 107 108 + <time class="text-xs text-gray-500 dark:text-gray-400" 109 + >{{ timeFmt .LastCommit.When }}</time 110 + > 111 + </div> 112 </div> 113 + {{ end }} 114 {{ end }} 115 116 + {{ range .Files }} 117 + {{ if .IsFile }} 118 + <div class="{{ $containerstyle }}"> 119 + <div class="flex justify-between items-center"> 120 + <a 121 + href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref | urlquery }}/{{ .Name }}" 122 + class="{{ $linkstyle }}" 123 + > 124 + <div class="flex items-center gap-2"> 125 + {{ i "file" "w-3 h-3" }}{{ .Name }} 126 + </div> 127 + </a> 128 129 + <time class="text-xs text-gray-500 dark:text-gray-400" 130 + >{{ timeFmt .LastCommit.When }}</time 131 + > 132 + </div> 133 </div> 134 + {{ end }} 135 {{ end }} 136 + </div> 137 {{ end }} 138 139 {{ define "commitLog" }} 140 + <div id="commit-log" class="hidden md:block md:col-span-1"> 141 + {{ range .Commits }} 142 + <div class="relative px-2 pb-8"> 143 + <div id="commit-message"> 144 + {{ $messageParts := splitN .Message "\n\n" 2 }} 145 + <div class="text-base cursor-pointer"> 146 + <div> 147 + <div> 148 + <a 149 + href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 150 + class="inline no-underline hover:underline dark:text-white" 151 + >{{ index $messageParts 0 }}</a 152 + > 153 + {{ if gt (len $messageParts) 1 }} 154 155 + <button 156 + class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 157 + hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 158 + > 159 + {{ i "ellipsis" "w-3 h-3" }} 160 + </button> 161 + {{ end }} 162 + </div> 163 + {{ if gt (len $messageParts) 1 }} 164 + <p 165 + class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 166 + > 167 + {{ nl2br (index $messageParts 1) }} 168 + </p> 169 + {{ end }} 170 + </div> 171 + </div> 172 + </div> 173 174 + <div class="text-xs text-gray-500 dark:text-gray-400"> 175 + <span class="font-mono"> 176 + <a 177 + href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 178 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 179 + >{{ slice .Hash.String 0 8 }}</a></span> 180 + <span 181 + class="mx-2 before:content-['ยท'] before:select-none" 182 + ></span> 183 + <span> 184 + {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 185 + <a 186 + href="{{ if $didOrHandle }} 187 + /{{ $didOrHandle }} 188 + {{ else }} 189 + mailto:{{ .Author.Email }} 190 + {{ end }}" 191 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 192 + >{{ if $didOrHandle }} 193 + {{ $didOrHandle }} 194 + {{ else }} 195 + {{ .Author.Name }} 196 + {{ end }}</a 197 + > 198 + </span> 199 <div 200 class="inline-block px-1 select-none after:content-['ยท']" 201 ></div> 202 + <span>{{ timeFmt .Author.When }}</span> 203 + {{ $tagsForCommit := index $.TagMap .Hash.String }} 204 + {{ if gt (len $tagsForCommit) 0 }} 205 + <div 206 + class="inline-block px-1 select-none after:content-['ยท']" 207 + ></div> 208 + {{ end }} 209 + {{ range $tagsForCommit }} 210 + <span 211 + class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center" 212 + > 213 + {{ . }} 214 + </span> 215 + {{ end }} 216 + </div> 217 + </div> 218 + {{ end }} 219 + </div> 220 {{ end }} 221 222 {{ define "repoAfter" }} 223 {{- if .HTMLReadme }} 224 + <section 225 + class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }} 226 + prose dark:prose-invert dark:[&_pre]:bg-gray-900 227 + dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 228 + dark:[&_pre]:border dark:[&_pre]:border-gray-700 229 + {{ end }}" 230 + > 231 + <article class="{{ if .Raw }}whitespace-pre{{ end }}"> 232 + {{ if .Raw }} 233 + <pre 234 + class="dark:bg-gray-900 dark:text-gray-200 dark:border dark:border-gray-700 dark:p-4 dark:rounded" 235 + > 236 + {{ .HTMLReadme }}</pre 237 + > 238 + {{ else }} 239 + {{ .HTMLReadme }} 240 + {{ end }} 241 + </article> 242 + </section> 243 {{- end -}} 244 245 + {{ template "repo/fragments/cloneInstructions" . }} 246 {{ end }}
+52
appview/pages/templates/repo/issues/fragments/editIssueComment.html
···
··· 1 + {{ define "repo/issues/fragments/editIssueComment" }} 2 + {{ with .Comment }} 3 + <div id="comment-container-{{.CommentId}}"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 text-sm"> 5 + {{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }} 6 + <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 + 8 + <!-- show user "hats" --> 9 + {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 10 + {{ if $isIssueAuthor }} 11 + <span class="before:content-['ยท']"></span> 12 + <span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center"> 13 + author 14 + </span> 15 + {{ end }} 16 + 17 + <span class="before:content-['ยท']"></span> 18 + <a 19 + href="#{{ .CommentId }}" 20 + class="text-gray-500 hover:text-gray-500 hover:underline no-underline" 21 + id="{{ .CommentId }}"> 22 + {{ .Created | timeFmt }} 23 + </a> 24 + 25 + <button 26 + class="btn px-2 py-1 flex items-center gap-2 text-sm" 27 + hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 28 + hx-include="#edit-textarea-{{ .CommentId }}" 29 + hx-target="#comment-container-{{ .CommentId }}" 30 + hx-swap="outerHTML"> 31 + {{ i "check" "w-4 h-4" }} 32 + </button> 33 + <button 34 + class="btn px-2 py-1 flex items-center gap-2 text-sm" 35 + hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 36 + hx-target="#comment-container-{{ .CommentId }}" 37 + hx-swap="outerHTML"> 38 + {{ i "x" "w-4 h-4" }} 39 + </button> 40 + <span id="comment-{{.CommentId}}-status"></span> 41 + </div> 42 + 43 + <div> 44 + <textarea 45 + id="edit-textarea-{{ .CommentId }}" 46 + name="body" 47 + class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea> 48 + </div> 49 + </div> 50 + {{ end }} 51 + {{ end }} 52 +
+59
appview/pages/templates/repo/issues/fragments/issueComment.html
···
··· 1 + {{ define "repo/issues/fragments/issueComment" }} 2 + {{ with .Comment }} 3 + <div id="comment-container-{{.CommentId}}"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm"> 5 + {{ $owner := index $.DidHandleMap .OwnerDid }} 6 + <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 + 8 + <span class="before:content-['ยท']"></span> 9 + <a 10 + href="#{{ .CommentId }}" 11 + class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline" 12 + id="{{ .CommentId }}"> 13 + {{ if .Deleted }} 14 + deleted {{ .Deleted | timeFmt }} 15 + {{ else if .Edited }} 16 + edited {{ .Edited | timeFmt }} 17 + {{ else }} 18 + {{ .Created | timeFmt }} 19 + {{ end }} 20 + </a> 21 + 22 + <!-- show user "hats" --> 23 + {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 24 + {{ if $isIssueAuthor }} 25 + <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 26 + author 27 + </span> 28 + {{ end }} 29 + 30 + {{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }} 31 + {{ if and $isCommentOwner (not .Deleted) }} 32 + <button 33 + class="btn px-2 py-1 text-sm" 34 + hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 35 + hx-swap="outerHTML" 36 + hx-target="#comment-container-{{.CommentId}}" 37 + > 38 + {{ i "pencil" "w-4 h-4" }} 39 + </button> 40 + <button 41 + class="btn px-2 py-1 text-sm text-red-500" 42 + hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 43 + hx-confirm="Are you sure you want to delete your comment?" 44 + hx-swap="outerHTML" 45 + hx-target="#comment-container-{{.CommentId}}" 46 + > 47 + {{ i "trash-2" "w-4 h-4" }} 48 + </button> 49 + {{ end }} 50 + 51 + </div> 52 + {{ if not .Deleted }} 53 + <div class="prose dark:prose-invert"> 54 + {{ .Body | markdown }} 55 + </div> 56 + {{ end }} 57 + </div> 58 + {{ end }} 59 + {{ end }}
+112 -42
appview/pages/templates/repo/issues/issue.html
··· 44 {{ end }} 45 46 {{ define "repoAfter" }} 47 - {{ if gt (len .Comments) 0 }} 48 - <section id="comments" class="mt-8 space-y-4 relative"> 49 {{ range $index, $comment := .Comments }} 50 <div 51 id="comment-{{ .CommentId }}" 52 - class="rounded bg-white px-6 py-4 relative dark:bg-gray-800"> 53 - {{ if eq $index 0 }} 54 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div> 55 - {{ else }} 56 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-700" ></div> 57 {{ end }} 58 - 59 - {{ template "fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}} 60 </div> 61 {{ end }} 62 </section> 63 - {{ end }} 64 65 {{ block "newComment" . }} {{ end }} 66 67 - {{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }} 68 - {{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }} 69 - {{ if or $isIssueAuthor $isRepoCollaborator }} 70 - {{ $action := "close" }} 71 - {{ $icon := "circle-x" }} 72 - {{ $hoverColor := "red" }} 73 - {{ if eq .State "closed" }} 74 - {{ $action = "reopen" }} 75 - {{ $icon = "circle-dot" }} 76 - {{ $hoverColor = "green" }} 77 - {{ end }} 78 - <form 79 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/{{ $action }}" 80 - hx-swap="none" 81 - class="mt-8" 82 - > 83 - <button type="submit" class="btn hover:bg-{{ $hoverColor }}-300"> 84 - {{ i $icon "w-4 h-4 mr-2" }} 85 - <span class="text-black dark:text-gray-400">{{ $action }}</span> 86 - </button> 87 - <div id="issue-action" class="error"></div> 88 - </form> 89 - {{ end }} 90 {{ end }} 91 92 {{ define "newComment" }} 93 {{ if .LoggedInUser }} 94 - <div class="bg-white rounded drop-shadow-sm py-4 px-6 relative w-full flex flex-col gap-2 mt-8 dark:bg-gray-800 dark:text-gray-400"> 95 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div> 96 - <div class="text-sm text-gray-500 dark:text-gray-400"> 97 {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 98 </div> 99 - <form hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"> 100 <textarea 101 name="body" 102 class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 103 - placeholder="Add to the discussion..." 104 ></textarea> 105 - <button type="submit" class="btn mt-2">comment</button> 106 <div id="issue-comment"></div> 107 - </form> 108 </div> 109 {{ else }} 110 - <div class="bg-white dark:bg-gray-800 dark:text-gray-400 rounded drop-shadow-sm px-6 py-4 mt-8"> 111 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div> 112 <a href="/login" class="underline">login</a> to join the discussion 113 </div> 114 {{ end }}
··· 44 {{ end }} 45 46 {{ define "repoAfter" }} 47 + <section id="comments" class="my-2 mt-2 space-y-2 relative"> 48 {{ range $index, $comment := .Comments }} 49 <div 50 id="comment-{{ .CommentId }}" 51 + class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit"> 52 + {{ if gt $index 0 }} 53 + <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 54 {{ end }} 55 + {{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}} 56 </div> 57 {{ end }} 58 </section> 59 60 {{ block "newComment" . }} {{ end }} 61 62 {{ end }} 63 64 {{ define "newComment" }} 65 {{ if .LoggedInUser }} 66 + <form 67 + id="comment-form" 68 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 69 + hx-on::after-request="if(event.detail.successful) this.reset()" 70 + > 71 + <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5"> 72 + <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 73 {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 74 </div> 75 <textarea 76 + id="comment-textarea" 77 name="body" 78 class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 79 + placeholder="Add to the discussion. Markdown is supported." 80 + onkeyup="updateCommentForm()" 81 ></textarea> 82 <div id="issue-comment"></div> 83 + <div id="issue-action" class="error"></div> 84 </div> 85 + 86 + <div class="flex gap-2 mt-2"> 87 + <button 88 + id="comment-button" 89 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 90 + type="submit" 91 + hx-disabled-elt="#comment-button" 92 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline" 93 + disabled 94 + > 95 + {{ i "message-square-plus" "w-4 h-4" }} 96 + comment 97 + </button> 98 + 99 + {{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }} 100 + {{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }} 101 + {{ if and (or $isIssueAuthor $isRepoCollaborator) (eq .State "open") }} 102 + <button 103 + id="close-button" 104 + type="button" 105 + class="btn flex items-center gap-2" 106 + hx-trigger="click" 107 + > 108 + {{ i "ban" "w-4 h-4" }} 109 + close 110 + </button> 111 + <div 112 + id="close-with-comment" 113 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 114 + hx-trigger="click from:#close-button" 115 + hx-disabled-elt="#close-with-comment" 116 + hx-target="#issue-comment" 117 + hx-vals="js:{body: document.getElementById('comment-textarea').value.trim() !== '' ? document.getElementById('comment-textarea').value : ''}" 118 + hx-swap="none" 119 + > 120 + </div> 121 + <div 122 + id="close-issue" 123 + hx-disabled-elt="#close-issue" 124 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/close" 125 + hx-trigger="click from:#close-button" 126 + hx-target="#issue-action" 127 + hx-swap="none" 128 + > 129 + </div> 130 + <script> 131 + document.addEventListener('htmx:configRequest', function(evt) { 132 + if (evt.target.id === 'close-with-comment') { 133 + const commentText = document.getElementById('comment-textarea').value.trim(); 134 + if (commentText === '') { 135 + evt.detail.parameters = {}; 136 + evt.preventDefault(); 137 + } 138 + } 139 + }); 140 + </script> 141 + {{ else if and (or $isIssueAuthor $isRepoCollaborator) (eq .State "closed") }} 142 + <button 143 + type="button" 144 + class="btn flex items-center gap-2" 145 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/reopen" 146 + hx-swap="none" 147 + > 148 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 149 + reopen 150 + </button> 151 + {{ end }} 152 + 153 + <script> 154 + function updateCommentForm() { 155 + const textarea = document.getElementById('comment-textarea'); 156 + const commentButton = document.getElementById('comment-button'); 157 + const closeButton = document.getElementById('close-button'); 158 + 159 + if (textarea.value.trim() !== '') { 160 + commentButton.removeAttribute('disabled'); 161 + } else { 162 + commentButton.setAttribute('disabled', ''); 163 + } 164 + 165 + if (closeButton) { 166 + if (textarea.value.trim() !== '') { 167 + closeButton.innerHTML = '{{ i "ban" "w-4 h-4" }} close with comment'; 168 + } else { 169 + closeButton.innerHTML = '{{ i "ban" "w-4 h-4" }} close'; 170 + } 171 + } 172 + } 173 + 174 + document.addEventListener('DOMContentLoaded', function() { 175 + updateCommentForm(); 176 + }); 177 + </script> 178 + </div> 179 + </form> 180 {{ else }} 181 + <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-fit"> 182 <a href="/login" class="underline">login</a> to join the discussion 183 </div> 184 {{ end }}
+3 -3
appview/pages/templates/repo/issues/issues.html
··· 4 <div class="flex justify-between items-center"> 5 <p> 6 filtering 7 - <select class="border px-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value"> 8 <option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option> 9 <option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option> 10 </select> ··· 13 <a 14 href="/{{ .RepoInfo.FullName }}/issues/new" 15 class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"> 16 - {{ i "plus" "w-4 h-4" }} 17 - <span>new issue</span> 18 </a> 19 </div> 20 <div class="error" id="issues"></div>
··· 4 <div class="flex justify-between items-center"> 5 <p> 6 filtering 7 + <select class="border p-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value"> 8 <option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option> 9 <option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option> 10 </select> ··· 13 <a 14 href="/{{ .RepoInfo.FullName }}/issues/new" 15 class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"> 16 + {{ i "circle-plus" "w-4 h-4" }} 17 + <span>new</span> 18 </a> 19 </div> 20 <div class="error" id="issues"></div>
+1 -1
appview/pages/templates/repo/log.html
··· 83 <p 84 class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 85 > 86 - {{ nl2br (unwrapText (index $messageParts 1)) }} 87 </p> 88 {{ end }} 89 </div>
··· 83 <p 84 class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 85 > 86 + {{ nl2br (index $messageParts 1) }} 87 </p> 88 {{ end }} 89 </div>
+90
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
··· 1 + {{ define "repo/pulls/fragments/pullActions" }} 2 + {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 3 + {{ $roundNumber := .RoundNumber }} 4 + 5 + {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 6 + {{ $isMerged := .Pull.State.IsMerged }} 7 + {{ $isClosed := .Pull.State.IsClosed }} 8 + {{ $isOpen := .Pull.State.IsOpen }} 9 + {{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }} 10 + {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 11 + {{ $isLastRound := eq $roundNumber $lastIdx }} 12 + {{ $isSameRepoBranch := .Pull.IsBranchBased }} 13 + {{ $isUpToDate := .ResubmitCheck.No }} 14 + <div class="relative w-fit"> 15 + <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2"> 16 + <button 17 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 18 + hx-target="#actions-{{$roundNumber}}" 19 + hx-swap="outerHtml" 20 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline"> 21 + {{ i "message-square-plus" "w-4 h-4" }} 22 + <span>comment</span> 23 + </button> 24 + {{ if and $isPushAllowed $isOpen $isLastRound }} 25 + {{ $disabled := "" }} 26 + {{ if $isConflicted }} 27 + {{ $disabled = "disabled" }} 28 + {{ end }} 29 + <button 30 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 31 + hx-swap="none" 32 + hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 33 + class="btn p-2 flex items-center gap-2" {{ $disabled }}> 34 + {{ i "git-merge" "w-4 h-4" }} 35 + <span>merge</span> 36 + </button> 37 + {{ end }} 38 + 39 + {{ if and $isPullAuthor $isOpen $isLastRound }} 40 + {{ $disabled := "" }} 41 + {{ if $isUpToDate }} 42 + {{ $disabled = "disabled" }} 43 + {{ end }} 44 + <button id="resubmitBtn" 45 + {{ if not .Pull.IsPatchBased }} 46 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 47 + {{ else }} 48 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 49 + hx-target="#actions-{{$roundNumber}}" 50 + hx-swap="outerHtml" 51 + {{ end }} 52 + 53 + hx-disabled-elt="#resubmitBtn" 54 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }} 55 + 56 + {{ if $disabled }} 57 + title="Update this branch to resubmit this pull request" 58 + {{ else }} 59 + title="Resubmit this pull request" 60 + {{ end }} 61 + > 62 + {{ i "rotate-ccw" "w-4 h-4" }} 63 + <span>resubmit</span> 64 + </button> 65 + {{ end }} 66 + 67 + {{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }} 68 + <button 69 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 70 + hx-swap="none" 71 + class="btn p-2 flex items-center gap-2"> 72 + {{ i "ban" "w-4 h-4" }} 73 + <span>close</span> 74 + </button> 75 + {{ end }} 76 + 77 + {{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }} 78 + <button 79 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 80 + hx-swap="none" 81 + class="btn p-2 flex items-center gap-2"> 82 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 83 + <span>reopen</span> 84 + </button> 85 + {{ end }} 86 + </div> 87 + </div> 88 + {{ end }} 89 + 90 +
+20
appview/pages/templates/repo/pulls/fragments/pullCompareBranches.html
···
··· 1 + {{ define "repo/pulls/fragments/pullCompareBranches" }} 2 + <div id="patch-upload"> 3 + <label for="targetBranch" class="dark:text-white" 4 + >select a branch</label 5 + > 6 + <div class="flex flex-wrap gap-2 items-center"> 7 + <select 8 + name="sourceBranch" 9 + class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 10 + > 11 + <option disabled selected>source branch</option> 12 + {{ range .Branches }} 13 + <option value="{{ .Reference.Name }}" class="py-1"> 14 + {{ .Reference.Name }} 15 + </option> 16 + {{ end }} 17 + </select> 18 + </div> 19 + </div> 20 + {{ end }}
+42
appview/pages/templates/repo/pulls/fragments/pullCompareForks.html
···
··· 1 + {{ define "repo/pulls/fragments/pullCompareForks" }} 2 + <div id="patch-upload"> 3 + <label for="forkSelect" class="dark:text-white" 4 + >select a fork to compare</label 5 + > 6 + <div class="flex flex-wrap gap-4 items-center mb-4"> 7 + <div class="flex flex-wrap gap-2 items-center"> 8 + <select 9 + id="forkSelect" 10 + name="fork" 11 + required 12 + class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 13 + hx-get="/{{ $.RepoInfo.FullName }}/pulls/new/fork-branches" 14 + hx-target="#branch-selection" 15 + hx-vals='{"fork": this.value}' 16 + hx-swap="innerHTML" 17 + onchange="document.getElementById('hiddenForkInput').value = this.value;" 18 + > 19 + <option disabled selected>select a fork</option> 20 + {{ range .Forks }} 21 + <option value="{{ .Name }}" class="py-1"> 22 + {{ .Name }} 23 + </option> 24 + {{ end }} 25 + </select> 26 + 27 + <input 28 + type="hidden" 29 + id="hiddenForkInput" 30 + name="fork" 31 + value="" 32 + /> 33 + </div> 34 + 35 + <div id="branch-selection"> 36 + <div class="text-sm text-gray-500 dark:text-gray-400"> 37 + Select a fork first to view available branches 38 + </div> 39 + </div> 40 + </div> 41 + </div> 42 + {{ end }}
+15
appview/pages/templates/repo/pulls/fragments/pullCompareForksBranches.html
···
··· 1 + {{ define "repo/pulls/fragments/pullCompareForksBranches" }} 2 + <div class="flex flex-wrap gap-2 items-center"> 3 + <select 4 + name="sourceBranch" 5 + class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 6 + > 7 + <option disabled selected>source branch</option> 8 + {{ range .SourceBranches }} 9 + <option value="{{ .Reference.Name }}" class="py-1"> 10 + {{ .Reference.Name }} 11 + </option> 12 + {{ end }} 13 + </select> 14 + </div> 15 + {{ end }}
+32
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
···
··· 1 + {{ define "repo/pulls/fragments/pullNewComment" }} 2 + <div 3 + id="pull-comment-card-{{ .RoundNumber }}" 4 + class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 + <div class="text-sm text-gray-500 dark:text-gray-400"> 6 + {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 7 + </div> 8 + <form 9 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 10 + hx-swap="none" 11 + class="w-full flex flex-wrap gap-2"> 12 + <textarea 13 + name="body" 14 + class="w-full p-2 rounded border border-gray-200" 15 + placeholder="Add to the discussion..."></textarea> 16 + <button type="submit" class="btn flex items-center gap-2"> 17 + {{ i "message-square" "w-4 h-4" }} comment 18 + </button> 19 + <button 20 + type="button" 21 + class="btn flex items-center gap-2" 22 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 23 + hx-swap="outerHTML" 24 + hx-target="#pull-comment-card-{{ .RoundNumber }}"> 25 + {{ i "x" "w-4 h-4" }} 26 + <span>cancel</span> 27 + </button> 28 + <div id="pull-comment"></div> 29 + </form> 30 + </div> 31 + {{ end }} 32 +
+14
appview/pages/templates/repo/pulls/fragments/pullPatchUpload.html
···
··· 1 + {{ define "repo/pulls/fragments/pullPatchUpload" }} 2 + <div id="patch-upload"> 3 + <textarea 4 + name="patch" 5 + id="patch" 6 + rows="12" 7 + class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600" 8 + placeholder="diff --git a/file.txt b/file.txt 9 + index 1234567..abcdefg 100644 10 + --- a/file.txt 11 + +++ b/file.txt" 12 + ></textarea> 13 + </div> 14 + {{ end }}
+52
appview/pages/templates/repo/pulls/fragments/pullResubmit.html
···
··· 1 + {{ define "repo/pulls/fragments/pullResubmit" }} 2 + <div 3 + id="resubmit-pull-card" 4 + class="rounded relative border bg-amber-50 dark:bg-amber-900 border-amber-200 dark:border-amber-500 px-6 py-2"> 5 + 6 + <div class="flex items-center gap-2 text-amber-500 dark:text-amber-50"> 7 + {{ i "pencil" "w-4 h-4" }} 8 + <span class="font-medium">resubmit your patch</span> 9 + </div> 10 + 11 + <div class="mt-2 text-sm text-gray-700 dark:text-gray-200"> 12 + You can update this patch to address any reviews. 13 + This will begin a new round of reviews, 14 + but you'll still be able to view your previous submissions and feedback. 15 + </div> 16 + 17 + <div class="mt-4 flex flex-col"> 18 + <form 19 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 20 + hx-swap="none" 21 + class="w-full flex flex-wrap gap-2"> 22 + <textarea 23 + name="patch" 24 + class="w-full p-2 mb-2" 25 + placeholder="Paste your updated patch here." 26 + rows="15" 27 + >{{.Pull.LatestPatch}}</textarea> 28 + <button 29 + type="submit" 30 + class="btn flex items-center gap-2" 31 + {{ if or .Pull.State.IsClosed }} 32 + disabled 33 + {{ end }}> 34 + {{ i "rotate-ccw" "w-4 h-4" }} 35 + <span>resubmit</span> 36 + </button> 37 + <button 38 + type="button" 39 + class="btn flex items-center gap-2" 40 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .Pull.LastRoundNumber }}/actions" 41 + hx-swap="outerHTML" 42 + hx-target="#resubmit-pull-card"> 43 + {{ i "x" "w-4 h-4" }} 44 + <span>cancel</span> 45 + </button> 46 + </form> 47 + 48 + <div id="resubmit-error" class="error"></div> 49 + <div id="resubmit-success" class="success"></div> 50 + </div> 51 + </div> 52 + {{ end }}
+1 -1
appview/pages/templates/repo/pulls/new.html
··· 84 </nav> 85 86 <section id="patch-strategy"> 87 - {{ template "fragments/pullPatchUpload" . }} 88 </section> 89 90 <div class="flex justify-start items-center gap-2 mt-4">
··· 84 </nav> 85 86 <section id="patch-strategy"> 87 + {{ template "repo/pulls/fragments/pullPatchUpload" . }} 88 </section> 89 90 <div class="flex justify-start items-center gap-2 mt-4">
+1 -14
appview/pages/templates/repo/pulls/patch.html
··· 69 {{ end }} 70 </section> 71 72 - <div id="diff-stat"> 73 - <br> 74 - <strong class="text-sm uppercase mb-4">Changed files</strong> 75 - {{ range .Diff.Diff }} 76 - <ul> 77 - {{ if .IsDelete }} 78 - <li><a href="#file-{{ .Name.Old }}">{{ .Name.Old }}</a></li> 79 - {{ else }} 80 - <li><a href="#file-{{ .Name.New }}">{{ .Name.New }}</a></li> 81 - {{ end }} 82 - </ul> 83 - {{ end }} 84 - </div> 85 </div> 86 87 <section> 88 - {{ template "fragments/diff" (list .RepoInfo.FullName .Diff) }} 89 </section> 90 {{ end }}
··· 69 {{ end }} 70 </section> 71 72 </div> 73 74 <section> 75 + {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }} 76 </section> 77 {{ end }}
+7 -7
appview/pages/templates/repo/pulls/pull.html
··· 21 {{ $icon = "git-merge" }} 22 {{ end }} 23 24 - <section> 25 <div class="flex items-center gap-2"> 26 <div 27 id="state" ··· 45 <a href="/{{ .RepoInfo.FullName }}/tree/{{ .Pull.TargetBranch }}" class="no-underline hover:underline">{{ .Pull.TargetBranch }}</a> 46 </span> 47 </span> 48 - {{ if not .Pull.IsPatch }} 49 <span>from 50 - {{ if not .Pull.IsSameRepoBranch }} 51 <a href="/{{ $owner }}/{{ .PullSourceRepo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSourceRepo.Name }}</a> 52 {{ end }} 53 54 {{ $fullRepo := .RepoInfo.FullName }} 55 - {{ if not .Pull.IsSameRepoBranch }} 56 {{ $fullRepo = printf "%s/%s" $owner .PullSourceRepo.Name }} 57 {{ end }} 58 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> ··· 64 </div> 65 66 {{ if .Pull.Body }} 67 - <article id="body" class="mt-2 prose dark:prose-invert"> 68 {{ .Pull.Body | markdown }} 69 </article> 70 {{ end }} ··· 147 {{ end }} 148 149 {{ if $.LoggedInUser }} 150 - {{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }} 151 {{ else }} 152 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white"> 153 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> ··· 156 {{ end }} 157 </div> 158 </details> 159 - <hr class="md:hidden"/> 160 {{ end }} 161 {{ end }} 162 {{ end }}
··· 21 {{ $icon = "git-merge" }} 22 {{ end }} 23 24 + <section class="mt-2"> 25 <div class="flex items-center gap-2"> 26 <div 27 id="state" ··· 45 <a href="/{{ .RepoInfo.FullName }}/tree/{{ .Pull.TargetBranch }}" class="no-underline hover:underline">{{ .Pull.TargetBranch }}</a> 46 </span> 47 </span> 48 + {{ if not .Pull.IsPatchBased }} 49 <span>from 50 + {{ if not .Pull.IsBranchBased }} 51 <a href="/{{ $owner }}/{{ .PullSourceRepo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSourceRepo.Name }}</a> 52 {{ end }} 53 54 {{ $fullRepo := .RepoInfo.FullName }} 55 + {{ if not .Pull.IsBranchBased }} 56 {{ $fullRepo = printf "%s/%s" $owner .PullSourceRepo.Name }} 57 {{ end }} 58 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> ··· 64 </div> 65 66 {{ if .Pull.Body }} 67 + <article id="body" class="mt-8 prose dark:prose-invert"> 68 {{ .Pull.Body | markdown }} 69 </article> 70 {{ end }} ··· 147 {{ end }} 148 149 {{ if $.LoggedInUser }} 150 + {{ template "repo/pulls/fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }} 151 {{ else }} 152 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white"> 153 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> ··· 156 {{ end }} 157 </div> 158 </details> 159 + <hr class="md:hidden border-t border-gray-300 dark:border-gray-600"/> 160 {{ end }} 161 {{ end }} 162 {{ end }}
+10 -6
appview/pages/templates/repo/pulls/pulls.html
··· 5 <p class="dark:text-white"> 6 filtering 7 <select 8 - class="border px-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white" 9 onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value" 10 > 11 <option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}> ··· 22 </p> 23 <a 24 href="/{{ .RepoInfo.FullName }}/pulls/new" 25 - class="btn text-sm flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600" 26 > 27 - {{ i "git-pull-request" "w-4 h-4" }} 28 - <span>new pull request</span> 29 </a> 30 </div> 31 <div class="error" id="pulls"></div> ··· 78 {{ .TargetBranch }} 79 </span> 80 </span> 81 - {{ if not .IsPatch }} 82 <span>from 83 - {{ if not .IsSameRepoBranch }} 84 <a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a> 85 {{ end }} 86 87 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
··· 5 <p class="dark:text-white"> 6 filtering 7 <select 8 + class="border p-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white" 9 onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value" 10 > 11 <option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}> ··· 22 </p> 23 <a 24 href="/{{ .RepoInfo.FullName }}/pulls/new" 25 + class="btn text-sm flex items-center gap-2 no-underline hover:no-underline" 26 > 27 + {{ i "git-pull-request-create" "w-4 h-4" }} 28 + <span>new</span> 29 </a> 30 </div> 31 <div class="error" id="pulls"></div> ··· 78 {{ .TargetBranch }} 79 </span> 80 </span> 81 + {{ if not .IsPatchBased }} 82 <span>from 83 + {{ if .IsForkBased }} 84 + {{ if .PullSource.Repo }} 85 <a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a> 86 + {{ else }} 87 + <span class="italic">[deleted fork]</span> 88 + {{ end }} 89 {{ end }} 90 91 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
+4 -3
appview/pages/templates/repo/tree.html
··· 28 {{ $stats := .TreeStats }} 29 30 <span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span> 31 - <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 32 {{ if eq $stats.NumFolders 1 }} 33 - <span>{{ $stats.NumFolders }} folder</span> 34 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 35 {{ else if gt $stats.NumFolders 1 }} 36 <span>{{ $stats.NumFolders }} folders</span> 37 - <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 38 {{ end }} 39 40 {{ if eq $stats.NumFiles 1 }} 41 <span>{{ $stats.NumFiles }} file</span> 42 {{ else if gt $stats.NumFiles 1 }} 43 <span>{{ $stats.NumFiles }} files</span> 44 {{ end }} 45
··· 28 {{ $stats := .TreeStats }} 29 30 <span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span> 31 {{ if eq $stats.NumFolders 1 }} 32 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 33 + <span>{{ $stats.NumFolders }} folder</span> 34 {{ else if gt $stats.NumFolders 1 }} 35 + <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 36 <span>{{ $stats.NumFolders }} folders</span> 37 {{ end }} 38 39 {{ if eq $stats.NumFiles 1 }} 40 + <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 41 <span>{{ $stats.NumFiles }} file</span> 42 {{ else if gt $stats.NumFiles 1 }} 43 + <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 44 <span>{{ $stats.NumFiles }} files</span> 45 {{ end }} 46
+17
appview/pages/templates/user/fragments/follow.html
···
··· 1 + {{ define "user/fragments/follow" }} 2 + <button id="followBtn" 3 + class="btn mt-2 w-full" 4 + 5 + {{ if eq .FollowStatus.String "IsNotFollowing" }} 6 + hx-post="/follow?subject={{.UserDid}}" 7 + {{ else }} 8 + hx-delete="/follow?subject={{.UserDid}}" 9 + {{ end }} 10 + 11 + hx-trigger="click" 12 + hx-target="#followBtn" 13 + hx-swap="outerHTML" 14 + > 15 + {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }} 16 + </button> 17 + {{ end }}
+1 -1
appview/pages/templates/user/profile.html
··· 247 </div> 248 249 {{ if ne .FollowStatus.String "IsSelf" }} 250 - {{ template "fragments/follow" . }} 251 {{ end }} 252 </div> 253 {{ end }}
··· 247 </div> 248 249 {{ if ne .FollowStatus.String "IsSelf" }} 250 + {{ template "user/fragments/follow" . }} 251 {{ end }} 252 </div> 253 {{ end }}
+347 -194
appview/state/pull.go
··· 369 pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 370 if err != nil { 371 log.Printf("failed to get repo by at uri: %v", err) 372 - return 373 } 374 } 375 - p.PullSource.Repo = pullSourceRepo 376 } 377 } 378 ··· 634 return 635 } 636 637 - resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 638 - switch resp.StatusCode { 639 - case 404: 640 - case 400: 641 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 642 - return 643 - } 644 - 645 - respBody, err := io.ReadAll(resp.Body) 646 if err != nil { 647 - log.Println("failed to compare across branches") 648 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 649 - return 650 - } 651 - defer resp.Body.Close() 652 - 653 - var diffTreeResponse types.RepoDiffTreeResponse 654 - err = json.Unmarshal(respBody, &diffTreeResponse) 655 - if err != nil { 656 - log.Println("failed to unmarshal diff tree response", err) 657 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 658 return 659 } 660 ··· 730 // hiddenRef: hidden/feature-1/main (on repo-fork) 731 // targetBranch: main (on repo-1) 732 // sourceBranch: feature-1 (on repo-fork) 733 - diffResp, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 734 if err != nil { 735 log.Println("failed to compare across branches", err) 736 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 737 - return 738 - } 739 - 740 - respBody, err := io.ReadAll(diffResp.Body) 741 - if err != nil { 742 - log.Println("failed to read response body", err) 743 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 744 - return 745 - } 746 - 747 - defer resp.Body.Close() 748 - 749 - var diffTreeResponse types.RepoDiffTreeResponse 750 - err = json.Unmarshal(respBody, &diffTreeResponse) 751 - if err != nil { 752 - log.Println("failed to unmarshal diff tree response", err) 753 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 754 return 755 } 756 ··· 1015 }) 1016 return 1017 case http.MethodPost: 1018 - patch := r.FormValue("patch") 1019 - var sourceRev string 1020 - var recordPullSource *tangled.RepoPull_Source 1021 1022 - var ownerDid, repoName, knotName string 1023 - var isSameRepo bool = pull.IsSameRepoBranch() 1024 - sourceBranch := pull.PullSource.Branch 1025 - targetBranch := pull.TargetBranch 1026 - recordPullSource = &tangled.RepoPull_Source{ 1027 - Branch: sourceBranch, 1028 - } 1029 1030 - isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 1031 - if isSameRepo && isPushAllowed { 1032 - ownerDid = f.OwnerDid() 1033 - repoName = f.RepoName 1034 - knotName = f.Knot 1035 - } else if !isSameRepo { 1036 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1037 - if err != nil { 1038 - log.Println("failed to get source repo", err) 1039 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1040 - return 1041 - } 1042 - ownerDid = sourceRepo.Did 1043 - repoName = sourceRepo.Name 1044 - knotName = sourceRepo.Knot 1045 - } 1046 1047 - if sourceBranch != "" && knotName != "" { 1048 - // extract patch by performing compare 1049 - ksClient, err := NewUnsignedClient(knotName, s.config.Dev) 1050 - if err != nil { 1051 - log.Printf("failed to create client for %s: %s", knotName, err) 1052 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1053 - return 1054 - } 1055 1056 - if !isSameRepo { 1057 - secret, err := db.GetRegistrationKey(s.db, knotName) 1058 - if err != nil { 1059 - log.Printf("failed to get registration key for %s: %s", knotName, err) 1060 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1061 - return 1062 - } 1063 - // update the hidden tracking branch to latest 1064 - signedClient, err := NewSignedClient(knotName, secret, s.config.Dev) 1065 - if err != nil { 1066 - log.Printf("failed to create signed client for %s: %s", knotName, err) 1067 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1068 - return 1069 - } 1070 - resp, err := signedClient.NewHiddenRef(ownerDid, repoName, sourceBranch, targetBranch) 1071 - if err != nil || resp.StatusCode != http.StatusNoContent { 1072 - log.Printf("failed to update tracking branch: %s", err) 1073 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1074 - return 1075 - } 1076 - } 1077 1078 - var compareResp *http.Response 1079 - if !isSameRepo { 1080 - hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 1081 - compareResp, err = ksClient.Compare(ownerDid, repoName, hiddenRef, sourceBranch) 1082 - } else { 1083 - compareResp, err = ksClient.Compare(ownerDid, repoName, targetBranch, sourceBranch) 1084 - } 1085 - if err != nil { 1086 - log.Printf("failed to compare branches: %s", err) 1087 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1088 - return 1089 - } 1090 - defer compareResp.Body.Close() 1091 1092 - switch compareResp.StatusCode { 1093 - case 404: 1094 - case 400: 1095 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 1096 - return 1097 - } 1098 1099 - respBody, err := io.ReadAll(compareResp.Body) 1100 - if err != nil { 1101 - log.Println("failed to compare across branches") 1102 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1103 - return 1104 - } 1105 - defer compareResp.Body.Close() 1106 1107 - var diffTreeResponse types.RepoDiffTreeResponse 1108 - err = json.Unmarshal(respBody, &diffTreeResponse) 1109 - if err != nil { 1110 - log.Println("failed to unmarshal diff tree response", err) 1111 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1112 - return 1113 - } 1114 1115 - sourceRev = diffTreeResponse.DiffTree.Rev2 1116 - patch = diffTreeResponse.DiffTree.Patch 1117 - } 1118 1119 - if patch == "" { 1120 - s.pages.Notice(w, "resubmit-error", "Patch is empty.") 1121 - return 1122 - } 1123 1124 - if patch == pull.LatestPatch() { 1125 - s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 1126 - return 1127 - } 1128 1129 - if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1130 - s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1131 - return 1132 - } 1133 1134 - if !isPatchValid(patch) { 1135 - s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") 1136 - return 1137 - } 1138 1139 - tx, err := s.db.BeginTx(r.Context(), nil) 1140 - if err != nil { 1141 - log.Println("failed to start tx") 1142 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1143 - return 1144 - } 1145 - defer tx.Rollback() 1146 1147 - err = db.ResubmitPull(tx, pull, patch, sourceRev) 1148 - if err != nil { 1149 - log.Println("failed to create pull request", err) 1150 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1151 - return 1152 - } 1153 - client, _ := s.auth.AuthorizedClient(r) 1154 1155 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1156 - if err != nil { 1157 - // failed to get record 1158 - s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1159 - return 1160 - } 1161 1162 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1163 - Collection: tangled.RepoPullNSID, 1164 - Repo: user.Did, 1165 - Rkey: pull.Rkey, 1166 - SwapRecord: ex.Cid, 1167 - Record: &lexutil.LexiconTypeDecoder{ 1168 - Val: &tangled.RepoPull{ 1169 - Title: pull.Title, 1170 - PullId: int64(pull.PullId), 1171 - TargetRepo: string(f.RepoAt), 1172 - TargetBranch: pull.TargetBranch, 1173 - Patch: patch, // new patch 1174 - Source: recordPullSource, 1175 - }, 1176 }, 1177 - }) 1178 - if err != nil { 1179 - log.Println("failed to update record", err) 1180 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1181 - return 1182 - } 1183 1184 - if err = tx.Commit(); err != nil { 1185 - log.Println("failed to commit transaction", err) 1186 - s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1187 - return 1188 - } 1189 1190 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1191 return 1192 } 1193 } 1194 1195 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
··· 369 pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 370 if err != nil { 371 log.Printf("failed to get repo by at uri: %v", err) 372 + continue 373 + } else { 374 + p.PullSource.Repo = pullSourceRepo 375 } 376 } 377 } 378 } 379 ··· 635 return 636 } 637 638 + diffTreeResponse, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 639 if err != nil { 640 + log.Println("failed to compare", err) 641 + s.pages.Notice(w, "pull", err.Error()) 642 return 643 } 644 ··· 714 // hiddenRef: hidden/feature-1/main (on repo-fork) 715 // targetBranch: main (on repo-1) 716 // sourceBranch: feature-1 (on repo-fork) 717 + diffTreeResponse, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 718 if err != nil { 719 log.Println("failed to compare across branches", err) 720 + s.pages.Notice(w, "pull", err.Error()) 721 return 722 } 723 ··· 982 }) 983 return 984 case http.MethodPost: 985 + if pull.IsPatchBased() { 986 + s.resubmitPatch(w, r) 987 + return 988 + } else if pull.IsBranchBased() { 989 + s.resubmitBranch(w, r) 990 + return 991 + } else if pull.IsForkBased() { 992 + s.resubmitFork(w, r) 993 + return 994 + } 995 + } 996 + } 997 + 998 + func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 999 + user := s.auth.GetUser(r) 1000 1001 + pull, ok := r.Context().Value("pull").(*db.Pull) 1002 + if !ok { 1003 + log.Println("failed to get pull") 1004 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1005 + return 1006 + } 1007 + 1008 + f, err := fullyResolvedRepo(r) 1009 + if err != nil { 1010 + log.Println("failed to get repo and knot", err) 1011 + return 1012 + } 1013 + 1014 + if user.Did != pull.OwnerDid { 1015 + log.Println("unauthorized user") 1016 + w.WriteHeader(http.StatusUnauthorized) 1017 + return 1018 + } 1019 + 1020 + patch := r.FormValue("patch") 1021 + 1022 + if err = validateResubmittedPatch(pull, patch); err != nil { 1023 + s.pages.Notice(w, "resubmit-error", err.Error()) 1024 + } 1025 + 1026 + tx, err := s.db.BeginTx(r.Context(), nil) 1027 + if err != nil { 1028 + log.Println("failed to start tx") 1029 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1030 + return 1031 + } 1032 + defer tx.Rollback() 1033 + 1034 + err = db.ResubmitPull(tx, pull, patch, "") 1035 + if err != nil { 1036 + log.Println("failed to resubmit pull request", err) 1037 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1038 + return 1039 + } 1040 + client, _ := s.auth.AuthorizedClient(r) 1041 + 1042 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1043 + if err != nil { 1044 + // failed to get record 1045 + s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1046 + return 1047 + } 1048 + 1049 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1050 + Collection: tangled.RepoPullNSID, 1051 + Repo: user.Did, 1052 + Rkey: pull.Rkey, 1053 + SwapRecord: ex.Cid, 1054 + Record: &lexutil.LexiconTypeDecoder{ 1055 + Val: &tangled.RepoPull{ 1056 + Title: pull.Title, 1057 + PullId: int64(pull.PullId), 1058 + TargetRepo: string(f.RepoAt), 1059 + TargetBranch: pull.TargetBranch, 1060 + Patch: patch, // new patch 1061 + }, 1062 + }, 1063 + }) 1064 + if err != nil { 1065 + log.Println("failed to update record", err) 1066 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1067 + return 1068 + } 1069 1070 + if err = tx.Commit(); err != nil { 1071 + log.Println("failed to commit transaction", err) 1072 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1073 + return 1074 + } 1075 1076 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1077 + return 1078 + } 1079 1080 + func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1081 + user := s.auth.GetUser(r) 1082 1083 + pull, ok := r.Context().Value("pull").(*db.Pull) 1084 + if !ok { 1085 + log.Println("failed to get pull") 1086 + s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1087 + return 1088 + } 1089 1090 + f, err := fullyResolvedRepo(r) 1091 + if err != nil { 1092 + log.Println("failed to get repo and knot", err) 1093 + return 1094 + } 1095 1096 + if user.Did != pull.OwnerDid { 1097 + log.Println("unauthorized user") 1098 + w.WriteHeader(http.StatusUnauthorized) 1099 + return 1100 + } 1101 1102 + if !f.RepoInfo(s, user).Roles.IsPushAllowed() { 1103 + log.Println("unauthorized user") 1104 + w.WriteHeader(http.StatusUnauthorized) 1105 + return 1106 + } 1107 1108 + ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1109 + if err != nil { 1110 + log.Printf("failed to create client for %s: %s", f.Knot, err) 1111 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1112 + return 1113 + } 1114 1115 + diffTreeResponse, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1116 + if err != nil { 1117 + log.Printf("compare request failed: %s", err) 1118 + s.pages.Notice(w, "resubmit-error", err.Error()) 1119 + return 1120 + } 1121 1122 + sourceRev := diffTreeResponse.DiffTree.Rev2 1123 + patch := diffTreeResponse.DiffTree.Patch 1124 1125 + if err = validateResubmittedPatch(pull, patch); err != nil { 1126 + s.pages.Notice(w, "resubmit-error", err.Error()) 1127 + } 1128 1129 + if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1130 + s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1131 + return 1132 + } 1133 1134 + tx, err := s.db.BeginTx(r.Context(), nil) 1135 + if err != nil { 1136 + log.Println("failed to start tx") 1137 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1138 + return 1139 + } 1140 + defer tx.Rollback() 1141 1142 + err = db.ResubmitPull(tx, pull, patch, sourceRev) 1143 + if err != nil { 1144 + log.Println("failed to create pull request", err) 1145 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1146 + return 1147 + } 1148 + client, _ := s.auth.AuthorizedClient(r) 1149 1150 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1151 + if err != nil { 1152 + // failed to get record 1153 + s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1154 + return 1155 + } 1156 1157 + recordPullSource := &tangled.RepoPull_Source{ 1158 + Branch: pull.PullSource.Branch, 1159 + } 1160 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1161 + Collection: tangled.RepoPullNSID, 1162 + Repo: user.Did, 1163 + Rkey: pull.Rkey, 1164 + SwapRecord: ex.Cid, 1165 + Record: &lexutil.LexiconTypeDecoder{ 1166 + Val: &tangled.RepoPull{ 1167 + Title: pull.Title, 1168 + PullId: int64(pull.PullId), 1169 + TargetRepo: string(f.RepoAt), 1170 + TargetBranch: pull.TargetBranch, 1171 + Patch: patch, // new patch 1172 + Source: recordPullSource, 1173 }, 1174 + }, 1175 + }) 1176 + if err != nil { 1177 + log.Println("failed to update record", err) 1178 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1179 + return 1180 + } 1181 + 1182 + if err = tx.Commit(); err != nil { 1183 + log.Println("failed to commit transaction", err) 1184 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1185 + return 1186 + } 1187 + 1188 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1189 + return 1190 + } 1191 + 1192 + func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1193 + user := s.auth.GetUser(r) 1194 1195 + pull, ok := r.Context().Value("pull").(*db.Pull) 1196 + if !ok { 1197 + log.Println("failed to get pull") 1198 + s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1199 + return 1200 + } 1201 1202 + f, err := fullyResolvedRepo(r) 1203 + if err != nil { 1204 + log.Println("failed to get repo and knot", err) 1205 return 1206 } 1207 + 1208 + if user.Did != pull.OwnerDid { 1209 + log.Println("unauthorized user") 1210 + w.WriteHeader(http.StatusUnauthorized) 1211 + return 1212 + } 1213 + 1214 + forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1215 + if err != nil { 1216 + log.Println("failed to get source repo", err) 1217 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1218 + return 1219 + } 1220 + 1221 + // extract patch by performing compare 1222 + ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1223 + if err != nil { 1224 + log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1225 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1226 + return 1227 + } 1228 + 1229 + secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1230 + if err != nil { 1231 + log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1232 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1233 + return 1234 + } 1235 + 1236 + // update the hidden tracking branch to latest 1237 + signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1238 + if err != nil { 1239 + log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1240 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1241 + return 1242 + } 1243 + 1244 + resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1245 + if err != nil || resp.StatusCode != http.StatusNoContent { 1246 + log.Printf("failed to update tracking branch: %s", err) 1247 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1248 + return 1249 + } 1250 + 1251 + hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)) 1252 + diffTreeResponse, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1253 + if err != nil { 1254 + log.Printf("failed to compare branches: %s", err) 1255 + s.pages.Notice(w, "resubmit-error", err.Error()) 1256 + return 1257 + } 1258 + 1259 + sourceRev := diffTreeResponse.DiffTree.Rev2 1260 + patch := diffTreeResponse.DiffTree.Patch 1261 + 1262 + if err = validateResubmittedPatch(pull, patch); err != nil { 1263 + s.pages.Notice(w, "resubmit-error", err.Error()) 1264 + } 1265 + 1266 + if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1267 + s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1268 + return 1269 + } 1270 + 1271 + tx, err := s.db.BeginTx(r.Context(), nil) 1272 + if err != nil { 1273 + log.Println("failed to start tx") 1274 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1275 + return 1276 + } 1277 + defer tx.Rollback() 1278 + 1279 + err = db.ResubmitPull(tx, pull, patch, sourceRev) 1280 + if err != nil { 1281 + log.Println("failed to create pull request", err) 1282 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1283 + return 1284 + } 1285 + client, _ := s.auth.AuthorizedClient(r) 1286 + 1287 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1288 + if err != nil { 1289 + // failed to get record 1290 + s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1291 + return 1292 + } 1293 + 1294 + repoAt := pull.PullSource.RepoAt.String() 1295 + recordPullSource := &tangled.RepoPull_Source{ 1296 + Branch: pull.PullSource.Branch, 1297 + Repo: &repoAt, 1298 + } 1299 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1300 + Collection: tangled.RepoPullNSID, 1301 + Repo: user.Did, 1302 + Rkey: pull.Rkey, 1303 + SwapRecord: ex.Cid, 1304 + Record: &lexutil.LexiconTypeDecoder{ 1305 + Val: &tangled.RepoPull{ 1306 + Title: pull.Title, 1307 + PullId: int64(pull.PullId), 1308 + TargetRepo: string(f.RepoAt), 1309 + TargetBranch: pull.TargetBranch, 1310 + Patch: patch, // new patch 1311 + Source: recordPullSource, 1312 + }, 1313 + }, 1314 + }) 1315 + if err != nil { 1316 + log.Println("failed to update record", err) 1317 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1318 + return 1319 + } 1320 + 1321 + if err = tx.Commit(); err != nil { 1322 + log.Println("failed to commit transaction", err) 1323 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1324 + return 1325 + } 1326 + 1327 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1328 + return 1329 + } 1330 + 1331 + // validate a resubmission against a pull request 1332 + func validateResubmittedPatch(pull *db.Pull, patch string) error { 1333 + if patch == "" { 1334 + return fmt.Errorf("Patch is empty.") 1335 + } 1336 + 1337 + if patch == pull.LatestPatch() { 1338 + return fmt.Errorf("Patch is identical to previous submission.") 1339 + } 1340 + 1341 + if !isPatchValid(patch) { 1342 + return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1343 + } 1344 + 1345 + return nil 1346 } 1347 1348 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
+7
appview/state/repo.go
··· 21 "github.com/bluesky-social/indigo/atproto/syntax" 22 securejoin "github.com/cyphar/filepath-securejoin" 23 "github.com/go-chi/chi/v5" 24 "tangled.sh/tangled.sh/core/api/tangled" 25 "tangled.sh/tangled.sh/core/appview/auth" 26 "tangled.sh/tangled.sh/core/appview/db" ··· 248 if !s.config.Dev { 249 protocol = "https" 250 } 251 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 252 if err != nil { 253 log.Println("failed to reach knotserver", err)
··· 21 "github.com/bluesky-social/indigo/atproto/syntax" 22 securejoin "github.com/cyphar/filepath-securejoin" 23 "github.com/go-chi/chi/v5" 24 + "github.com/go-git/go-git/v5/plumbing" 25 "tangled.sh/tangled.sh/core/api/tangled" 26 "tangled.sh/tangled.sh/core/appview/auth" 27 "tangled.sh/tangled.sh/core/appview/db" ··· 249 if !s.config.Dev { 250 protocol = "https" 251 } 252 + 253 + if !plumbing.IsHash(ref) { 254 + s.pages.Error404(w) 255 + return 256 + } 257 + 258 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 259 if err != nil { 260 log.Println("failed to reach knotserver", err)
+31 -3
appview/state/signer.go
··· 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "net/url" 12 "time" ··· 376 return &capabilities, nil 377 } 378 379 - func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*http.Response, error) { 380 const ( 381 Method = "GET" 382 ) ··· 385 386 req, err := us.newRequest(Method, endpoint, nil) 387 if err != nil { 388 - return nil, err 389 } 390 391 - return us.client.Do(req) 392 }
··· 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 + "io" 11 + "log" 12 "net/http" 13 "net/url" 14 "time" ··· 378 return &capabilities, nil 379 } 380 381 + func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoDiffTreeResponse, error) { 382 const ( 383 Method = "GET" 384 ) ··· 387 388 req, err := us.newRequest(Method, endpoint, nil) 389 if err != nil { 390 + return nil, fmt.Errorf("Failed to create request.") 391 } 392 393 + compareResp, err := us.client.Do(req) 394 + if err != nil { 395 + return nil, fmt.Errorf("Failed to create request.") 396 + } 397 + defer compareResp.Body.Close() 398 + 399 + switch compareResp.StatusCode { 400 + case 404: 401 + case 400: 402 + return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 403 + } 404 + 405 + respBody, err := io.ReadAll(compareResp.Body) 406 + if err != nil { 407 + log.Println("failed to compare across branches") 408 + return nil, fmt.Errorf("Failed to compare branches.") 409 + } 410 + defer compareResp.Body.Close() 411 + 412 + var diffTreeResponse types.RepoDiffTreeResponse 413 + err = json.Unmarshal(respBody, &diffTreeResponse) 414 + if err != nil { 415 + log.Println("failed to unmarshal diff tree response", err) 416 + return nil, fmt.Errorf("Failed to compare branches.") 417 + } 418 + 419 + return &diffTreeResponse, nil 420 }
+1 -1
docs/knot-hosting.md
··· 26 docker compose -f docker/docker-compose.yml up 27 ``` 28 29 - ### manual setup 30 31 First, clone this repository: 32
··· 26 docker compose -f docker/docker-compose.yml up 27 ``` 28 29 + ## manual setup 30 31 First, clone this repository: 32
+1 -1
flake.nix
··· 420 g = config.services.tangled-knotserver.gitUser; 421 in [ 422 "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 423 - "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=6995e040e80e2d593b5e5e9ca611a70140b9ef8044add0a28b48b1ee34aa3e85" 424 ]; 425 services.tangled-knotserver = { 426 enable = true;
··· 420 g = config.services.tangled-knotserver.gitUser; 421 in [ 422 "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 423 + "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2" 424 ]; 425 services.tangled-knotserver = { 426 enable = true;
+1 -17
rbac/rbac.go
··· 3 import ( 4 "database/sql" 5 "fmt" 6 - "path" 7 "strings" 8 9 adapter "github.com/Blank-Xu/sql-adapter" ··· 26 e = some(where (p.eft == allow)) 27 28 [matchers] 29 - m = r.act == p.act && r.dom == p.dom && keyMatch2(r.obj, p.obj) && g(r.sub, p.sub, r.dom) 30 ` 31 ) 32 ··· 34 E *casbin.Enforcer 35 } 36 37 - func keyMatch2(key1 string, key2 string) bool { 38 - matched, _ := path.Match(key2, key1) 39 - return matched 40 - } 41 - 42 func NewEnforcer(path string) (*Enforcer, error) { 43 m, err := model.NewModelFromString(Model) 44 if err != nil { ··· 61 } 62 63 e.EnableAutoSave(false) 64 - 65 - e.AddFunction("keyMatch2", keyMatch2Func) 66 67 return &Enforcer{e}, nil 68 } ··· 209 } 210 211 return permissions 212 - } 213 - 214 - // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 215 - func keyMatch2Func(args ...interface{}) (interface{}, error) { 216 - name1 := args[0].(string) 217 - name2 := args[1].(string) 218 - 219 - return keyMatch2(name1, name2), nil 220 } 221 222 func checkRepoFormat(repo string) error {
··· 3 import ( 4 "database/sql" 5 "fmt" 6 "strings" 7 8 adapter "github.com/Blank-Xu/sql-adapter" ··· 25 e = some(where (p.eft == allow)) 26 27 [matchers] 28 + m = r.act == p.act && r.dom == p.dom && r.obj == p.obj && g(r.sub, p.sub, r.dom) 29 ` 30 ) 31 ··· 33 E *casbin.Enforcer 34 } 35 36 func NewEnforcer(path string) (*Enforcer, error) { 37 m, err := model.NewModelFromString(Model) 38 if err != nil { ··· 55 } 56 57 e.EnableAutoSave(false) 58 59 return &Enforcer{e}, nil 60 } ··· 201 } 202 203 return permissions 204 } 205 206 func checkRepoFormat(repo string) error {
+9 -3
readme.md
··· 6 7 Read the introduction to Tangled [here](https://blog.tangled.sh/intro). 8 9 - Documentation: 10 * [knot hosting 11 - guide](https://tangled.sh/@tangled.sh/core/tree/master/docs/knot-hosting.md) 12 * [contributing 13 - guide](https://tangled.sh/@tangled.sh/core/tree/master/docs/contributing.md)&mdash;**read this before opening a PR!**
··· 6 7 Read the introduction to Tangled [here](https://blog.tangled.sh/intro). 8 9 + ## docs 10 + 11 * [knot hosting 12 + guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md) 13 * [contributing 14 + guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/contributing.md)&mdash;**read this before opening a PR!** 15 + 16 + ## security 17 + 18 + If you've identified a security issue in Tangled, please email 19 + [security@tangled.sh](mailto:security@tangled.sh) with details!
+68 -45
tailwind.config.js
··· 2 const colors = require("tailwindcss/colors"); 3 4 module.exports = { 5 - content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go"], 6 - darkMode: "media", 7 - theme: { 8 - container: { 9 - padding: "2rem", 10 - center: true, 11 - screens: { 12 - sm: "500px", 13 - md: "600px", 14 - lg: "800px", 15 - xl: "1000px", 16 - "2xl": "1200px", 17 - }, 18 - }, 19 - extend: { 20 - fontFamily: { 21 - sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"], 22 - mono: [ 23 - "IBMPlexMono", 24 - "ui-monospace", 25 - "SFMono-Regular", 26 - "Menlo", 27 - "Monaco", 28 - "Consolas", 29 - "Liberation Mono", 30 - "Courier New", 31 - "monospace", 32 - ], 33 - }, 34 - typography: { 35 - DEFAULT: { 36 - css: { 37 - maxWidth: "none", 38 - pre: { 39 - backgroundColor: colors.gray[100], 40 - color: colors.black, 41 - "@apply dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": 42 - {}, 43 - }, 44 - }, 45 - }, 46 - }, 47 - }, 48 - }, 49 - plugins: [require("@tailwindcss/typography")], 50 };
··· 2 const colors = require("tailwindcss/colors"); 3 4 module.exports = { 5 + content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go"], 6 + darkMode: "media", 7 + theme: { 8 + container: { 9 + padding: "2rem", 10 + center: true, 11 + screens: { 12 + sm: "500px", 13 + md: "600px", 14 + lg: "800px", 15 + xl: "1000px", 16 + "2xl": "1200px", 17 + }, 18 + }, 19 + extend: { 20 + fontFamily: { 21 + sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"], 22 + mono: [ 23 + "IBMPlexMono", 24 + "ui-monospace", 25 + "SFMono-Regular", 26 + "Menlo", 27 + "Monaco", 28 + "Consolas", 29 + "Liberation Mono", 30 + "Courier New", 31 + "monospace", 32 + ], 33 + }, 34 + typography: { 35 + DEFAULT: { 36 + css: { 37 + maxWidth: "none", 38 + pre: { 39 + backgroundColor: colors.gray[100], 40 + color: colors.black, 41 + "@apply font-normal text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": {}, 42 + }, 43 + code: { 44 + "@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {}, 45 + }, 46 + "code::before": { 47 + content: '""', 48 + }, 49 + "code::after": { 50 + content: '""', 51 + }, 52 + blockquote: { 53 + quotes: "none", 54 + }, 55 + 'h1, h2, h3, h4': { 56 + "@apply mt-4 mb-2": {} 57 + }, 58 + h1: { 59 + "@apply mt-3 pb-3 border-b border-gray-300 dark:border-gray-600": {} 60 + }, 61 + h2: { 62 + "@apply mt-3 pb-3 border-b border-gray-200 dark:border-gray-700": {} 63 + }, 64 + h3: { 65 + "@apply mt-2": {} 66 + }, 67 + }, 68 + }, 69 + }, 70 + }, 71 + }, 72 + plugins: [require("@tailwindcss/typography")], 73 };
+14
types/diff.go
··· 23 IsRename bool `json:"is_rename"` 24 } 25 26 // A nicer git diff representation. 27 type NiceDiff struct { 28 Commit struct {
··· 23 IsRename bool `json:"is_rename"` 24 } 25 26 + type DiffStat struct { 27 + Insertions int64 28 + Deletions int64 29 + } 30 + 31 + func (d *Diff) Stats() DiffStat { 32 + var stats DiffStat 33 + for _, f := range d.TextFragments { 34 + stats.Insertions += f.LinesAdded 35 + stats.Deletions += f.LinesDeleted 36 + } 37 + return stats 38 + } 39 + 40 // A nicer git diff representation. 41 type NiceDiff struct { 42 Commit struct {