+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
···
88
89
}
89
90
}
90
91
91
-
func RoleMiddleware(s *State, group string) Middleware {
92
+
func knotRoleMiddleware(s *State, group string) Middleware {
92
93
return func(next http.Handler) http.Handler {
93
94
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
94
95
// requires auth also
···
118
119
}
119
120
}
120
121
122
+
func KnotOwner(s *State) Middleware {
123
+
return knotRoleMiddleware(s, "server:owner")
124
+
}
125
+
121
126
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
122
127
return func(next http.Handler) http.Handler {
123
128
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
···
178
183
}
179
184
}
180
185
181
-
func ResolveRepoKnot(s *State) Middleware {
186
+
func ResolveRepo(s *State) Middleware {
182
187
return func(next http.Handler) http.Handler {
183
188
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
184
189
repoName := chi.URLParam(req, "repo")
···
205
210
})
206
211
}
207
212
}
213
+
214
+
// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
215
+
func ResolvePull(s *State) Middleware {
216
+
return func(next http.Handler) http.Handler {
217
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
218
+
f, err := fullyResolvedRepo(r)
219
+
if err != nil {
220
+
log.Println("failed to fully resolve repo", err)
221
+
http.Error(w, "invalid repo url", http.StatusNotFound)
222
+
return
223
+
}
224
+
225
+
prId := chi.URLParam(r, "pull")
226
+
prIdInt, err := strconv.Atoi(prId)
227
+
if err != nil {
228
+
http.Error(w, "bad pr id", http.StatusBadRequest)
229
+
log.Println("failed to parse pr id", err)
230
+
return
231
+
}
232
+
233
+
pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
234
+
if err != nil {
235
+
log.Println("failed to get pull and comments", err)
236
+
return
237
+
}
238
+
239
+
ctx := context.WithValue(r.Context(), "pull", pr)
240
+
ctx = context.WithValue(ctx, "pull_comments", comments)
241
+
242
+
next.ServeHTTP(w, r.WithContext(ctx))
243
+
})
244
+
}
245
+
}
+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,
···
1448
1435
return
1449
1436
}
1450
1437
1451
-
// Get the pull request ID from the request URL
1452
-
pullId := chi.URLParam(r, "pull")
1453
-
pullIdInt, err := strconv.Atoi(pullId)
1454
-
if err != nil {
1455
-
log.Println("failed to parse pull ID:", err)
1456
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1438
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1439
+
if !ok {
1440
+
log.Println("failed to get pull")
1441
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1457
1442
return
1458
1443
}
1459
1444
1460
-
// Get the patch data from the request body
1461
-
patch := r.FormValue("patch")
1462
-
branch := r.FormValue("targetBranch")
1463
-
1464
1445
secret, err := db.GetRegistrationKey(s.db, f.Knot)
1465
1446
if err != nil {
1466
1447
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
···
1476
1457
}
1477
1458
1478
1459
// Merge the pull request
1479
-
resp, err := ksClient.Merge([]byte(patch), user.Did, f.RepoName, branch)
1460
+
resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch)
1480
1461
if err != nil {
1481
1462
log.Printf("failed to merge pull request: %s", err)
1482
1463
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1484
1465
}
1485
1466
1486
1467
if resp.StatusCode == http.StatusOK {
1487
-
err := db.MergePull(s.db, f.RepoAt, pullIdInt)
1468
+
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
1488
1469
if err != nil {
1489
1470
log.Printf("failed to update pull request status in database: %s", err)
1490
1471
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1491
1472
return
1492
1473
}
1493
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pullIdInt))
1474
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
1494
1475
} else {
1495
1476
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
1496
1477
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1593
1574
}
1594
1575
1595
1576
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1577
+
user := s.auth.GetUser(r)
1578
+
1596
1579
f, err := fullyResolvedRepo(r)
1597
1580
if err != nil {
1598
1581
log.Println("malformed middleware")
1599
1582
return
1600
1583
}
1601
1584
1602
-
pullId := chi.URLParam(r, "pull")
1603
-
pullIdInt, err := strconv.Atoi(pullId)
1604
-
if err != nil {
1605
-
log.Println("malformed middleware")
1585
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1586
+
if !ok {
1587
+
log.Println("failed to get pull")
1588
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1589
+
return
1590
+
}
1591
+
1592
+
// auth filter: only owner or collaborators can close
1593
+
roles := RolesInRepo(s, user, f)
1594
+
isCollaborator := roles.IsCollaborator()
1595
+
isPullAuthor := user.Did == pull.OwnerDid
1596
+
isCloseAllowed := isCollaborator || isPullAuthor
1597
+
if !isCloseAllowed {
1598
+
log.Println("failed to close pull")
1599
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1606
1600
return
1607
1601
}
1608
1602
···
1615
1609
}
1616
1610
1617
1611
// Close the pull in the database
1618
-
err = db.ClosePull(tx, f.RepoAt, pullIdInt)
1612
+
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
1619
1613
if err != nil {
1620
1614
log.Println("failed to close pull", err)
1621
1615
s.pages.Notice(w, "pull-close", "Failed to close pull.")
···
1629
1623
return
1630
1624
}
1631
1625
1632
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
1626
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1633
1627
return
1634
1628
}
1635
1629
1636
1630
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1631
+
user := s.auth.GetUser(r)
1632
+
1637
1633
f, err := fullyResolvedRepo(r)
1638
1634
if err != nil {
1639
1635
log.Println("failed to resolve repo", err)
···
1641
1637
return
1642
1638
}
1643
1639
1644
-
// Start a transaction
1645
-
tx, err := s.db.BeginTx(r.Context(), nil)
1646
-
if err != nil {
1647
-
log.Println("failed to start transaction", err)
1648
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1640
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1641
+
if !ok {
1642
+
log.Println("failed to get pull")
1643
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1649
1644
return
1650
1645
}
1651
1646
1652
-
pullId := chi.URLParam(r, "pull")
1653
-
pullIdInt, err := strconv.Atoi(pullId)
1647
+
// auth filter: only owner or collaborators can close
1648
+
roles := RolesInRepo(s, user, f)
1649
+
isCollaborator := roles.IsCollaborator()
1650
+
isPullAuthor := user.Did == pull.OwnerDid
1651
+
isCloseAllowed := isCollaborator || isPullAuthor
1652
+
if !isCloseAllowed {
1653
+
log.Println("failed to close pull")
1654
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1655
+
return
1656
+
}
1657
+
1658
+
// Start a transaction
1659
+
tx, err := s.db.BeginTx(r.Context(), nil)
1654
1660
if err != nil {
1655
-
log.Println("failed to parse pull id", err)
1661
+
log.Println("failed to start transaction", err)
1656
1662
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1657
1663
return
1658
1664
}
1659
1665
1660
1666
// Reopen the pull in the database
1661
-
err = db.ReopenPull(tx, f.RepoAt, pullIdInt)
1667
+
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
1662
1668
if err != nil {
1663
1669
log.Println("failed to reopen pull", err)
1664
1670
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
···
1672
1678
return
1673
1679
}
1674
1680
1675
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
1681
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1676
1682
return
1677
1683
}
1678
1684
···
1715
1721
}, nil
1716
1722
}
1717
1723
1718
-
func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1724
+
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1719
1725
if u != nil {
1720
1726
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
1721
1727
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;