forked from tangled.org/core
Monorepo for Tangled

add acls to patch-requests, refactor

Changed files
+348 -328
appview
db
pages
templates
repo
pulls
state
+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 ··· 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
··· 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
··· 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
··· 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
··· 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;