Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

add acls to patch-requests, refactor

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