This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pulls / resubmit.go
20 kB 605 lines
1package pulls 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "time" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/db" 11 "tangled.org/core/appview/knotcompat" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/oauth" 14 "tangled.org/core/appview/pages" 15 "tangled.org/core/appview/reporesolver" 16 "tangled.org/core/orm" 17 "tangled.org/core/patchutil" 18 "tangled.org/core/types" 19 "tangled.org/core/xrpc" 20 "tangled.org/core/xrpc/xrpcclient" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" 23 "github.com/bluesky-social/indigo/atproto/syntax" 24 lexutil "github.com/bluesky-social/indigo/lex/util" 25) 26 27func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 28 l := s.logger.With("handler", "ResubmitPull") 29 30 user := s.oauth.GetMultiAccountUser(r) 31 if user != nil { 32 l = l.With("user", user.Did) 33 } 34 35 pull, ok := r.Context().Value("pull").(*models.Pull) 36 if !ok { 37 l.Error("failed to get pull") 38 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 39 return 40 } 41 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 42 43 switch r.Method { 44 case http.MethodGet: 45 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 46 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 47 Pull: pull, 48 }) 49 return 50 case http.MethodPost: 51 if pull.IsPatchBased() { 52 s.resubmitPatch(w, r) 53 return 54 } else if pull.IsBranchBased() { 55 s.resubmitBranch(w, r) 56 return 57 } else if pull.IsForkBased() { 58 s.resubmitFork(w, r) 59 return 60 } 61 } 62} 63 64func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 65 l := s.logger.With("handler", "resubmitPatch") 66 67 user := s.oauth.GetMultiAccountUser(r) 68 if user != nil { 69 l = l.With("user", user.Did) 70 } 71 72 pull, ok := r.Context().Value("pull").(*models.Pull) 73 if !ok { 74 l.Error("failed to get pull") 75 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 76 return 77 } 78 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 79 80 if user == nil || user.Did != pull.OwnerDid { 81 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 82 w.WriteHeader(http.StatusUnauthorized) 83 return 84 } 85 86 f, err := s.repoResolver.Resolve(r) 87 if err != nil { 88 l.Error("failed to get repo and knot", "err", err) 89 return 90 } 91 92 patch := r.FormValue("patch") 93 94 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, "", "", "") 95} 96 97func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 98 l := s.logger.With("handler", "resubmitBranch") 99 100 user := s.oauth.GetMultiAccountUser(r) 101 if user != nil { 102 l = l.With("user", user.Did) 103 } 104 105 pull, ok := r.Context().Value("pull").(*models.Pull) 106 if !ok { 107 l.Error("failed to get pull") 108 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 109 return 110 } 111 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 112 113 if user == nil || user.Did != pull.OwnerDid { 114 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 115 w.WriteHeader(http.StatusUnauthorized) 116 return 117 } 118 119 f, err := s.repoResolver.Resolve(r) 120 if err != nil { 121 l.Error("failed to get repo and knot", "err", err) 122 return 123 } 124 125 roles := s.acl.RolesInRepo(r.Context(), f, user.Did) 126 if !roles.IsPushAllowed() { 127 l.Warn("unauthorized user - no push permission") 128 w.WriteHeader(http.StatusUnauthorized) 129 return 130 } 131 132 xrpcc := s.knotClient(f.Knot) 133 134 xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 135 if err != nil { 136 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 137 l.Error("failed to call XRPC repo.compare", "xrpcerr", xrpcerr, "err", err, "source_branch", pull.PullSource.Branch) 138 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 139 return 140 } 141 l.Error("compare request failed", "err", err, "source_branch", pull.PullSource.Branch) 142 s.pages.Notice(w, "resubmit-error", err.Error()) 143 return 144 } 145 146 var comparison types.RepoFormatPatchResponse 147 if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 148 l.Error("failed to decode XRPC compare response", "err", err) 149 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 150 return 151 } 152 153 sourceRev := comparison.Rev2 154 patch := comparison.FormatPatchRaw 155 combined := comparison.CombinedPatchRaw 156 157 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev, comparison.MergeBase) 158} 159 160func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 161 l := s.logger.With("handler", "resubmitFork") 162 163 user := s.oauth.GetMultiAccountUser(r) 164 if user != nil { 165 l = l.With("user", user.Did) 166 } 167 168 pull, ok := r.Context().Value("pull").(*models.Pull) 169 if !ok { 170 l.Error("failed to get pull") 171 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 172 return 173 } 174 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 175 176 if user == nil || user.Did != pull.OwnerDid { 177 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 178 w.WriteHeader(http.StatusUnauthorized) 179 return 180 } 181 182 f, err := s.repoResolver.Resolve(r) 183 if err != nil { 184 l.Error("failed to get repo and knot", "err", err) 185 return 186 } 187 188 forkRepo, err := db.GetRepoByDid(s.db, string(*pull.PullSource.RepoDid)) 189 if err != nil { 190 l.Error("failed to get source repo", "err", err, "repo_did", pull.PullSource.RepoDid.String()) 191 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 192 return 193 } 194 195 // update the hidden tracking branch to latest 196 client, err := s.oauth.ServiceClient( 197 r, 198 oauth.WithService(forkRepo.Knot), 199 oauth.WithLxm(tangled.RepoHiddenRefNSID), 200 oauth.WithDev(s.config.Core.Dev), 201 ) 202 if err != nil { 203 l.Error("failed to connect to knot server", "err", err, "fork_knot", forkRepo.Knot) 204 return 205 } 206 207 resp, err := tangled.RepoHiddenRef( 208 r.Context(), 209 client, 210 &tangled.RepoHiddenRef_Input{ 211 ForkRef: pull.PullSource.Branch, 212 RemoteRef: pull.TargetBranch, 213 Repo: forkRepo.RepoAt().String(), 214 }, 215 ) 216 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 217 s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 218 s.pages.Notice(w, "resubmit-error", xrpcerr.Error()) 219 return 220 } 221 if !resp.Success { 222 l.Error("failed to update tracking ref", "err", resp.Error, "fork_ref", pull.PullSource.Branch, "remote_ref", pull.TargetBranch) 223 s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 224 return 225 } 226 227 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 228 // extract patch by performing compare 229 forkXrpcBytes, err := tangled.RepoCompare(r.Context(), s.knotClient(forkRepo.Knot), forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 230 if err != nil { 231 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 232 l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 233 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 234 return 235 } 236 l.Error("failed to compare branches", "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 237 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 238 return 239 } 240 241 var forkComparison types.RepoFormatPatchResponse 242 if err := json.Unmarshal(forkXrpcBytes, &forkComparison); err != nil { 243 l.Error("failed to decode XRPC compare response for fork", "err", err) 244 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 245 return 246 } 247 248 // Use the fork comparison we already made 249 comparison := forkComparison 250 251 sourceRev := comparison.Rev2 252 patch := comparison.FormatPatchRaw 253 combined := comparison.CombinedPatchRaw 254 255 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev, comparison.MergeBase) 256} 257 258func (s *Pulls) resubmitPullHelper( 259 w http.ResponseWriter, 260 r *http.Request, 261 repo *models.Repo, 262 userDid syntax.DID, 263 pull *models.Pull, 264 patch string, 265 combined string, 266 sourceRev string, 267 mergeBase string, 268) { 269 l := s.logger.With("handler", "resubmitPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 270 271 stack := r.Context().Value("stack").(models.Stack) 272 if stack != nil && len(stack) != 1 { 273 l.Info("resubmitting stacked PR", "stack_size", len(stack)) 274 s.resubmitStackedPullHelper(w, r, repo, userDid, pull, patch) 275 return 276 } 277 278 if err := validatePatch(&patch); err != nil { 279 s.pages.Notice(w, "resubmit-error", err.Error()) 280 return 281 } 282 283 if patch == pull.LatestPatch() { 284 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 285 return 286 } 287 288 // validate sourceRev if branch/fork based 289 if pull.IsBranchBased() || pull.IsForkBased() { 290 if sourceRev == pull.LatestSha() { 291 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 292 return 293 } 294 } 295 296 pullAt := pull.AtUri() 297 newRoundNumber := len(pull.Submissions) 298 newPatch := patch 299 newSourceRev := sourceRev 300 combinedPatch := combined 301 302 client, err := s.oauth.AuthorizedClient(r) 303 if err != nil { 304 l.Error("failed to authorize client", "err", err) 305 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 306 return 307 } 308 309 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, userDid.String(), pull.Rkey) 310 if err != nil { 311 // failed to get record 312 l.Error("failed to get record from PDS", "err", err, "rkey", pull.Rkey) 313 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 314 return 315 } 316 317 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 318 if err != nil { 319 l.Error("failed to upload patch blob", "err", err) 320 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 321 return 322 } 323 record := pull.AsRecord() 324 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 325 CreatedAt: time.Now().Format(time.RFC3339), 326 PatchBlob: blob.Blob, 327 }) 328 329 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 330 Collection: tangled.RepoPullNSID, 331 Repo: userDid.String(), 332 Rkey: pull.Rkey, 333 SwapRecord: ex.Cid, 334 Record: knotcompat.Pull(&record), 335 }) 336 if err != nil { 337 l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey) 338 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 339 return 340 } 341 342 err = db.ResubmitPull(s.db, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, mergeBase, blob.Blob) 343 if err != nil { 344 l.Error("failed to resubmit pull request in database", "err", err, "round_number", newRoundNumber) 345 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 346 return 347 } 348 349 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 350 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 351} 352 353func (s *Pulls) resubmitStackedPullHelper( 354 w http.ResponseWriter, 355 r *http.Request, 356 repo *models.Repo, 357 userDid syntax.DID, 358 pull *models.Pull, 359 patch string, 360) { 361 l := s.logger.With("handler", "resubmitStackedPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 362 363 targetBranch := pull.TargetBranch 364 365 origStack, _ := r.Context().Value("stack").(models.Stack) 366 367 formatPatches, err := patchutil.ExtractPatches(patch) 368 if err != nil { 369 l.Error("failed to extract patches", "err", err) 370 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Failed to parse patches.") 371 return 372 } 373 374 // must have atleast 1 patch to begin with 375 if len(formatPatches) == 0 { 376 l.Error("no patches found in the generated format-patch") 377 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request: No patches found in the generated patch.") 378 return 379 } 380 381 client, err := s.oauth.AuthorizedClient(r) 382 if err != nil { 383 l.Error("failed to get authorized client", "err", err) 384 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 385 return 386 } 387 388 // first upload all blobs 389 blobs := make([]*lexutil.LexBlob, len(formatPatches)) 390 for i, p := range formatPatches { 391 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 392 if err != nil { 393 l.Error("failed to upload patch blob", "err", err, "patch_index", i) 394 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 395 return 396 } 397 l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 398 blobs[i] = blob.Blob 399 } 400 401 newStack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pull.PullSource, formatPatches, blobs, nil, nil) 402 if err != nil { 403 l.Error("failed to create resubmitted stack", "err", err) 404 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 405 return 406 } 407 408 // find the diff between the stacks, first, map them by changeId 409 origById := make(map[string]*models.Pull) 410 newById := make(map[string]*models.Pull) 411 for _, p := range origStack { 412 origById[p.LatestSubmission().ChangeId()] = p 413 } 414 for _, p := range newStack { 415 newById[p.LatestSubmission().ChangeId()] = p 416 } 417 418 // commits that got deleted: corresponding pull is closed 419 // commits that got added: new pull is created 420 // commits that got updated: corresponding pull is resubmitted & new round begins 421 additions := make(map[string]*models.Pull) 422 deletions := make(map[string]*models.Pull) 423 updated := make(map[string]struct{}) 424 425 // pulls in original stack but not in new one 426 for _, op := range origStack { 427 if _, ok := newById[op.LatestSubmission().ChangeId()]; !ok { 428 deletions[op.LatestSubmission().ChangeId()] = op 429 } 430 } 431 432 // pulls in new stack but not in original one 433 for _, np := range newStack { 434 if _, ok := origById[np.LatestSubmission().ChangeId()]; !ok { 435 additions[np.LatestSubmission().ChangeId()] = np 436 } 437 } 438 439 // NOTE: this loop can be written in any of above blocks, 440 // but is written separately in the interest of simpler code 441 for _, np := range newStack { 442 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 443 // pull exists in both stacks 444 updated[op.LatestSubmission().ChangeId()] = struct{}{} 445 } 446 } 447 448 // NOTE: we can go through the newStack and update dependent relations and 449 // rkeys now that we know which ones have been updated 450 // update dependentOn relations for the entire stack 451 var parentAt *syntax.ATURI 452 for _, np := range newStack { 453 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 454 // pull exists in both stacks 455 np.Rkey = op.Rkey 456 } 457 np.DependentOn = parentAt 458 x := np.AtUri() 459 parentAt = &x 460 } 461 462 l = l.With("additions", len(additions), "deletions", len(deletions), "updates", len(updated)) 463 464 tx, err := s.db.Begin() 465 if err != nil { 466 l.Error("failed to start transaction", "err", err) 467 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 468 return 469 } 470 defer tx.Rollback() 471 472 // pds updates to make 473 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 474 475 // deleted pulls are marked as deleted in the DB 476 for _, p := range deletions { 477 // do not do delete already merged PRs 478 if p.State == models.PullMerged { 479 continue 480 } 481 482 err := db.AbandonPulls(tx, orm.FilterEq("repo_did", string(p.RepoDid)), orm.FilterEq("at_uri", p.AtUri())) 483 if err != nil { 484 l.Error("failed to delete pull", "err", err, "pull_id", p.PullId) 485 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 486 return 487 } 488 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 489 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 490 Collection: tangled.RepoPullNSID, 491 Rkey: p.Rkey, 492 }, 493 }) 494 } 495 496 // new pulls are created 497 for _, p := range additions { 498 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()), ApplicationGzip) 499 if err != nil { 500 l.Error("failed to upload patch blob for new pull", "err", err, "change_id", p.LatestSubmission().ChangeId()) 501 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 502 return 503 } 504 p.Submissions[0].Blob = *blob.Blob 505 506 if err = db.PutPull(tx, p); err != nil { 507 l.Error("failed to create pull", "err", err, "pull_id", p.PullId, "change_id", p.LatestSubmission().ChangeId()) 508 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 509 return 510 } 511 512 record := p.AsRecord() 513 record.Rounds = []*tangled.RepoPull_Round{ 514 { 515 CreatedAt: time.Now().Format(time.RFC3339), 516 PatchBlob: blob.Blob, 517 }, 518 } 519 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 520 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 521 Collection: tangled.RepoPullNSID, 522 Rkey: &p.Rkey, 523 Value: knotcompat.Pull(&record), 524 }, 525 }) 526 } 527 528 // updated pulls are, well, updated; to start a new round 529 for id := range updated { 530 op, _ := origById[id] 531 np, _ := newById[id] 532 533 // do not update already merged PRs 534 if op.State == models.PullMerged { 535 continue 536 } 537 538 // resubmit the new pull 539 np.Rkey = op.Rkey 540 pullAt := op.AtUri() 541 newRoundNumber := len(op.Submissions) 542 newPatch := np.LatestPatch() 543 combinedPatch := np.LatestSubmission().Combined 544 newSourceRev := np.LatestSha() 545 546 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(newPatch), ApplicationGzip) 547 if err != nil { 548 l.Error("failed to upload patch blob for update", "err", err, "change_id", id, "pull_id", op.PullId) 549 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 550 return 551 } 552 553 // create new round 554 err = db.ResubmitPull(tx, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, np.LatestSubmission().MergeBase, blob.Blob) 555 if err != nil { 556 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 557 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 558 return 559 } 560 561 // update dependent-on relation 562 if np.DependentOn != nil { 563 err := db.SetDependentOn(tx, *np.DependentOn, orm.FilterEq("at_uri", np.AtUri())) 564 if err != nil { 565 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 566 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 567 return 568 } 569 } 570 571 record := np.AsRecord() 572 record.Rounds = op.AsRecord().Rounds 573 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 574 CreatedAt: time.Now().Format(time.RFC3339), 575 PatchBlob: blob.Blob, 576 }) 577 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 578 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 579 Collection: tangled.RepoPullNSID, 580 Rkey: op.Rkey, 581 Value: knotcompat.Pull(&record), 582 }, 583 }) 584 } 585 586 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 587 Repo: userDid.String(), 588 Writes: writes, 589 }) 590 if err != nil { 591 l.Error("failed to apply writes for stacked pull request", "err", err, "writes_count", len(writes)) 592 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 593 return 594 } 595 596 err = tx.Commit() 597 if err != nil { 598 l.Error("failed to commit resubmit transaction", "err", err) 599 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 600 return 601 } 602 603 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 604 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 605}