+6
-3
appview/db/pulls.go
+6
-3
appview/db/pulls.go
···
295
295
func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) {
296
296
row := e.QueryRow(`
297
297
select
298
-
count(case when state = 0 then 1 end) as open_count,
299
-
count(case when state = 1 then 1 end) as merged_count,
300
-
count(case when state = 2 then 1 end) as closed_count
298
+
count(case when state = ? then 1 end) as open_count,
299
+
count(case when state = ? then 1 end) as merged_count,
300
+
count(case when state = ? then 1 end) as closed_count
301
301
from pulls
302
302
where repo_at = ?`,
303
+
PullOpen,
304
+
PullMerged,
305
+
PullClosed,
303
306
repoAt,
304
307
)
305
308
+197
-236
appview/pages/templates/repo/pulls/pull.html
+197
-236
appview/pages/templates/repo/pulls/pull.html
···
35
35
></i>
36
36
<span class="text-white">{{ .Pull.State.String }}</span>
37
37
</div>
38
-
<span class="text-gray-400 text-sm">
38
+
<span class="text-gray-500 text-sm">
39
39
opened by
40
-
{{ $owner := didOrHandle .Pull.OwnerDid .PullOwnerHandle }}
40
+
{{ $owner := index $.DidHandleMap .Pull.OwnerDid }}
41
41
<a href="/{{ $owner }}" class="no-underline hover:underline"
42
42
>{{ $owner }}</a
43
43
>
···
80
80
id="patch"
81
81
name="patch"
82
82
class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden"
83
-
>
84
-
{{- .Pull.Patch -}}</textarea
85
-
>
83
+
>{{- .Pull.Patch -}}</textarea>
86
84
87
85
<div class="flex gap-2 justify-end mt-2">
88
86
<button
···
153
151
{{ end }}
154
152
155
153
{{ define "repoAfter" }}
154
+
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
155
+
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
156
+
156
157
<section id="comments" class="mt-8 space-y-4 relative">
157
-
{{ range $index, $comment := .Comments }}
158
-
<div
159
-
id="comment-{{ .CommentId }}"
160
-
class="rounded bg-white p-4 relative"
161
-
>
162
-
{{ if eq $index 0 }}
163
-
<div
164
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
165
-
></div>
166
-
{{ else }}
167
-
<div
168
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
169
-
></div>
170
-
{{ end }}
171
-
<div class="flex items-center gap-2 mb-2 text-gray-400">
172
-
{{ $owner := index $.DidHandleMap .OwnerDid }}
173
-
<span class="text-sm">
174
-
<a
175
-
href="/{{ $owner }}"
176
-
class="no-underline hover:underline"
177
-
>{{ $owner }}</a
178
-
>
179
-
</span>
180
-
<span
181
-
class="px-1 select-none before:content-['\00B7']"
182
-
></span>
183
-
<a
184
-
href="#{{ .CommentId }}"
185
-
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline"
186
-
id="{{ .CommentId }}"
187
-
>
188
-
{{ .Created | timeFmt }}
189
-
</a>
190
-
</div>
191
-
<div class="prose">
192
-
{{ .Body | markdown }}
193
-
</div>
194
-
</div>
195
-
{{ end }}
158
+
{{ block "comments" . }} {{ end }}
196
159
197
-
{{ if .Pull.State.IsMerged }}
198
-
<div
199
-
id="merge-status-card"
200
-
class="rounded relative bg-purple-50 border border-purple-200 p-4"
201
-
>
202
-
{{ if gt (len .Comments) 0 }}
203
-
<div
204
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
205
-
></div>
206
-
{{ else }}
207
-
<div
208
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
209
-
></div>
210
-
{{ end }}
211
-
212
-
213
-
<div class="flex items-center gap-2 text-purple-500">
214
-
<i data-lucide="git-merge" class="w-4 h-4"></i>
215
-
<span class="font-medium"
216
-
>Pull request successfully merged</span
217
-
>
218
-
</div>
219
-
220
-
<div class="mt-2 text-sm text-gray-700">
221
-
<p>
222
-
This pull request has been merged into the base branch.
223
-
</p>
224
-
</div>
225
-
</div>
226
-
{{ else if .MergeCheck }}
227
-
<div
228
-
id="merge-status-card"
229
-
class="rounded relative {{ if .MergeCheck.IsConflicted }}
230
-
bg-red-50 border border-red-200
231
-
{{ else }}
232
-
bg-green-50 border border-green-200
233
-
{{ end }} p-4"
234
-
>
235
-
{{ if gt (len .Comments) 0 }}
236
-
<div
237
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
238
-
></div>
239
-
{{ else }}
240
-
<div
241
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
242
-
></div>
243
-
{{ end }}
244
-
245
-
246
-
<div
247
-
class="flex items-center gap-2 {{ if .MergeCheck.IsConflicted }}
248
-
text-red-500
249
-
{{ else }}
250
-
text-green-500
251
-
{{ end }}"
252
-
>
253
-
{{ if .MergeCheck.IsConflicted }}
254
-
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
255
-
<span class="font-medium"
256
-
>merge conflicts detected</span
257
-
>
258
-
{{ else }}
259
-
<i data-lucide="check-circle" class="w-4 h-4"></i>
260
-
<span class="font-medium">ready to merge</span>
261
-
{{ end }}
262
-
</div>
263
-
264
-
{{ if .MergeCheck.IsConflicted }}
265
-
<div class="mt-2">
266
-
<ul class="text-sm space-y-1">
267
-
{{ range .MergeCheck.Conflicts }}
268
-
<li class="flex items-center">
269
-
<i
270
-
data-lucide="file-warning"
271
-
class="w-3 h-3 mr-1.5 text-red-500"
272
-
></i>
273
-
<span class="font-mono"
274
-
>{{ slice .Filename 0 (sub (len .Filename) 2) }}</span
275
-
>
276
-
</li>
277
-
{{ end }}
278
-
</ul>
279
-
</div>
280
-
<div class="mt-3 text-sm text-gray-700">
281
-
<p>
282
-
Please resolve these conflicts locally and update
283
-
the patch to continue with the merge.
284
-
</p>
285
-
</div>
286
-
{{ else }}
287
-
<div class="mt-2 text-sm text-gray-700">
288
-
<p>
289
-
No conflicts detected with the base branch. This
290
-
pull request can be merged safely.
291
-
</p>
292
-
</div>
293
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }}
294
-
<div class="mt-4 flex items-center gap-2">
295
-
<form
296
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
297
-
hx-swap="none"
298
-
>
299
-
<input
300
-
type="hidden"
301
-
name="targetBranch"
302
-
value="{{ .Pull.TargetBranch }}"
303
-
/>
304
-
<input
305
-
type="hidden"
306
-
name="patch"
307
-
value="{{ .Pull.Patch }}"
308
-
/>
309
-
<button
310
-
type="submit"
311
-
class="btn flex items-center gap-2"
312
-
{{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }}
313
-
disabled
314
-
{{ end }}
315
-
>
316
-
<i
317
-
data-lucide="git-merge"
318
-
class="w-4 h-4 text-purple-500"
319
-
></i>
320
-
<span>merge</span>
321
-
</button>
322
-
</form>
323
-
324
-
{{ if or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }}
325
-
{{ $action := "close" }}
326
-
{{ $icon := "circle-x" }}
327
-
{{ $hoverColor := "red" }}
328
-
{{ if .Pull.State.IsClosed }}
329
-
{{ $action = "reopen" }}
330
-
{{ $icon = "circle-dot" }}
331
-
{{ $hoverColor = "green" }}
332
-
{{ end }}
333
-
<form
334
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
335
-
hx-swap="none"
336
-
>
337
-
<button
338
-
type="submit"
339
-
class="btn flex items-center gap-2"
340
-
>
341
-
<i
342
-
data-lucide="{{ $icon }}"
343
-
class="w-4 h-4 text-{{ $hoverColor }}-400"
344
-
></i>
345
-
<span>{{ $action }}</span>
346
-
</button>
347
-
</form>
348
-
<div id="pull-merge-error" class="error"></div>
349
-
<div
350
-
id="pull-merge-success"
351
-
class="success"
352
-
></div>
353
-
{{ end }}
354
-
</div>
355
-
{{ end }}
356
-
{{ end }}
357
-
</div>
358
-
{{ end }}
160
+
{{ if .Pull.State.IsMerged }}
161
+
{{ block "alreadyMergedCard" . }} {{ end }}
162
+
{{ else if .MergeCheck }}
163
+
{{ if .MergeCheck.IsConflicted }}
164
+
{{ block "isConflictedCard" . }} {{ end }}
165
+
{{ else }}
166
+
{{ block "noConflictsCard" . }} {{ end }}
167
+
{{ end }}
168
+
{{ end }}
359
169
</section>
360
170
361
-
{{ if .LoggedInUser }}
362
-
<form
363
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment"
364
-
class="mt-8"
365
-
hx-swap="none"
366
-
>
367
-
<textarea
368
-
name="body"
369
-
class="w-full p-2 rounded border border-gray-200"
370
-
placeholder="Add to the discussion..."
371
-
></textarea>
372
-
<button type="submit" class="btn mt-2">comment</button>
373
-
<div id="pull-comment"></div>
374
-
</form>
375
-
{{ end }}
171
+
{{ block "newComment" . }} {{ end }}
376
172
377
-
{{ if and (or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) (not .MergeCheck) (not .Pull.State.IsMerged) }}
173
+
{{ if and (or $isPullAuthor $isRepoCollaborator) (not .Pull.State.IsMerged) }}
378
174
{{ $action := "close" }}
379
175
{{ $icon := "circle-x" }}
380
176
{{ $hoverColor := "red" }}
···
383
179
{{ $icon = "circle-dot" }}
384
180
{{ $hoverColor = "green" }}
385
181
{{ end }}
386
-
<form
387
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
388
-
hx-swap="none"
389
-
class="mt-8"
390
-
>
391
-
<button type="submit" class="btn text-sm flex items-center gap-2">
392
-
<i
393
-
data-lucide="{{ $icon }}"
394
-
class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"
395
-
></i>
396
-
<span class="text-black">{{ $action }}</span>
397
-
</button>
398
-
</form>
182
+
<button
183
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
184
+
hx-swap="none"
185
+
class="btn mt-8 text-sm flex items-center gap-2">
186
+
<i data-lucide="{{ $icon }}" class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"></i>
187
+
<span class="text-black">{{ $action }}</span>
188
+
</button>
399
189
{{ end }}
400
190
401
-
402
191
<div id="pull-close"></div>
403
192
<div id="pull-reopen"></div>
404
193
{{ end }}
194
+
195
+
{{ define "comments" }}
196
+
{{ range $index, $comment := .Comments }}
197
+
<div
198
+
id="comment-{{ .CommentId }}"
199
+
class="rounded bg-white p-4 relative drop-shadow-sm"
200
+
>
201
+
{{ if eq $index 0 }}
202
+
<div
203
+
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
204
+
></div>
205
+
{{ else }}
206
+
<div
207
+
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
208
+
></div>
209
+
{{ end }}
210
+
<div class="flex items-center gap-2 mb-2 text-gray-400">
211
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
212
+
<span class="text-sm">
213
+
<a
214
+
href="/{{ $owner }}"
215
+
class="no-underline hover:underline"
216
+
>{{ $owner }}</a
217
+
>
218
+
</span>
219
+
<span
220
+
class="px-1 select-none before:content-['\00B7']"
221
+
></span>
222
+
<a
223
+
href="#{{ .CommentId }}"
224
+
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline"
225
+
id="{{ .CommentId }}"
226
+
>
227
+
{{ .Created | timeFmt }}
228
+
</a>
229
+
</div>
230
+
<div class="prose">
231
+
{{ .Body | markdown }}
232
+
</div>
233
+
</div>
234
+
{{ end }}
235
+
{{ end }}
236
+
237
+
{{ define "newComment" }}
238
+
{{ if .LoggedInUser }}
239
+
<form
240
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment"
241
+
class="mt-8"
242
+
hx-swap="none">
243
+
<textarea
244
+
name="body"
245
+
class="w-full p-2 rounded border border-gray-200"
246
+
placeholder="Add to the discussion..."
247
+
></textarea>
248
+
<button type="submit" class="btn mt-2">comment</button>
249
+
<div id="pull-comment"></div>
250
+
</form>
251
+
{{ else }}
252
+
<div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8">
253
+
<a href="/login" class="underline">login</a> to join the discussion
254
+
</div>
255
+
{{ end }}
256
+
{{ end }}
257
+
258
+
{{ define "alreadyMergedCard" }}
259
+
<div
260
+
id="merge-status-card"
261
+
class="rounded relative bg-purple-50 border border-purple-200 p-4">
262
+
{{ if gt (len .Comments) 0 }}
263
+
<div
264
+
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
265
+
></div>
266
+
{{ else }}
267
+
<div
268
+
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
269
+
></div>
270
+
{{ end }}
271
+
272
+
273
+
<div class="flex items-center gap-2 text-purple-500">
274
+
<i data-lucide="git-merge" class="w-4 h-4"></i>
275
+
<span class="font-medium"
276
+
>Pull request successfully merged</span
277
+
>
278
+
</div>
279
+
280
+
<div class="mt-2 text-sm text-gray-700">
281
+
<p>This pull request has been merged into the base branch.</p>
282
+
</div>
283
+
</div>
284
+
{{ end }}
285
+
286
+
{{ define "isConflictedCard" }}
287
+
<div
288
+
id="merge-status-card"
289
+
class="rounded relative border bg-red-50 border-red-200 p-4">
290
+
{{ if gt (len .Comments) 0 }}
291
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div>
292
+
{{ else }}
293
+
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div>
294
+
{{ end }}
295
+
296
+
<div class="flex items-center gap-2 text-red-500">
297
+
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
298
+
<span class="font-medium">merge conflicts detected</span>
299
+
</div>
300
+
301
+
<div class="mt-2">
302
+
<ul class="text-sm space-y-1">
303
+
{{ range .MergeCheck.Conflicts }}
304
+
<li class="flex items-center">
305
+
<i
306
+
data-lucide="file-warning"
307
+
class="w-3 h-3 mr-1.5 text-red-500"
308
+
></i>
309
+
<span class="font-mono"
310
+
>{{ slice .Filename 0 (sub (len .Filename) 2) }}</span
311
+
>
312
+
</li>
313
+
{{ end }}
314
+
</ul>
315
+
</div>
316
+
<div class="mt-3 text-sm text-gray-700">
317
+
<p>
318
+
Please resolve these conflicts locally and update
319
+
the patch to continue with the merge.
320
+
</p>
321
+
</div>
322
+
</div>
323
+
{{ end }}
324
+
325
+
326
+
{{ define "noConflictsCard" }}
327
+
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
328
+
<div
329
+
id="merge-status-card"
330
+
class="rounded relative border bg-green-50 border-green-200 p-4">
331
+
{{ if gt (len .Comments) 0 }}
332
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div>
333
+
{{ else }}
334
+
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div>
335
+
{{ end }}
336
+
337
+
<div class="flex items-center gap-2 text-green-500">
338
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
339
+
<span class="font-medium">ready to merge</span>
340
+
</div>
341
+
342
+
<div class="mt-2 text-sm text-gray-700">
343
+
No conflicts detected with the base branch. This
344
+
pull request can be merged safely.
345
+
</div>
346
+
347
+
<div class="mt-4 flex items-center gap-2">
348
+
{{ if $isRepoCollaborator }}
349
+
<button
350
+
class="btn flex items-center gap-2"
351
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
352
+
hx-swap="none"
353
+
{{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }}
354
+
disabled
355
+
{{ end }}>
356
+
<i data-lucide="git-merge" class="w-4 h-4 text-purple-500"></i>
357
+
<span>merge</span>
358
+
</button>
359
+
{{ end }}
360
+
361
+
<div id="pull-merge-error" class="error"></div>
362
+
<div id="pull-merge-success" class="success"></div>
363
+
</div>
364
+
</div>
365
+
{{ end }}
+40
-2
appview/state/middleware.go
+40
-2
appview/state/middleware.go
···
4
4
"context"
5
5
"log"
6
6
"net/http"
7
+
"strconv"
7
8
"strings"
8
9
"time"
9
10
···
98
99
}
99
100
}
100
101
101
-
func RoleMiddleware(s *State, group string) Middleware {
102
+
func knotRoleMiddleware(s *State, group string) Middleware {
102
103
return func(next http.Handler) http.Handler {
103
104
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104
105
// requires auth also
···
128
129
}
129
130
}
130
131
132
+
func KnotOwner(s *State) Middleware {
133
+
return knotRoleMiddleware(s, "server:owner")
134
+
}
135
+
131
136
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
132
137
return func(next http.Handler) http.Handler {
133
138
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
···
188
193
}
189
194
}
190
195
191
-
func ResolveRepoKnot(s *State) Middleware {
196
+
func ResolveRepo(s *State) Middleware {
192
197
return func(next http.Handler) http.Handler {
193
198
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
194
199
repoName := chi.URLParam(req, "repo")
···
215
220
})
216
221
}
217
222
}
223
+
224
+
// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
225
+
func ResolvePull(s *State) Middleware {
226
+
return func(next http.Handler) http.Handler {
227
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
228
+
f, err := fullyResolvedRepo(r)
229
+
if err != nil {
230
+
log.Println("failed to fully resolve repo", err)
231
+
http.Error(w, "invalid repo url", http.StatusNotFound)
232
+
return
233
+
}
234
+
235
+
prId := chi.URLParam(r, "pull")
236
+
prIdInt, err := strconv.Atoi(prId)
237
+
if err != nil {
238
+
http.Error(w, "bad pr id", http.StatusBadRequest)
239
+
log.Println("failed to parse pr id", err)
240
+
return
241
+
}
242
+
243
+
pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
244
+
if err != nil {
245
+
log.Println("failed to get pull and comments", err)
246
+
return
247
+
}
248
+
249
+
ctx := context.WithValue(r.Context(), "pull", pr)
250
+
ctx = context.WithValue(ctx, "pull_comments", comments)
251
+
252
+
next.ServeHTTP(w, r.WithContext(ctx))
253
+
})
254
+
}
255
+
}
+80
-74
appview/state/repo.go
+80
-74
appview/state/repo.go
···
232
232
233
233
func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
234
234
user := s.auth.GetUser(r)
235
-
f, err := fullyResolvedRepo(r)
236
-
if err != nil {
237
-
log.Println("failed to get repo and knot", err)
238
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
235
+
236
+
patch := r.FormValue("patch")
237
+
if patch == "" {
238
+
s.pages.Notice(w, "pull-error", "Patch is required.")
239
239
return
240
240
}
241
241
242
-
prId := chi.URLParam(r, "pull")
243
-
prIdInt, err := strconv.Atoi(prId)
244
-
if err != nil {
245
-
http.Error(w, "bad pr id", http.StatusBadRequest)
246
-
log.Println("failed to parse pr id", err)
242
+
pull, ok := r.Context().Value("pull").(*db.Pull)
243
+
if !ok {
244
+
log.Println("failed to get pull")
245
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
247
246
return
248
247
}
249
248
250
-
patch := r.FormValue("patch")
251
-
if patch == "" {
252
-
s.pages.Notice(w, "pull-error", "Patch is required.")
249
+
if pull.OwnerDid != user.Did {
250
+
log.Println("failed to edit pull information")
251
+
s.pages.Notice(w, "pull-error", "Unauthorized")
253
252
return
254
253
}
255
254
256
-
// Get pull information before updating to get the atproto record URI
257
-
pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
255
+
f, err := fullyResolvedRepo(r)
258
256
if err != nil {
259
-
log.Println("failed to get pull information", err)
257
+
log.Println("failed to get repo and knot", err)
260
258
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
261
259
return
262
260
}
···
273
271
defer tx.Rollback()
274
272
275
273
// Update patch in the database within transaction
276
-
err = db.EditPatch(tx, f.RepoAt, prIdInt, patch)
274
+
err = db.EditPatch(tx, f.RepoAt, pull.PullId, patch)
277
275
if err != nil {
278
276
log.Println("failed to update patch", err)
279
277
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
362
360
return
363
361
}
364
362
365
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt))
363
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
366
364
return
367
365
}
368
366
···
489
487
return
490
488
}
491
489
492
-
prId := chi.URLParam(r, "pull")
493
-
prIdInt, err := strconv.Atoi(prId)
494
-
if err != nil {
495
-
http.Error(w, "bad pr id", http.StatusBadRequest)
496
-
log.Println("failed to parse pr id", err)
497
-
return
498
-
}
499
-
500
-
pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
501
-
if err != nil {
502
-
log.Println("failed to get pr and comments", err)
503
-
s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
490
+
pull, ok1 := r.Context().Value("pull").(*db.Pull)
491
+
comments, ok2 := r.Context().Value("pull_comments").([]db.PullComment)
492
+
if !ok1 || !ok2 {
493
+
log.Println("failed to get pull")
494
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
504
495
return
505
496
}
506
497
507
-
pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid)
508
-
if err != nil {
509
-
log.Println("failed to resolve pull owner", err)
510
-
}
511
-
512
498
identsToResolve := make([]string, len(comments))
513
499
for i, comment := range comments {
514
500
identsToResolve[i] = comment.OwnerDid
515
501
}
502
+
identsToResolve = append(identsToResolve, pull.OwnerDid)
503
+
516
504
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
517
505
didHandleMap := make(map[string]string)
518
506
for _, identity := range resolvedIds {
···
526
514
var mergeCheckResponse types.MergeCheckResponse
527
515
528
516
// Only perform merge check if the pull request is not already merged
529
-
if pr.State != db.PullMerged {
517
+
if pull.State != db.PullMerged {
530
518
secret, err := db.GetRegistrationKey(s.db, f.Knot)
531
519
if err != nil {
532
520
log.Printf("failed to get registration key for %s", f.Knot)
···
536
524
537
525
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
538
526
if err == nil {
539
-
resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch)
527
+
resp, err := ksClient.MergeCheck([]byte(pull.Patch), pull.OwnerDid, f.RepoName, pull.TargetBranch)
540
528
if err != nil {
541
529
log.Println("failed to check for mergeability:", err)
542
530
} else {
···
556
544
}
557
545
558
546
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
559
-
LoggedInUser: user,
560
-
RepoInfo: f.RepoInfo(s, user),
561
-
Pull: *pr,
562
-
Comments: comments,
563
-
PullOwnerHandle: pullOwnerIdent.Handle.String(),
564
-
DidHandleMap: didHandleMap,
565
-
MergeCheck: mergeCheckResponse,
547
+
LoggedInUser: user,
548
+
RepoInfo: f.RepoInfo(s, user),
549
+
Pull: *pull,
550
+
Comments: comments,
551
+
DidHandleMap: didHandleMap,
552
+
MergeCheck: mergeCheckResponse,
566
553
})
567
554
}
568
555
···
1012
999
Description: f.Description,
1013
1000
IsStarred: isStarred,
1014
1001
Knot: knot,
1015
-
Roles: rolesInRepo(s, u, f),
1002
+
Roles: RolesInRepo(s, u, f),
1016
1003
Stats: db.RepoStats{
1017
1004
StarCount: starCount,
1018
1005
IssueCount: issueCount,
···
1464
1451
return
1465
1452
}
1466
1453
1467
-
// Get the pull request ID from the request URL
1468
-
pullId := chi.URLParam(r, "pull")
1469
-
pullIdInt, err := strconv.Atoi(pullId)
1470
-
if err != nil {
1471
-
log.Println("failed to parse pull ID:", err)
1472
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1454
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1455
+
if !ok {
1456
+
log.Println("failed to get pull")
1457
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1473
1458
return
1474
1459
}
1475
1460
1476
-
// Get the patch data from the request body
1477
-
patch := r.FormValue("patch")
1478
-
branch := r.FormValue("targetBranch")
1479
-
1480
1461
secret, err := db.GetRegistrationKey(s.db, f.Knot)
1481
1462
if err != nil {
1482
1463
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
···
1492
1473
}
1493
1474
1494
1475
// Merge the pull request
1495
-
resp, err := ksClient.Merge([]byte(patch), user.Did, f.RepoName, branch)
1476
+
resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch)
1496
1477
if err != nil {
1497
1478
log.Printf("failed to merge pull request: %s", err)
1498
1479
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1500
1481
}
1501
1482
1502
1483
if resp.StatusCode == http.StatusOK {
1503
-
err := db.MergePull(s.db, f.RepoAt, pullIdInt)
1484
+
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
1504
1485
if err != nil {
1505
1486
log.Printf("failed to update pull request status in database: %s", err)
1506
1487
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1507
1488
return
1508
1489
}
1509
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pullIdInt))
1490
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
1510
1491
} else {
1511
1492
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
1512
1493
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1609
1590
}
1610
1591
1611
1592
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1593
+
user := s.auth.GetUser(r)
1594
+
1612
1595
f, err := fullyResolvedRepo(r)
1613
1596
if err != nil {
1614
1597
log.Println("malformed middleware")
1615
1598
return
1616
1599
}
1617
1600
1618
-
pullId := chi.URLParam(r, "pull")
1619
-
pullIdInt, err := strconv.Atoi(pullId)
1620
-
if err != nil {
1621
-
log.Println("malformed middleware")
1601
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1602
+
if !ok {
1603
+
log.Println("failed to get pull")
1604
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1605
+
return
1606
+
}
1607
+
1608
+
// auth filter: only owner or collaborators can close
1609
+
roles := RolesInRepo(s, user, f)
1610
+
isCollaborator := roles.IsCollaborator()
1611
+
isPullAuthor := user.Did == pull.OwnerDid
1612
+
isCloseAllowed := isCollaborator || isPullAuthor
1613
+
if !isCloseAllowed {
1614
+
log.Println("failed to close pull")
1615
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1622
1616
return
1623
1617
}
1624
1618
···
1631
1625
}
1632
1626
1633
1627
// Close the pull in the database
1634
-
err = db.ClosePull(tx, f.RepoAt, pullIdInt)
1628
+
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
1635
1629
if err != nil {
1636
1630
log.Println("failed to close pull", err)
1637
1631
s.pages.Notice(w, "pull-close", "Failed to close pull.")
···
1645
1639
return
1646
1640
}
1647
1641
1648
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
1642
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1649
1643
return
1650
1644
}
1651
1645
1652
1646
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1647
+
user := s.auth.GetUser(r)
1648
+
1653
1649
f, err := fullyResolvedRepo(r)
1654
1650
if err != nil {
1655
1651
log.Println("failed to resolve repo", err)
···
1657
1653
return
1658
1654
}
1659
1655
1660
-
// Start a transaction
1661
-
tx, err := s.db.BeginTx(r.Context(), nil)
1662
-
if err != nil {
1663
-
log.Println("failed to start transaction", err)
1664
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1656
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1657
+
if !ok {
1658
+
log.Println("failed to get pull")
1659
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1665
1660
return
1666
1661
}
1667
1662
1668
-
pullId := chi.URLParam(r, "pull")
1669
-
pullIdInt, err := strconv.Atoi(pullId)
1663
+
// auth filter: only owner or collaborators can close
1664
+
roles := RolesInRepo(s, user, f)
1665
+
isCollaborator := roles.IsCollaborator()
1666
+
isPullAuthor := user.Did == pull.OwnerDid
1667
+
isCloseAllowed := isCollaborator || isPullAuthor
1668
+
if !isCloseAllowed {
1669
+
log.Println("failed to close pull")
1670
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1671
+
return
1672
+
}
1673
+
1674
+
// Start a transaction
1675
+
tx, err := s.db.BeginTx(r.Context(), nil)
1670
1676
if err != nil {
1671
-
log.Println("failed to parse pull id", err)
1677
+
log.Println("failed to start transaction", err)
1672
1678
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1673
1679
return
1674
1680
}
1675
1681
1676
1682
// Reopen the pull in the database
1677
-
err = db.ReopenPull(tx, f.RepoAt, pullIdInt)
1683
+
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
1678
1684
if err != nil {
1679
1685
log.Println("failed to reopen pull", err)
1680
1686
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
···
1688
1694
return
1689
1695
}
1690
1696
1691
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
1697
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1692
1698
return
1693
1699
}
1694
1700
···
1731
1737
}, nil
1732
1738
}
1733
1739
1734
-
func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1740
+
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1735
1741
if u != nil {
1736
1742
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
1737
1743
return pages.RolesInRepo{r}
+24
-12
appview/state/router.go
+24
-12
appview/state/router.go
···
30
30
31
31
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
32
32
r.Get("/", s.ProfilePage)
33
-
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
33
+
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
34
34
r.Get("/", s.RepoIndex)
35
35
r.Get("/commits/{ref}", s.RepoLog)
36
36
r.Route("/tree/{ref}", func(r chi.Router) {
···
58
58
59
59
r.Route("/pulls", func(r chi.Router) {
60
60
r.Get("/", s.RepoPulls)
61
-
r.Get("/{pull}", s.RepoSinglePull)
61
+
r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) {
62
+
r.Get("/", s.NewPull)
63
+
r.Post("/", s.NewPull)
64
+
})
62
65
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.Patch("/{pull}/patch", s.EditPatch)
68
-
r.Post("/{pull}/comment", s.PullComment)
69
-
r.Post("/{pull}/close", s.ClosePull)
70
-
r.Post("/{pull}/reopen", s.ReopenPull)
71
-
r.Post("/{pull}/merge", s.MergePull)
66
+
r.Route("/{pull}", func(r chi.Router) {
67
+
r.Use(ResolvePull(s))
68
+
r.Get("/", s.RepoSinglePull)
69
+
70
+
// authorized requests below this point
71
+
r.Group(func(r chi.Router) {
72
+
r.Use(AuthMiddleware(s))
73
+
r.Patch("/patch", s.EditPatch)
74
+
r.Post("/comment", s.PullComment)
75
+
r.Post("/close", s.ClosePull)
76
+
r.Post("/reopen", s.ReopenPull)
77
+
// collaborators only
78
+
r.Group(func(r chi.Router) {
79
+
r.Use(RepoPermissionMiddleware(s, "repo:collaborator"))
80
+
r.Post("/merge", s.MergePull)
81
+
// maybe lock, etc.
82
+
})
83
+
})
72
84
})
73
85
})
74
86
···
123
135
r.Post("/init", s.InitKnotServer)
124
136
r.Get("/", s.KnotServerInfo)
125
137
r.Route("/member", func(r chi.Router) {
126
-
r.Use(RoleMiddleware(s, "server:owner"))
138
+
r.Use(KnotOwner(s))
127
139
r.Get("/", s.ListMembers)
128
140
r.Put("/", s.AddMember)
129
141
r.Delete("/", s.RemoveMember)
+1
-1
flake.nix
+1
-1
flake.nix
···
44
44
inherit (gitignore.lib) gitignoreSource;
45
45
in {
46
46
overlays.default = final: prev: let
47
-
goModHash = "sha256-k+WeNx9jZ5YGgskCJYiU2mwyz25E0bhFgSg2GDWZXFw=";
47
+
goModHash = "sha256-zJKjcxd+gr+9Kx2e1lUv+0hlXlxJm5YbWeIGUo0eIiE=";
48
48
buildCmdPackage = name:
49
49
final.buildGoModule {
50
50
pname = name;