+13
-3
appview/db/pulls.go
+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
+4
appview/pages/funcmap.go
+80
-41
appview/pages/pages.go
+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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
+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
+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
+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
+1
-1
appview/pages/templates/repo/empty.html
+51
appview/pages/templates/repo/fragments/cloneInstructions.html
+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
+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">···</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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
appview/pages/templates/repo/log.html
+90
appview/pages/templates/repo/pulls/fragments/pullActions.html
+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
+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
+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
+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
+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
+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
+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
+1
-1
appview/pages/templates/repo/pulls/new.html
+1
-14
appview/pages/templates/repo/pulls/patch.html
+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 }}
+7
-7
appview/pages/templates/repo/pulls/pull.html
+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
+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
+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
+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
+1
-1
appview/pages/templates/user/profile.html
+347
-194
appview/state/pull.go
+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
+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
+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
+1
-1
docs/knot-hosting.md
+1
-1
flake.nix
+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
+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
+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)—**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)—**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
+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
+14
types/diff.go
···
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 {