Monorepo for Tangled
tangled.org
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}