+5
appview/db/pulls.go
+5
appview/db/pulls.go
···
233
233
return err
234
234
}
235
235
236
+
func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error {
237
+
_, err := e.Exec(`update pulls set open = 2 where repo_at = ? and pull_id = ?`, repoAt, pullId)
238
+
return err
239
+
}
240
+
236
241
type PullCount struct {
237
242
Open int
238
243
Closed int
+17
-6
appview/pages/pages.go
+17
-6
appview/pages/pages.go
···
531
531
}
532
532
533
533
type RepoSinglePullParams struct {
534
-
LoggedInUser *auth.User
535
-
RepoInfo RepoInfo
536
-
DidHandleMap map[string]string
537
-
Pull db.Pull
538
-
Comments []db.PullComment
539
-
Active string
534
+
LoggedInUser *auth.User
535
+
RepoInfo RepoInfo
536
+
DidHandleMap map[string]string
537
+
Pull db.Pull
538
+
State string
539
+
PullOwnerHandle string
540
+
Comments []db.PullComment
541
+
Active string
542
+
MergeCheck types.MergeCheckResponse
540
543
}
541
544
542
545
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
546
+
switch params.Pull.Open {
547
+
case 0:
548
+
params.State = "close"
549
+
case 1:
550
+
params.State = "open"
551
+
case 2:
552
+
params.State = "merged"
553
+
}
543
554
params.Active = "pulls"
544
555
return p.executeRepo("repo/pulls/pull", w, params)
545
556
}
+16
-7
appview/pages/templates/repo/pulls/new.html
+16
-7
appview/pages/templates/repo/pulls/new.html
···
10
10
<div>
11
11
<label for="title">title</label>
12
12
<input type="text" name="title" id="title" class="w-full" />
13
+
14
+
<label for="targetBranch">target branch</label>
15
+
<p class="text-gray-500">
16
+
The branch you want to make your change against.
17
+
</p>
13
18
<input type="text" name="targetBranch" id="targetBranch" />
14
19
</div>
15
20
<div>
···
21
26
class="w-full resize-y"
22
27
placeholder="Describe your change. Markdown is supported."
23
28
></textarea>
24
-
<textarea
25
-
name="patch"
26
-
id="patch"
27
-
rows="10"
28
-
class="w-full resize-y font-mono"
29
-
placeholder="Paste your git-format-patch output here."
30
-
></textarea>
29
+
30
+
<div class="mt-4">
31
+
<label for="patch">paste your patch here</label>
32
+
<textarea
33
+
name="patch"
34
+
id="patch"
35
+
rows="10"
36
+
class="w-full resize-y font-mono"
37
+
placeholder="Paste your git-format-patch output here."
38
+
></textarea>
39
+
</div>
31
40
</div>
32
41
<div>
33
42
<button type="submit" class="btn">create</button>
+47
-2
appview/pages/templates/repo/pulls/pull.html
+47
-2
appview/pages/templates/repo/pulls/pull.html
···
1
1
{{ define "title" }}
2
-
{{ .Pull.Title }} ·
2
+
{{ .Pull.Title }} · pull #{{ .Pull.PullId }} ·
3
3
{{ .RepoInfo.FullName }}
4
4
{{ end }}
5
5
···
8
8
{{ .Pull.Title }}
9
9
<span class="text-gray-400">#{{ .Pull.PullId }}</span>
10
10
</h1>
11
-
12
11
{{ $bgColor := "bg-gray-800" }}
13
12
{{ $icon := "ban" }}
14
13
{{ if eq .State "open" }}
15
14
{{ $bgColor = "bg-green-600" }}
16
15
{{ $icon = "circle-dot" }}
16
+
{{ else if eq .State "merged" }}
17
+
{{ $bgColor = "bg-purple-600" }}
18
+
{{ $icon = "git-merge" }}
17
19
{{ end }}
18
20
19
21
···
46
48
</article>
47
49
{{ end }}
48
50
</section>
51
+
52
+
<div>
53
+
<details>
54
+
<summary
55
+
class="list-none cursor-pointer sticky top-0 bg-white rounded-sm px-3 py-2 border border-gray-200 flex items-center text-gray-700 hover:bg-gray-50 transition-colors"
56
+
>
57
+
<i data-lucide="code" class="w-4 h-4 mr-2"></i>
58
+
<span>patch</span>
59
+
</summary>
60
+
<pre class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm">
61
+
{{- .Pull.Patch -}}
62
+
</pre>
63
+
</details>
64
+
</div>
65
+
66
+
<div class="mt-4">
67
+
{{ if .MergeCheck }}
68
+
<div class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }}bg-red-50 border-red-200{{ else }}bg-green-50 border-green-200{{ end }}">
69
+
<div class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }}text-red-500{{ else }}text-green-500 {{ end }}">
70
+
{{ if .MergeCheck.IsConflicted }}
71
+
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
72
+
<span class="font-medium">merge conflicts detected</span>
73
+
{{ else }}
74
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
75
+
<span class="font-medium">ready to merge</span>
76
+
{{ end }}
77
+
</div>
78
+
79
+
{{ if .MergeCheck.IsConflicted }}
80
+
<div class="mt-2">
81
+
<ul class="text-sm space-y-1">
82
+
{{ range .MergeCheck.Conflicts }}
83
+
<li class="flex items-center">
84
+
<i data-lucide="file-warning" class="w-3 h-3 mr-1.5 text-red-500"></i>
85
+
<span class="font-mono">{{ slice .Filename 0 (sub (len .Filename) 2) }}</span>
86
+
</li>
87
+
{{ end }}
88
+
</ul>
89
+
</div>
90
+
{{ end }}
91
+
</div>
92
+
{{ end }}
93
+
</div>
49
94
{{ end }}
50
95
51
96
{{ define "repoAfter" }}
+4
-2
appview/pages/templates/settings.html
+4
-2
appview/pages/templates/settings.html
···
12
12
{{ end }}
13
13
14
14
{{ define "profile" }}
15
-
<header class="text-sm font-bold py-2 px-6 uppercase">profile</header>
15
+
<<<<<<< HEAD
16
+
<h2 class="text-sm font-bold py-2 px-6 uppercase">profile</h2>
16
17
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
17
18
<dl class="grid grid-cols-[auto_1fr] gap-x-4">
18
19
{{ if .LoggedInUser.Handle }}
···
28
29
{{ end }}
29
30
30
31
{{ define "keys" }}
31
-
<header class="text-sm font-bold py-2 px-6 uppercase">ssh keys</header>
32
+
<<<<<<< HEAD
33
+
<h2 class="text-sm font-bold py-2 px-6 uppercase">ssh keys</h2>
32
34
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
33
35
<div id="key-list" class="flex flex-col gap-6 mb-8">
34
36
{{ range .PubKeys }}
+40
-6
appview/state/repo.go
+40
-6
appview/state/repo.go
···
404
404
return
405
405
}
406
406
407
+
pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid)
408
+
if err != nil {
409
+
log.Println("failed to resolve pull owner", err)
410
+
}
411
+
407
412
identsToResolve := make([]string, len(comments))
408
413
for i, comment := range comments {
409
414
identsToResolve[i] = comment.OwnerDid
···
418
423
}
419
424
}
420
425
421
-
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
422
-
LoggedInUser: user,
423
-
RepoInfo: f.RepoInfo(s, user),
424
-
Pull: *pr,
425
-
Comments: comments,
426
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
427
+
if err != nil {
428
+
log.Printf("failed to get registration key for %s", f.Knot)
429
+
s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
430
+
return
431
+
}
426
432
427
-
DidHandleMap: didHandleMap,
433
+
var mergeCheckResponse types.MergeCheckResponse
434
+
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
435
+
if err == nil {
436
+
resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch)
437
+
if err != nil {
438
+
log.Println("failed to check for mergeability:", err)
439
+
} else {
440
+
respBody, err := io.ReadAll(resp.Body)
441
+
if err != nil {
442
+
log.Println("failed to read merge check response body")
443
+
} else {
444
+
err = json.Unmarshal(respBody, &mergeCheckResponse)
445
+
if err != nil {
446
+
log.Println("failed to unmarshal merge check response", err)
447
+
}
448
+
}
449
+
}
450
+
} else {
451
+
log.Printf("failed to setup signed client for %s; ignoring...", f.Knot)
452
+
}
453
+
454
+
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
455
+
LoggedInUser: user,
456
+
RepoInfo: f.RepoInfo(s, user),
457
+
Pull: *pr,
458
+
Comments: comments,
459
+
PullOwnerHandle: pullOwnerIdent.Handle.String(),
460
+
DidHandleMap: didHandleMap,
461
+
MergeCheck: mergeCheckResponse,
428
462
})
429
463
}
430
464
+164
appview/state/router.go
+164
appview/state/router.go
···
1
+
package state
2
+
3
+
import (
4
+
"net/http"
5
+
"strings"
6
+
7
+
"github.com/go-chi/chi/v5"
8
+
)
9
+
10
+
func (s *State) Router() http.Handler {
11
+
router := chi.NewRouter()
12
+
13
+
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
14
+
pat := chi.URLParam(r, "*")
15
+
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
16
+
s.UserRouter().ServeHTTP(w, r)
17
+
} else {
18
+
s.StandardRouter().ServeHTTP(w, r)
19
+
}
20
+
})
21
+
22
+
return router
23
+
}
24
+
25
+
func (s *State) UserRouter() http.Handler {
26
+
r := chi.NewRouter()
27
+
28
+
// strip @ from user
29
+
r.Use(StripLeadingAt)
30
+
31
+
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
32
+
r.Get("/", s.ProfilePage)
33
+
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
34
+
r.Get("/", s.RepoIndex)
35
+
r.Get("/commits/{ref}", s.RepoLog)
36
+
r.Route("/tree/{ref}", func(r chi.Router) {
37
+
r.Get("/", s.RepoIndex)
38
+
r.Get("/*", s.RepoTree)
39
+
})
40
+
r.Get("/commit/{ref}", s.RepoCommit)
41
+
r.Get("/branches", s.RepoBranches)
42
+
r.Get("/tags", s.RepoTags)
43
+
r.Get("/blob/{ref}/*", s.RepoBlob)
44
+
45
+
r.Route("/issues", func(r chi.Router) {
46
+
r.Get("/", s.RepoIssues)
47
+
r.Get("/{issue}", s.RepoSingleIssue)
48
+
49
+
r.Group(func(r chi.Router) {
50
+
r.Use(AuthMiddleware(s))
51
+
r.Get("/new", s.NewIssue)
52
+
r.Post("/new", s.NewIssue)
53
+
r.Post("/{issue}/comment", s.IssueComment)
54
+
r.Post("/{issue}/close", s.CloseIssue)
55
+
r.Post("/{issue}/reopen", s.ReopenIssue)
56
+
})
57
+
})
58
+
59
+
r.Route("/pulls", func(r chi.Router) {
60
+
r.Get("/", s.RepoPulls)
61
+
r.Get("/{pull}", s.RepoSinglePull)
62
+
63
+
r.Group(func(r chi.Router) {
64
+
r.Use(AuthMiddleware(s))
65
+
r.Get("/new", s.NewPull)
66
+
r.Post("/new", s.NewPull)
67
+
// r.Post("/{pull}/comment", s.PullComment)
68
+
// r.Post("/{pull}/close", s.ClosePull)
69
+
// r.Post("/{pull}/reopen", s.ReopenPull)
70
+
// r.Post("/{pull}/merge", s.MergePull)
71
+
})
72
+
})
73
+
74
+
// These routes get proxied to the knot
75
+
r.Get("/info/refs", s.InfoRefs)
76
+
r.Post("/git-upload-pack", s.UploadPack)
77
+
78
+
// settings routes, needs auth
79
+
r.Group(func(r chi.Router) {
80
+
r.Use(AuthMiddleware(s))
81
+
// repo description can only be edited by owner
82
+
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
83
+
r.Put("/", s.RepoDescription)
84
+
r.Get("/", s.RepoDescription)
85
+
r.Get("/edit", s.RepoDescriptionEdit)
86
+
})
87
+
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
88
+
r.Get("/", s.RepoSettings)
89
+
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
90
+
})
91
+
})
92
+
})
93
+
})
94
+
95
+
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
96
+
s.pages.Error404(w)
97
+
})
98
+
99
+
return r
100
+
}
101
+
102
+
func (s *State) StandardRouter() http.Handler {
103
+
r := chi.NewRouter()
104
+
105
+
r.Handle("/static/*", s.pages.Static())
106
+
107
+
r.Get("/", s.Timeline)
108
+
109
+
r.With(AuthMiddleware(s)).Get("/logout", s.Logout)
110
+
111
+
r.Route("/login", func(r chi.Router) {
112
+
r.Get("/", s.Login)
113
+
r.Post("/", s.Login)
114
+
})
115
+
116
+
r.Route("/knots", func(r chi.Router) {
117
+
r.Use(AuthMiddleware(s))
118
+
r.Get("/", s.Knots)
119
+
r.Post("/key", s.RegistrationKey)
120
+
121
+
r.Route("/{domain}", func(r chi.Router) {
122
+
r.Post("/init", s.InitKnotServer)
123
+
r.Get("/", s.KnotServerInfo)
124
+
r.Route("/member", func(r chi.Router) {
125
+
r.Use(RoleMiddleware(s, "server:owner"))
126
+
r.Get("/", s.ListMembers)
127
+
r.Put("/", s.AddMember)
128
+
r.Delete("/", s.RemoveMember)
129
+
})
130
+
})
131
+
})
132
+
133
+
r.Route("/repo", func(r chi.Router) {
134
+
r.Route("/new", func(r chi.Router) {
135
+
r.Use(AuthMiddleware(s))
136
+
r.Get("/", s.NewRepo)
137
+
r.Post("/", s.NewRepo)
138
+
})
139
+
// r.Post("/import", s.ImportRepo)
140
+
})
141
+
142
+
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
143
+
r.Post("/", s.Follow)
144
+
r.Delete("/", s.Follow)
145
+
})
146
+
147
+
r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
148
+
r.Post("/", s.Star)
149
+
r.Delete("/", s.Star)
150
+
})
151
+
152
+
r.Route("/settings", func(r chi.Router) {
153
+
r.Use(AuthMiddleware(s))
154
+
r.Get("/", s.Settings)
155
+
r.Put("/keys", s.SettingsKeys)
156
+
})
157
+
158
+
r.Get("/keys/{user}", s.Keys)
159
+
160
+
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
161
+
s.pages.Error404(w)
162
+
})
163
+
return r
164
+
}
-157
appview/state/state.go
-157
appview/state/state.go
···
829
829
830
830
return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil
831
831
}
832
-
833
-
func (s *State) Router() http.Handler {
834
-
router := chi.NewRouter()
835
-
836
-
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
837
-
pat := chi.URLParam(r, "*")
838
-
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
839
-
s.UserRouter().ServeHTTP(w, r)
840
-
} else {
841
-
s.StandardRouter().ServeHTTP(w, r)
842
-
}
843
-
})
844
-
845
-
return router
846
-
}
847
-
848
-
func (s *State) UserRouter() http.Handler {
849
-
r := chi.NewRouter()
850
-
851
-
// strip @ from user
852
-
r.Use(StripLeadingAt)
853
-
854
-
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
855
-
r.Get("/", s.ProfilePage)
856
-
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
857
-
r.Get("/", s.RepoIndex)
858
-
r.Get("/commits/{ref}", s.RepoLog)
859
-
r.Route("/tree/{ref}", func(r chi.Router) {
860
-
r.Get("/", s.RepoIndex)
861
-
r.Get("/*", s.RepoTree)
862
-
})
863
-
r.Get("/commit/{ref}", s.RepoCommit)
864
-
r.Get("/branches", s.RepoBranches)
865
-
r.Get("/tags", s.RepoTags)
866
-
r.Get("/blob/{ref}/*", s.RepoBlob)
867
-
868
-
r.Route("/issues", func(r chi.Router) {
869
-
r.Get("/", s.RepoIssues)
870
-
r.Get("/{issue}", s.RepoSingleIssue)
871
-
872
-
r.Group(func(r chi.Router) {
873
-
r.Use(AuthMiddleware(s))
874
-
r.Get("/new", s.NewIssue)
875
-
r.Post("/new", s.NewIssue)
876
-
r.Post("/{issue}/comment", s.IssueComment)
877
-
r.Post("/{issue}/close", s.CloseIssue)
878
-
r.Post("/{issue}/reopen", s.ReopenIssue)
879
-
})
880
-
})
881
-
882
-
r.Route("/pulls", func(r chi.Router) {
883
-
r.Get("/", s.RepoPulls)
884
-
r.Get("/{pull}", s.RepoSinglePull)
885
-
886
-
r.Group(func(r chi.Router) {
887
-
r.Use(AuthMiddleware(s))
888
-
r.Get("/new", s.NewPull)
889
-
r.Post("/new", s.NewPull)
890
-
// r.Post("/{pull}/comment", s.PullComment)
891
-
// r.Post("/{pull}/close", s.ClosePull)
892
-
// r.Post("/{pull}/reopen", s.ReopenPull)
893
-
// r.Post("/{pull}/merge", s.MergePull)
894
-
})
895
-
})
896
-
897
-
// These routes get proxied to the knot
898
-
r.Get("/info/refs", s.InfoRefs)
899
-
r.Post("/git-upload-pack", s.UploadPack)
900
-
901
-
// settings routes, needs auth
902
-
r.Group(func(r chi.Router) {
903
-
r.Use(AuthMiddleware(s))
904
-
// repo description can only be edited by owner
905
-
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
906
-
r.Put("/", s.RepoDescription)
907
-
r.Get("/", s.RepoDescription)
908
-
r.Get("/edit", s.RepoDescriptionEdit)
909
-
})
910
-
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
911
-
r.Get("/", s.RepoSettings)
912
-
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
913
-
})
914
-
})
915
-
})
916
-
})
917
-
918
-
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
919
-
s.pages.Error404(w)
920
-
})
921
-
922
-
return r
923
-
}
924
-
925
-
func (s *State) StandardRouter() http.Handler {
926
-
r := chi.NewRouter()
927
-
928
-
r.Handle("/static/*", s.pages.Static())
929
-
930
-
r.Get("/", s.Timeline)
931
-
932
-
r.With(AuthMiddleware(s)).Get("/logout", s.Logout)
933
-
934
-
r.Route("/login", func(r chi.Router) {
935
-
r.Get("/", s.Login)
936
-
r.Post("/", s.Login)
937
-
})
938
-
939
-
r.Route("/knots", func(r chi.Router) {
940
-
r.Use(AuthMiddleware(s))
941
-
r.Get("/", s.Knots)
942
-
r.Post("/key", s.RegistrationKey)
943
-
944
-
r.Route("/{domain}", func(r chi.Router) {
945
-
r.Post("/init", s.InitKnotServer)
946
-
r.Get("/", s.KnotServerInfo)
947
-
r.Route("/member", func(r chi.Router) {
948
-
r.Use(RoleMiddleware(s, "server:owner"))
949
-
r.Get("/", s.ListMembers)
950
-
r.Put("/", s.AddMember)
951
-
r.Delete("/", s.RemoveMember)
952
-
})
953
-
})
954
-
})
955
-
956
-
r.Route("/repo", func(r chi.Router) {
957
-
r.Route("/new", func(r chi.Router) {
958
-
r.Use(AuthMiddleware(s))
959
-
r.Get("/", s.NewRepo)
960
-
r.Post("/", s.NewRepo)
961
-
})
962
-
// r.Post("/import", s.ImportRepo)
963
-
})
964
-
965
-
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
966
-
r.Post("/", s.Follow)
967
-
r.Delete("/", s.Follow)
968
-
})
969
-
970
-
r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
971
-
r.Post("/", s.Star)
972
-
r.Delete("/", s.Star)
973
-
})
974
-
975
-
r.Route("/settings", func(r chi.Router) {
976
-
r.Use(AuthMiddleware(s))
977
-
r.Get("/", s.Settings)
978
-
r.Put("/keys", s.SettingsKeys)
979
-
r.Delete("/keys", s.SettingsKeys)
980
-
})
981
-
982
-
r.Get("/keys/{user}", s.Keys)
983
-
984
-
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
985
-
s.pages.Error404(w)
986
-
})
987
-
return r
988
-
}
+5
-5
input.css
+5
-5
input.css
···
110
110
font-size: 14px;
111
111
}
112
112
a {
113
-
@apply no-underline text-black hover:underline hover:text-gray-800;
113
+
@apply no-underline text-black hover:underline hover:text-gray-800;
114
114
}
115
115
116
116
label {
117
-
@apply block text-sm text-black;
117
+
@apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase;
118
118
}
119
119
input {
120
-
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2;
120
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3;
121
121
}
122
122
textarea {
123
-
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2;
123
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3;
124
124
}
125
125
details summary::-webkit-details-marker {
126
-
display: none;
126
+
display: none;
127
127
}
128
128
}
129
129