Monorepo for Tangled tangled.org
4

Configure Feed

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

core / appview / pulls / resubmit.go
20 kB 634 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) 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) 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) { 268 l := s.logger.With("handler", "resubmitPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 269 270 stack := r.Context().Value("stack").(models.Stack) 271 if stack != nil && len(stack) != 1 { 272 l.Info("resubmitting stacked PR", "stack_size", len(stack)) 273 s.resubmitStackedPullHelper(w, r, repo, userDid, pull, patch) 274 return 275 } 276 277 if err := validatePatch(&patch); err != nil { 278 s.pages.Notice(w, "resubmit-error", err.Error()) 279 return 280 } 281 282 if patch == pull.LatestPatch() { 283 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 284 return 285 } 286 287 // validate sourceRev if branch/fork based 288 if pull.IsBranchBased() || pull.IsForkBased() { 289 if sourceRev == pull.LatestSha() { 290 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 291 return 292 } 293 } 294 295 pullAt := pull.AtUri() 296 newRoundNumber := len(pull.Submissions) 297 newPatch := patch 298 newSourceRev := sourceRev 299 combinedPatch := combined 300 301 client, err := s.oauth.AuthorizedClient(r) 302 if err != nil { 303 l.Error("failed to authorize client", "err", err) 304 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 305 return 306 } 307 308 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, userDid.String(), pull.Rkey) 309 if err != nil { 310 // failed to get record 311 l.Error("failed to get record from PDS", "err", err, "rkey", pull.Rkey) 312 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 313 return 314 } 315 316 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 317 if err != nil { 318 l.Error("failed to upload patch blob", "err", err) 319 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 320 return 321 } 322 record := pull.AsRecord() 323 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 324 CreatedAt: time.Now().Format(time.RFC3339), 325 PatchBlob: blob.Blob, 326 }) 327 328 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 329 Collection: tangled.RepoPullNSID, 330 Repo: userDid.String(), 331 Rkey: pull.Rkey, 332 SwapRecord: ex.Cid, 333 Record: knotcompat.Pull(&record), 334 }) 335 if err != nil { 336 l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey) 337 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 338 return 339 } 340 341 err = db.ResubmitPull(s.db, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 342 if err != nil { 343 l.Error("failed to resubmit pull request in database", "err", err, "round_number", newRoundNumber) 344 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 345 return 346 } 347 348 pull.Submissions = append(pull.Submissions, &models.PullSubmission{ 349 PullAt: pullAt, 350 RoundNumber: newRoundNumber, 351 Patch: newPatch, 352 Combined: combinedPatch, 353 SourceRev: newSourceRev, 354 Created: time.Now(), 355 }) 356 s.notifier.ResubmitPull(r.Context(), pull) 357 358 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 359 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 360} 361 362func (s *Pulls) resubmitStackedPullHelper( 363 w http.ResponseWriter, 364 r *http.Request, 365 repo *models.Repo, 366 userDid syntax.DID, 367 pull *models.Pull, 368 patch string, 369) { 370 l := s.logger.With("handler", "resubmitStackedPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 371 372 targetBranch := pull.TargetBranch 373 374 origStack, _ := r.Context().Value("stack").(models.Stack) 375 376 formatPatches, err := patchutil.ExtractPatches(patch) 377 if err != nil { 378 l.Error("failed to extract patches", "err", err) 379 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Failed to parse patches.") 380 return 381 } 382 383 // must have atleast 1 patch to begin with 384 if len(formatPatches) == 0 { 385 l.Error("no patches found in the generated format-patch") 386 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request: No patches found in the generated patch.") 387 return 388 } 389 390 client, err := s.oauth.AuthorizedClient(r) 391 if err != nil { 392 l.Error("failed to get authorized client", "err", err) 393 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 394 return 395 } 396 397 // first upload all blobs 398 blobs := make([]*lexutil.LexBlob, len(formatPatches)) 399 for i, p := range formatPatches { 400 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 401 if err != nil { 402 l.Error("failed to upload patch blob", "err", err, "patch_index", i) 403 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 404 return 405 } 406 l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 407 blobs[i] = blob.Blob 408 } 409 410 newStack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pull.PullSource, formatPatches, blobs, nil, nil) 411 if err != nil { 412 l.Error("failed to create resubmitted stack", "err", err) 413 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 414 return 415 } 416 417 // find the diff between the stacks, first, map them by changeId 418 origById := make(map[string]*models.Pull) 419 newById := make(map[string]*models.Pull) 420 for _, p := range origStack { 421 origById[p.LatestSubmission().ChangeId()] = p 422 } 423 for _, p := range newStack { 424 newById[p.LatestSubmission().ChangeId()] = p 425 } 426 427 // commits that got deleted: corresponding pull is closed 428 // commits that got added: new pull is created 429 // commits that got updated: corresponding pull is resubmitted & new round begins 430 additions := make(map[string]*models.Pull) 431 deletions := make(map[string]*models.Pull) 432 updated := make(map[string]struct{}) 433 434 // pulls in original stack but not in new one 435 for _, op := range origStack { 436 if _, ok := newById[op.LatestSubmission().ChangeId()]; !ok { 437 deletions[op.LatestSubmission().ChangeId()] = op 438 } 439 } 440 441 // pulls in new stack but not in original one 442 for _, np := range newStack { 443 if _, ok := origById[np.LatestSubmission().ChangeId()]; !ok { 444 additions[np.LatestSubmission().ChangeId()] = np 445 } 446 } 447 448 // NOTE: this loop can be written in any of above blocks, 449 // but is written separately in the interest of simpler code 450 for _, np := range newStack { 451 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 452 // pull exists in both stacks 453 updated[op.LatestSubmission().ChangeId()] = struct{}{} 454 } 455 } 456 457 // NOTE: we can go through the newStack and update dependent relations and 458 // rkeys now that we know which ones have been updated 459 // update dependentOn relations for the entire stack 460 var parentAt *syntax.ATURI 461 for _, np := range newStack { 462 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 463 // pull exists in both stacks 464 np.Rkey = op.Rkey 465 } 466 np.DependentOn = parentAt 467 x := np.AtUri() 468 parentAt = &x 469 } 470 471 l = l.With("additions", len(additions), "deletions", len(deletions), "updates", len(updated)) 472 473 tx, err := s.db.Begin() 474 if err != nil { 475 l.Error("failed to start transaction", "err", err) 476 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 477 return 478 } 479 defer tx.Rollback() 480 481 // pds updates to make 482 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 483 484 // pulls to notify for after the transaction commits 485 var resubmitted []*models.Pull 486 487 // deleted pulls are marked as deleted in the DB 488 for _, p := range deletions { 489 // do not do delete already merged PRs 490 if p.State == models.PullMerged { 491 continue 492 } 493 494 err := db.AbandonPulls(tx, orm.FilterEq("repo_did", string(p.RepoDid)), orm.FilterEq("at_uri", p.AtUri())) 495 if err != nil { 496 l.Error("failed to delete pull", "err", err, "pull_id", p.PullId) 497 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 498 return 499 } 500 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 501 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 502 Collection: tangled.RepoPullNSID, 503 Rkey: p.Rkey, 504 }, 505 }) 506 } 507 508 // new pulls are created 509 for _, p := range additions { 510 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()), ApplicationGzip) 511 if err != nil { 512 l.Error("failed to upload patch blob for new pull", "err", err, "change_id", p.LatestSubmission().ChangeId()) 513 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 514 return 515 } 516 p.Submissions[0].Blob = *blob.Blob 517 518 if err = db.PutPull(tx, p); err != nil { 519 l.Error("failed to create pull", "err", err, "pull_id", p.PullId, "change_id", p.LatestSubmission().ChangeId()) 520 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 521 return 522 } 523 524 record := p.AsRecord() 525 record.Rounds = []*tangled.RepoPull_Round{ 526 { 527 CreatedAt: time.Now().Format(time.RFC3339), 528 PatchBlob: blob.Blob, 529 }, 530 } 531 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 532 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 533 Collection: tangled.RepoPullNSID, 534 Rkey: &p.Rkey, 535 Value: knotcompat.Pull(&record), 536 }, 537 }) 538 } 539 540 // updated pulls are, well, updated; to start a new round 541 for id := range updated { 542 op, _ := origById[id] 543 np, _ := newById[id] 544 545 // do not update already merged PRs 546 if op.State == models.PullMerged { 547 continue 548 } 549 550 // resubmit the new pull 551 np.Rkey = op.Rkey 552 pullAt := op.AtUri() 553 newRoundNumber := len(op.Submissions) 554 newPatch := np.LatestPatch() 555 combinedPatch := np.LatestSubmission().Combined 556 newSourceRev := np.LatestSha() 557 558 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(newPatch), ApplicationGzip) 559 if err != nil { 560 l.Error("failed to upload patch blob for update", "err", err, "change_id", id, "pull_id", op.PullId) 561 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 562 return 563 } 564 565 // create new round 566 err = db.ResubmitPull(tx, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 567 if err != nil { 568 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 569 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 570 return 571 } 572 573 // update dependent-on relation 574 if np.DependentOn != nil { 575 err := db.SetDependentOn(tx, *np.DependentOn, orm.FilterEq("at_uri", np.AtUri())) 576 if err != nil { 577 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 578 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 579 return 580 } 581 } 582 583 record := np.AsRecord() 584 record.Rounds = op.AsRecord().Rounds 585 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 586 CreatedAt: time.Now().Format(time.RFC3339), 587 PatchBlob: blob.Blob, 588 }) 589 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 590 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 591 Collection: tangled.RepoPullNSID, 592 Rkey: op.Rkey, 593 Value: knotcompat.Pull(&record), 594 }, 595 }) 596 597 op.Submissions = append(op.Submissions, &models.PullSubmission{ 598 PullAt: pullAt, 599 RoundNumber: newRoundNumber, 600 Patch: newPatch, 601 Combined: combinedPatch, 602 SourceRev: newSourceRev, 603 Created: time.Now(), 604 }) 605 resubmitted = append(resubmitted, op) 606 } 607 608 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 609 Repo: userDid.String(), 610 Writes: writes, 611 }) 612 if err != nil { 613 l.Error("failed to apply writes for stacked pull request", "err", err, "writes_count", len(writes)) 614 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 615 return 616 } 617 618 err = tx.Commit() 619 if err != nil { 620 l.Error("failed to commit resubmit transaction", "err", err) 621 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 622 return 623 } 624 625 for _, p := range additions { 626 s.notifier.NewPull(r.Context(), p) 627 } 628 for _, p := range resubmitted { 629 s.notifier.ResubmitPull(r.Context(), p) 630 } 631 632 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 633 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 634}