this repo has no description
1package pulls
2
3import (
4 "database/sql"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "sort"
12 "strconv"
13 "strings"
14 "time"
15
16 "tangled.sh/tangled.sh/core/api/tangled"
17 "tangled.sh/tangled.sh/core/appview/config"
18 "tangled.sh/tangled.sh/core/appview/db"
19 "tangled.sh/tangled.sh/core/appview/notify"
20 "tangled.sh/tangled.sh/core/appview/oauth"
21 "tangled.sh/tangled.sh/core/appview/pages"
22 "tangled.sh/tangled.sh/core/appview/reporesolver"
23 "tangled.sh/tangled.sh/core/idresolver"
24 "tangled.sh/tangled.sh/core/knotclient"
25 "tangled.sh/tangled.sh/core/patchutil"
26 "tangled.sh/tangled.sh/core/tid"
27 "tangled.sh/tangled.sh/core/types"
28
29 "github.com/bluekeyes/go-gitdiff/gitdiff"
30 comatproto "github.com/bluesky-social/indigo/api/atproto"
31 lexutil "github.com/bluesky-social/indigo/lex/util"
32 "github.com/go-chi/chi/v5"
33 "github.com/google/uuid"
34)
35
36type Pulls struct {
37 oauth *oauth.OAuth
38 repoResolver *reporesolver.RepoResolver
39 pages *pages.Pages
40 idResolver *idresolver.Resolver
41 db *db.DB
42 config *config.Config
43 notifier notify.Notifier
44}
45
46func New(
47 oauth *oauth.OAuth,
48 repoResolver *reporesolver.RepoResolver,
49 pages *pages.Pages,
50 resolver *idresolver.Resolver,
51 db *db.DB,
52 config *config.Config,
53 notifier notify.Notifier,
54) *Pulls {
55 return &Pulls{
56 oauth: oauth,
57 repoResolver: repoResolver,
58 pages: pages,
59 idResolver: resolver,
60 db: db,
61 config: config,
62 notifier: notifier,
63 }
64}
65
66// htmx fragment
67func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
68 switch r.Method {
69 case http.MethodGet:
70 user := s.oauth.GetUser(r)
71 f, err := s.repoResolver.Resolve(r)
72 if err != nil {
73 log.Println("failed to get repo and knot", err)
74 return
75 }
76
77 pull, ok := r.Context().Value("pull").(*db.Pull)
78 if !ok {
79 log.Println("failed to get pull")
80 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
81 return
82 }
83
84 // can be nil if this pull is not stacked
85 stack, _ := r.Context().Value("stack").(db.Stack)
86
87 roundNumberStr := chi.URLParam(r, "round")
88 roundNumber, err := strconv.Atoi(roundNumberStr)
89 if err != nil {
90 roundNumber = pull.LastRoundNumber()
91 }
92 if roundNumber >= len(pull.Submissions) {
93 http.Error(w, "bad round id", http.StatusBadRequest)
94 log.Println("failed to parse round id", err)
95 return
96 }
97
98 mergeCheckResponse := s.mergeCheck(f, pull, stack)
99 resubmitResult := pages.Unknown
100 if user.Did == pull.OwnerDid {
101 resubmitResult = s.resubmitCheck(f, pull, stack)
102 }
103
104 s.pages.PullActionsFragment(w, pages.PullActionsParams{
105 LoggedInUser: user,
106 RepoInfo: f.RepoInfo(user),
107 Pull: pull,
108 RoundNumber: roundNumber,
109 MergeCheck: mergeCheckResponse,
110 ResubmitCheck: resubmitResult,
111 Stack: stack,
112 })
113 return
114 }
115}
116
117func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
118 user := s.oauth.GetUser(r)
119 f, err := s.repoResolver.Resolve(r)
120 if err != nil {
121 log.Println("failed to get repo and knot", err)
122 return
123 }
124
125 pull, ok := r.Context().Value("pull").(*db.Pull)
126 if !ok {
127 log.Println("failed to get pull")
128 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
129 return
130 }
131
132 // can be nil if this pull is not stacked
133 stack, _ := r.Context().Value("stack").(db.Stack)
134 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*db.Pull)
135
136 totalIdents := 1
137 for _, submission := range pull.Submissions {
138 totalIdents += len(submission.Comments)
139 }
140
141 identsToResolve := make([]string, totalIdents)
142
143 // populate idents
144 identsToResolve[0] = pull.OwnerDid
145 idx := 1
146 for _, submission := range pull.Submissions {
147 for _, comment := range submission.Comments {
148 identsToResolve[idx] = comment.OwnerDid
149 idx += 1
150 }
151 }
152
153 mergeCheckResponse := s.mergeCheck(f, pull, stack)
154 resubmitResult := pages.Unknown
155 if user != nil && user.Did == pull.OwnerDid {
156 resubmitResult = s.resubmitCheck(f, pull, stack)
157 }
158
159 repoInfo := f.RepoInfo(user)
160
161 m := make(map[string]db.Pipeline)
162
163 var shas []string
164 for _, s := range pull.Submissions {
165 shas = append(shas, s.SourceRev)
166 }
167 for _, p := range stack {
168 shas = append(shas, p.LatestSha())
169 }
170 for _, p := range abandonedPulls {
171 shas = append(shas, p.LatestSha())
172 }
173
174 ps, err := db.GetPipelineStatuses(
175 s.db,
176 db.FilterEq("repo_owner", repoInfo.OwnerDid),
177 db.FilterEq("repo_name", repoInfo.Name),
178 db.FilterEq("knot", repoInfo.Knot),
179 db.FilterIn("sha", shas),
180 )
181 if err != nil {
182 log.Printf("failed to fetch pipeline statuses: %s", err)
183 // non-fatal
184 }
185
186 for _, p := range ps {
187 m[p.Sha] = p
188 }
189
190 reactionCountMap, err := db.GetReactionCountMap(s.db, pull.PullAt())
191 if err != nil {
192 log.Println("failed to get pull reactions")
193 s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.")
194 }
195
196 userReactions := map[db.ReactionKind]bool{}
197 if user != nil {
198 userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt())
199 }
200
201 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
202 LoggedInUser: user,
203 RepoInfo: repoInfo,
204 Pull: pull,
205 Stack: stack,
206 AbandonedPulls: abandonedPulls,
207 MergeCheck: mergeCheckResponse,
208 ResubmitCheck: resubmitResult,
209 Pipelines: m,
210
211 OrderedReactionKinds: db.OrderedReactionKinds,
212 Reactions: reactionCountMap,
213 UserReacted: userReactions,
214 })
215}
216
217func (s *Pulls) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse {
218 if pull.State == db.PullMerged {
219 return types.MergeCheckResponse{}
220 }
221
222 secret, err := db.GetRegistrationKey(s.db, f.Knot)
223 if err != nil {
224 log.Printf("failed to get registration key: %v", err)
225 return types.MergeCheckResponse{
226 Error: "failed to check merge status: this knot is unregistered",
227 }
228 }
229
230 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
231 if err != nil {
232 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
233 return types.MergeCheckResponse{
234 Error: "failed to check merge status",
235 }
236 }
237
238 patch := pull.LatestPatch()
239 if pull.IsStacked() {
240 // combine patches of substack
241 subStack := stack.Below(pull)
242 // collect the portion of the stack that is mergeable
243 mergeable := subStack.Mergeable()
244 // combine each patch
245 patch = mergeable.CombinedPatch()
246 }
247
248 resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch)
249 if err != nil {
250 log.Println("failed to check for mergeability:", err)
251 return types.MergeCheckResponse{
252 Error: "failed to check merge status",
253 }
254 }
255 switch resp.StatusCode {
256 case 404:
257 return types.MergeCheckResponse{
258 Error: "failed to check merge status: this knot does not support PRs",
259 }
260 case 400:
261 return types.MergeCheckResponse{
262 Error: "failed to check merge status: does this knot support PRs?",
263 }
264 }
265
266 respBody, err := io.ReadAll(resp.Body)
267 if err != nil {
268 log.Println("failed to read merge check response body")
269 return types.MergeCheckResponse{
270 Error: "failed to check merge status: knot is not speaking the right language",
271 }
272 }
273 defer resp.Body.Close()
274
275 var mergeCheckResponse types.MergeCheckResponse
276 err = json.Unmarshal(respBody, &mergeCheckResponse)
277 if err != nil {
278 log.Println("failed to unmarshal merge check response", err)
279 return types.MergeCheckResponse{
280 Error: "failed to check merge status: knot is not speaking the right language",
281 }
282 }
283
284 return mergeCheckResponse
285}
286
287func (s *Pulls) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult {
288 if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil {
289 return pages.Unknown
290 }
291
292 var knot, ownerDid, repoName string
293
294 if pull.PullSource.RepoAt != nil {
295 // fork-based pulls
296 sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
297 if err != nil {
298 log.Println("failed to get source repo", err)
299 return pages.Unknown
300 }
301
302 knot = sourceRepo.Knot
303 ownerDid = sourceRepo.Did
304 repoName = sourceRepo.Name
305 } else {
306 // pulls within the same repo
307 knot = f.Knot
308 ownerDid = f.OwnerDid()
309 repoName = f.Name
310 }
311
312 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
313 if err != nil {
314 log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
315 return pages.Unknown
316 }
317
318 result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
319 if err != nil {
320 log.Println("failed to reach knotserver", err)
321 return pages.Unknown
322 }
323
324 latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev
325
326 if pull.IsStacked() && stack != nil {
327 top := stack[0]
328 latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev
329 }
330
331 if latestSourceRev != result.Branch.Hash {
332 return pages.ShouldResubmit
333 }
334
335 return pages.ShouldNotResubmit
336}
337
338func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
339 user := s.oauth.GetUser(r)
340 f, err := s.repoResolver.Resolve(r)
341 if err != nil {
342 log.Println("failed to get repo and knot", err)
343 return
344 }
345
346 var diffOpts types.DiffOpts
347 if d := r.URL.Query().Get("diff"); d == "split" {
348 diffOpts.Split = true
349 }
350
351 pull, ok := r.Context().Value("pull").(*db.Pull)
352 if !ok {
353 log.Println("failed to get pull")
354 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
355 return
356 }
357
358 stack, _ := r.Context().Value("stack").(db.Stack)
359
360 roundId := chi.URLParam(r, "round")
361 roundIdInt, err := strconv.Atoi(roundId)
362 if err != nil || roundIdInt >= len(pull.Submissions) {
363 http.Error(w, "bad round id", http.StatusBadRequest)
364 log.Println("failed to parse round id", err)
365 return
366 }
367
368 patch := pull.Submissions[roundIdInt].Patch
369 diff := patchutil.AsNiceDiff(patch, pull.TargetBranch)
370
371 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
372 LoggedInUser: user,
373 RepoInfo: f.RepoInfo(user),
374 Pull: pull,
375 Stack: stack,
376 Round: roundIdInt,
377 Submission: pull.Submissions[roundIdInt],
378 Diff: &diff,
379 DiffOpts: diffOpts,
380 })
381
382}
383
384func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
385 user := s.oauth.GetUser(r)
386
387 f, err := s.repoResolver.Resolve(r)
388 if err != nil {
389 log.Println("failed to get repo and knot", err)
390 return
391 }
392
393 var diffOpts types.DiffOpts
394 if d := r.URL.Query().Get("diff"); d == "split" {
395 diffOpts.Split = true
396 }
397
398 pull, ok := r.Context().Value("pull").(*db.Pull)
399 if !ok {
400 log.Println("failed to get pull")
401 s.pages.Notice(w, "pull-error", "Failed to get pull.")
402 return
403 }
404
405 roundId := chi.URLParam(r, "round")
406 roundIdInt, err := strconv.Atoi(roundId)
407 if err != nil || roundIdInt >= len(pull.Submissions) {
408 http.Error(w, "bad round id", http.StatusBadRequest)
409 log.Println("failed to parse round id", err)
410 return
411 }
412
413 if roundIdInt == 0 {
414 http.Error(w, "bad round id", http.StatusBadRequest)
415 log.Println("cannot interdiff initial submission")
416 return
417 }
418
419 currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch)
420 if err != nil {
421 log.Println("failed to interdiff; current patch malformed")
422 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
423 return
424 }
425
426 previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch)
427 if err != nil {
428 log.Println("failed to interdiff; previous patch malformed")
429 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
430 return
431 }
432
433 interdiff := patchutil.Interdiff(previousPatch, currentPatch)
434
435 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
436 LoggedInUser: s.oauth.GetUser(r),
437 RepoInfo: f.RepoInfo(user),
438 Pull: pull,
439 Round: roundIdInt,
440 Interdiff: interdiff,
441 DiffOpts: diffOpts,
442 })
443}
444
445func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
446 pull, ok := r.Context().Value("pull").(*db.Pull)
447 if !ok {
448 log.Println("failed to get pull")
449 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
450 return
451 }
452
453 roundId := chi.URLParam(r, "round")
454 roundIdInt, err := strconv.Atoi(roundId)
455 if err != nil || roundIdInt >= len(pull.Submissions) {
456 http.Error(w, "bad round id", http.StatusBadRequest)
457 log.Println("failed to parse round id", err)
458 return
459 }
460
461 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
462 w.Write([]byte(pull.Submissions[roundIdInt].Patch))
463}
464
465func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
466 user := s.oauth.GetUser(r)
467 params := r.URL.Query()
468
469 state := db.PullOpen
470 switch params.Get("state") {
471 case "closed":
472 state = db.PullClosed
473 case "merged":
474 state = db.PullMerged
475 }
476
477 f, err := s.repoResolver.Resolve(r)
478 if err != nil {
479 log.Println("failed to get repo and knot", err)
480 return
481 }
482
483 pulls, err := db.GetPulls(
484 s.db,
485 db.FilterEq("repo_at", f.RepoAt()),
486 db.FilterEq("state", state),
487 )
488 if err != nil {
489 log.Println("failed to get pulls", err)
490 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
491 return
492 }
493
494 for _, p := range pulls {
495 var pullSourceRepo *db.Repo
496 if p.PullSource != nil {
497 if p.PullSource.RepoAt != nil {
498 pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String())
499 if err != nil {
500 log.Printf("failed to get repo by at uri: %v", err)
501 continue
502 } else {
503 p.PullSource.Repo = pullSourceRepo
504 }
505 }
506 }
507 }
508
509 // we want to group all stacked PRs into just one list
510 stacks := make(map[string]db.Stack)
511 var shas []string
512 n := 0
513 for _, p := range pulls {
514 // store the sha for later
515 shas = append(shas, p.LatestSha())
516 // this PR is stacked
517 if p.StackId != "" {
518 // we have already seen this PR stack
519 if _, seen := stacks[p.StackId]; seen {
520 stacks[p.StackId] = append(stacks[p.StackId], p)
521 // skip this PR
522 } else {
523 stacks[p.StackId] = nil
524 pulls[n] = p
525 n++
526 }
527 } else {
528 pulls[n] = p
529 n++
530 }
531 }
532 pulls = pulls[:n]
533
534 repoInfo := f.RepoInfo(user)
535 ps, err := db.GetPipelineStatuses(
536 s.db,
537 db.FilterEq("repo_owner", repoInfo.OwnerDid),
538 db.FilterEq("repo_name", repoInfo.Name),
539 db.FilterEq("knot", repoInfo.Knot),
540 db.FilterIn("sha", shas),
541 )
542 if err != nil {
543 log.Printf("failed to fetch pipeline statuses: %s", err)
544 // non-fatal
545 }
546 m := make(map[string]db.Pipeline)
547 for _, p := range ps {
548 m[p.Sha] = p
549 }
550
551 s.pages.RepoPulls(w, pages.RepoPullsParams{
552 LoggedInUser: s.oauth.GetUser(r),
553 RepoInfo: f.RepoInfo(user),
554 Pulls: pulls,
555 FilteringBy: state,
556 Stacks: stacks,
557 Pipelines: m,
558 })
559}
560
561func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
562 user := s.oauth.GetUser(r)
563 f, err := s.repoResolver.Resolve(r)
564 if err != nil {
565 log.Println("failed to get repo and knot", err)
566 return
567 }
568
569 pull, ok := r.Context().Value("pull").(*db.Pull)
570 if !ok {
571 log.Println("failed to get pull")
572 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
573 return
574 }
575
576 roundNumberStr := chi.URLParam(r, "round")
577 roundNumber, err := strconv.Atoi(roundNumberStr)
578 if err != nil || roundNumber >= len(pull.Submissions) {
579 http.Error(w, "bad round id", http.StatusBadRequest)
580 log.Println("failed to parse round id", err)
581 return
582 }
583
584 switch r.Method {
585 case http.MethodGet:
586 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
587 LoggedInUser: user,
588 RepoInfo: f.RepoInfo(user),
589 Pull: pull,
590 RoundNumber: roundNumber,
591 })
592 return
593 case http.MethodPost:
594 body := r.FormValue("body")
595 if body == "" {
596 s.pages.Notice(w, "pull", "Comment body is required")
597 return
598 }
599
600 // Start a transaction
601 tx, err := s.db.BeginTx(r.Context(), nil)
602 if err != nil {
603 log.Println("failed to start transaction", err)
604 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
605 return
606 }
607 defer tx.Rollback()
608
609 createdAt := time.Now().Format(time.RFC3339)
610 ownerDid := user.Did
611
612 pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId)
613 if err != nil {
614 log.Println("failed to get pull at", err)
615 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
616 return
617 }
618
619 atUri := f.RepoAt().String()
620 client, err := s.oauth.AuthorizedClient(r)
621 if err != nil {
622 log.Println("failed to get authorized client", err)
623 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
624 return
625 }
626 atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
627 Collection: tangled.RepoPullCommentNSID,
628 Repo: user.Did,
629 Rkey: tid.TID(),
630 Record: &lexutil.LexiconTypeDecoder{
631 Val: &tangled.RepoPullComment{
632 Repo: &atUri,
633 Pull: string(pullAt),
634 Owner: &ownerDid,
635 Body: body,
636 CreatedAt: createdAt,
637 },
638 },
639 })
640 if err != nil {
641 log.Println("failed to create pull comment", err)
642 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
643 return
644 }
645
646 comment := &db.PullComment{
647 OwnerDid: user.Did,
648 RepoAt: f.RepoAt().String(),
649 PullId: pull.PullId,
650 Body: body,
651 CommentAt: atResp.Uri,
652 SubmissionId: pull.Submissions[roundNumber].ID,
653 }
654
655 // Create the pull comment in the database with the commentAt field
656 commentId, err := db.NewPullComment(tx, comment)
657 if err != nil {
658 log.Println("failed to create pull comment", err)
659 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
660 return
661 }
662
663 // Commit the transaction
664 if err = tx.Commit(); err != nil {
665 log.Println("failed to commit transaction", err)
666 s.pages.Notice(w, "pull-comment", "Failed to create comment.")
667 return
668 }
669
670 s.notifier.NewPullComment(r.Context(), comment)
671
672 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
673 return
674 }
675}
676
677func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) {
678 user := s.oauth.GetUser(r)
679 f, err := s.repoResolver.Resolve(r)
680 if err != nil {
681 log.Println("failed to get repo and knot", err)
682 return
683 }
684
685 switch r.Method {
686 case http.MethodGet:
687 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
688 if err != nil {
689 log.Printf("failed to create unsigned client for %s", f.Knot)
690 s.pages.Error503(w)
691 return
692 }
693
694 result, err := us.Branches(f.OwnerDid(), f.Name)
695 if err != nil {
696 log.Println("failed to fetch branches", err)
697 return
698 }
699
700 // can be one of "patch", "branch" or "fork"
701 strategy := r.URL.Query().Get("strategy")
702 // ignored if strategy is "patch"
703 sourceBranch := r.URL.Query().Get("sourceBranch")
704 targetBranch := r.URL.Query().Get("targetBranch")
705
706 s.pages.RepoNewPull(w, pages.RepoNewPullParams{
707 LoggedInUser: user,
708 RepoInfo: f.RepoInfo(user),
709 Branches: result.Branches,
710 Strategy: strategy,
711 SourceBranch: sourceBranch,
712 TargetBranch: targetBranch,
713 Title: r.URL.Query().Get("title"),
714 Body: r.URL.Query().Get("body"),
715 })
716
717 case http.MethodPost:
718 title := r.FormValue("title")
719 body := r.FormValue("body")
720 targetBranch := r.FormValue("targetBranch")
721 fromFork := r.FormValue("fork")
722 sourceBranch := r.FormValue("sourceBranch")
723 patch := r.FormValue("patch")
724
725 if targetBranch == "" {
726 s.pages.Notice(w, "pull", "Target branch is required.")
727 return
728 }
729
730 // Determine PR type based on input parameters
731 isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed()
732 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
733 isForkBased := fromFork != "" && sourceBranch != ""
734 isPatchBased := patch != "" && !isBranchBased && !isForkBased
735 isStacked := r.FormValue("isStacked") == "on"
736
737 if isPatchBased && !patchutil.IsFormatPatch(patch) {
738 if title == "" {
739 s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
740 return
741 }
742 }
743
744 // Validate we have at least one valid PR creation method
745 if !isBranchBased && !isPatchBased && !isForkBased {
746 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
747 return
748 }
749
750 // Can't mix branch-based and patch-based approaches
751 if isBranchBased && patch != "" {
752 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
753 return
754 }
755
756 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
757 if err != nil {
758 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
759 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
760 return
761 }
762
763 caps, err := us.Capabilities()
764 if err != nil {
765 log.Println("error fetching knot caps", f.Knot, err)
766 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
767 return
768 }
769
770 if !caps.PullRequests.FormatPatch {
771 s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.")
772 return
773 }
774
775 // Handle the PR creation based on the type
776 if isBranchBased {
777 if !caps.PullRequests.BranchSubmissions {
778 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
779 return
780 }
781 s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked)
782 } else if isForkBased {
783 if !caps.PullRequests.ForkSubmissions {
784 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
785 return
786 }
787 s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked)
788 } else if isPatchBased {
789 if !caps.PullRequests.PatchSubmissions {
790 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
791 return
792 }
793 s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked)
794 }
795 return
796 }
797}
798
799func (s *Pulls) handleBranchBasedPull(
800 w http.ResponseWriter,
801 r *http.Request,
802 f *reporesolver.ResolvedRepo,
803 user *oauth.User,
804 title,
805 body,
806 targetBranch,
807 sourceBranch string,
808 isStacked bool,
809) {
810 // Generate a patch using /compare
811 ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
812 if err != nil {
813 log.Printf("failed to create signed client for %s: %s", f.Knot, err)
814 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
815 return
816 }
817
818 comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch)
819 if err != nil {
820 log.Println("failed to compare", err)
821 s.pages.Notice(w, "pull", err.Error())
822 return
823 }
824
825 sourceRev := comparison.Rev2
826 patch := comparison.Patch
827
828 if !patchutil.IsPatchValid(patch) {
829 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
830 return
831 }
832
833 pullSource := &db.PullSource{
834 Branch: sourceBranch,
835 }
836 recordPullSource := &tangled.RepoPull_Source{
837 Branch: sourceBranch,
838 Sha: comparison.Rev2,
839 }
840
841 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
842}
843
844func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
845 if !patchutil.IsPatchValid(patch) {
846 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
847 return
848 }
849
850 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked)
851}
852
853func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
854 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
855 if errors.Is(err, sql.ErrNoRows) {
856 s.pages.Notice(w, "pull", "No such fork.")
857 return
858 } else if err != nil {
859 log.Println("failed to fetch fork:", err)
860 s.pages.Notice(w, "pull", "Failed to fetch fork.")
861 return
862 }
863
864 secret, err := db.GetRegistrationKey(s.db, fork.Knot)
865 if err != nil {
866 log.Println("failed to fetch registration key:", err)
867 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
868 return
869 }
870
871 sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
872 if err != nil {
873 log.Println("failed to create signed client:", err)
874 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
875 return
876 }
877
878 us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
879 if err != nil {
880 log.Println("failed to create unsigned client:", err)
881 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
882 return
883 }
884
885 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
886 if err != nil {
887 log.Println("failed to create hidden ref:", err, resp.StatusCode)
888 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
889 return
890 }
891
892 switch resp.StatusCode {
893 case 404:
894 case 400:
895 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
896 return
897 }
898
899 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)
900 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking
901 // the targetBranch on the target repository. This code is a bit confusing, but here's an example:
902 // hiddenRef: hidden/feature-1/main (on repo-fork)
903 // targetBranch: main (on repo-1)
904 // sourceBranch: feature-1 (on repo-fork)
905 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
906 if err != nil {
907 log.Println("failed to compare across branches", err)
908 s.pages.Notice(w, "pull", err.Error())
909 return
910 }
911
912 sourceRev := comparison.Rev2
913 patch := comparison.Patch
914
915 if !patchutil.IsPatchValid(patch) {
916 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
917 return
918 }
919
920 forkAtUri := fork.RepoAt()
921 forkAtUriStr := forkAtUri.String()
922
923 pullSource := &db.PullSource{
924 Branch: sourceBranch,
925 RepoAt: &forkAtUri,
926 }
927 recordPullSource := &tangled.RepoPull_Source{
928 Branch: sourceBranch,
929 Repo: &forkAtUriStr,
930 Sha: sourceRev,
931 }
932
933 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
934}
935
936func (s *Pulls) createPullRequest(
937 w http.ResponseWriter,
938 r *http.Request,
939 f *reporesolver.ResolvedRepo,
940 user *oauth.User,
941 title, body, targetBranch string,
942 patch string,
943 sourceRev string,
944 pullSource *db.PullSource,
945 recordPullSource *tangled.RepoPull_Source,
946 isStacked bool,
947) {
948 if isStacked {
949 // creates a series of PRs, each linking to the previous, identified by jj's change-id
950 s.createStackedPullRequest(
951 w,
952 r,
953 f,
954 user,
955 targetBranch,
956 patch,
957 sourceRev,
958 pullSource,
959 )
960 return
961 }
962
963 client, err := s.oauth.AuthorizedClient(r)
964 if err != nil {
965 log.Println("failed to get authorized client", err)
966 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
967 return
968 }
969
970 tx, err := s.db.BeginTx(r.Context(), nil)
971 if err != nil {
972 log.Println("failed to start tx")
973 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
974 return
975 }
976 defer tx.Rollback()
977
978 // We've already checked earlier if it's diff-based and title is empty,
979 // so if it's still empty now, it's intentionally skipped owing to format-patch.
980 if title == "" {
981 formatPatches, err := patchutil.ExtractPatches(patch)
982 if err != nil {
983 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
984 return
985 }
986 if len(formatPatches) == 0 {
987 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
988 return
989 }
990
991 title = formatPatches[0].Title
992 body = formatPatches[0].Body
993 }
994
995 rkey := tid.TID()
996 initialSubmission := db.PullSubmission{
997 Patch: patch,
998 SourceRev: sourceRev,
999 }
1000 pull := &db.Pull{
1001 Title: title,
1002 Body: body,
1003 TargetBranch: targetBranch,
1004 OwnerDid: user.Did,
1005 RepoAt: f.RepoAt(),
1006 Rkey: rkey,
1007 Submissions: []*db.PullSubmission{
1008 &initialSubmission,
1009 },
1010 PullSource: pullSource,
1011 }
1012 err = db.NewPull(tx, pull)
1013 if err != nil {
1014 log.Println("failed to create pull request", err)
1015 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1016 return
1017 }
1018 pullId, err := db.NextPullId(tx, f.RepoAt())
1019 if err != nil {
1020 log.Println("failed to get pull id", err)
1021 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1022 return
1023 }
1024
1025 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1026 Collection: tangled.RepoPullNSID,
1027 Repo: user.Did,
1028 Rkey: rkey,
1029 Record: &lexutil.LexiconTypeDecoder{
1030 Val: &tangled.RepoPull{
1031 Title: title,
1032 PullId: int64(pullId),
1033 TargetRepo: string(f.RepoAt()),
1034 TargetBranch: targetBranch,
1035 Patch: patch,
1036 Source: recordPullSource,
1037 },
1038 },
1039 })
1040 if err != nil {
1041 log.Println("failed to create pull request", err)
1042 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1043 return
1044 }
1045
1046 if err = tx.Commit(); err != nil {
1047 log.Println("failed to create pull request", err)
1048 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1049 return
1050 }
1051
1052 s.notifier.NewPull(r.Context(), pull)
1053
1054 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1055}
1056
1057func (s *Pulls) createStackedPullRequest(
1058 w http.ResponseWriter,
1059 r *http.Request,
1060 f *reporesolver.ResolvedRepo,
1061 user *oauth.User,
1062 targetBranch string,
1063 patch string,
1064 sourceRev string,
1065 pullSource *db.PullSource,
1066) {
1067 // run some necessary checks for stacked-prs first
1068
1069 // must be branch or fork based
1070 if sourceRev == "" {
1071 log.Println("stacked PR from patch-based pull")
1072 s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.")
1073 return
1074 }
1075
1076 formatPatches, err := patchutil.ExtractPatches(patch)
1077 if err != nil {
1078 log.Println("failed to extract patches", err)
1079 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
1080 return
1081 }
1082
1083 // must have atleast 1 patch to begin with
1084 if len(formatPatches) == 0 {
1085 log.Println("empty patches")
1086 s.pages.Notice(w, "pull", "No patches found in the generated format-patch.")
1087 return
1088 }
1089
1090 // build a stack out of this patch
1091 stackId := uuid.New()
1092 stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1093 if err != nil {
1094 log.Println("failed to create stack", err)
1095 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
1096 return
1097 }
1098
1099 client, err := s.oauth.AuthorizedClient(r)
1100 if err != nil {
1101 log.Println("failed to get authorized client", err)
1102 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1103 return
1104 }
1105
1106 // apply all record creations at once
1107 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1108 for _, p := range stack {
1109 record := p.AsRecord()
1110 write := comatproto.RepoApplyWrites_Input_Writes_Elem{
1111 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1112 Collection: tangled.RepoPullNSID,
1113 Rkey: &p.Rkey,
1114 Value: &lexutil.LexiconTypeDecoder{
1115 Val: &record,
1116 },
1117 },
1118 }
1119 writes = append(writes, &write)
1120 }
1121 _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1122 Repo: user.Did,
1123 Writes: writes,
1124 })
1125 if err != nil {
1126 log.Println("failed to create stacked pull request", err)
1127 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.")
1128 return
1129 }
1130
1131 // create all pulls at once
1132 tx, err := s.db.BeginTx(r.Context(), nil)
1133 if err != nil {
1134 log.Println("failed to start tx")
1135 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1136 return
1137 }
1138 defer tx.Rollback()
1139
1140 for _, p := range stack {
1141 err = db.NewPull(tx, p)
1142 if err != nil {
1143 log.Println("failed to create pull request", err)
1144 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1145 return
1146 }
1147 }
1148
1149 if err = tx.Commit(); err != nil {
1150 log.Println("failed to create pull request", err)
1151 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1152 return
1153 }
1154
1155 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1156}
1157
1158func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) {
1159 _, err := s.repoResolver.Resolve(r)
1160 if err != nil {
1161 log.Println("failed to get repo and knot", err)
1162 return
1163 }
1164
1165 patch := r.FormValue("patch")
1166 if patch == "" {
1167 s.pages.Notice(w, "patch-error", "Patch is required.")
1168 return
1169 }
1170
1171 if patch == "" || !patchutil.IsPatchValid(patch) {
1172 s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")
1173 return
1174 }
1175
1176 if patchutil.IsFormatPatch(patch) {
1177 s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.")
1178 } else {
1179 s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
1180 }
1181}
1182
1183func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1184 user := s.oauth.GetUser(r)
1185 f, err := s.repoResolver.Resolve(r)
1186 if err != nil {
1187 log.Println("failed to get repo and knot", err)
1188 return
1189 }
1190
1191 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1192 RepoInfo: f.RepoInfo(user),
1193 })
1194}
1195
1196func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1197 user := s.oauth.GetUser(r)
1198 f, err := s.repoResolver.Resolve(r)
1199 if err != nil {
1200 log.Println("failed to get repo and knot", err)
1201 return
1202 }
1203
1204 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1205 if err != nil {
1206 log.Printf("failed to create unsigned client for %s", f.Knot)
1207 s.pages.Error503(w)
1208 return
1209 }
1210
1211 result, err := us.Branches(f.OwnerDid(), f.Name)
1212 if err != nil {
1213 log.Println("failed to reach knotserver", err)
1214 return
1215 }
1216
1217 branches := result.Branches
1218 sort.Slice(branches, func(i int, j int) bool {
1219 return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When)
1220 })
1221
1222 withoutDefault := []types.Branch{}
1223 for _, b := range branches {
1224 if b.IsDefault {
1225 continue
1226 }
1227 withoutDefault = append(withoutDefault, b)
1228 }
1229
1230 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1231 RepoInfo: f.RepoInfo(user),
1232 Branches: withoutDefault,
1233 })
1234}
1235
1236func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1237 user := s.oauth.GetUser(r)
1238 f, err := s.repoResolver.Resolve(r)
1239 if err != nil {
1240 log.Println("failed to get repo and knot", err)
1241 return
1242 }
1243
1244 forks, err := db.GetForksByDid(s.db, user.Did)
1245 if err != nil {
1246 log.Println("failed to get forks", err)
1247 return
1248 }
1249
1250 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1251 RepoInfo: f.RepoInfo(user),
1252 Forks: forks,
1253 Selected: r.URL.Query().Get("fork"),
1254 })
1255}
1256
1257func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1258 user := s.oauth.GetUser(r)
1259
1260 f, err := s.repoResolver.Resolve(r)
1261 if err != nil {
1262 log.Println("failed to get repo and knot", err)
1263 return
1264 }
1265
1266 forkVal := r.URL.Query().Get("fork")
1267
1268 // fork repo
1269 repo, err := db.GetRepo(s.db, user.Did, forkVal)
1270 if err != nil {
1271 log.Println("failed to get repo", user.Did, forkVal)
1272 return
1273 }
1274
1275 sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1276 if err != nil {
1277 log.Printf("failed to create unsigned client for %s", repo.Knot)
1278 s.pages.Error503(w)
1279 return
1280 }
1281
1282 sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name)
1283 if err != nil {
1284 log.Println("failed to reach knotserver for source branches", err)
1285 return
1286 }
1287
1288 targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1289 if err != nil {
1290 log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1291 s.pages.Error503(w)
1292 return
1293 }
1294
1295 targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name)
1296 if err != nil {
1297 log.Println("failed to reach knotserver for target branches", err)
1298 return
1299 }
1300
1301 sourceBranches := sourceResult.Branches
1302 sort.Slice(sourceBranches, func(i int, j int) bool {
1303 return sourceBranches[i].Commit.Committer.When.After(sourceBranches[j].Commit.Committer.When)
1304 })
1305
1306 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1307 RepoInfo: f.RepoInfo(user),
1308 SourceBranches: sourceBranches,
1309 TargetBranches: targetResult.Branches,
1310 })
1311}
1312
1313func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1314 user := s.oauth.GetUser(r)
1315 f, err := s.repoResolver.Resolve(r)
1316 if err != nil {
1317 log.Println("failed to get repo and knot", err)
1318 return
1319 }
1320
1321 pull, ok := r.Context().Value("pull").(*db.Pull)
1322 if !ok {
1323 log.Println("failed to get pull")
1324 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1325 return
1326 }
1327
1328 switch r.Method {
1329 case http.MethodGet:
1330 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1331 RepoInfo: f.RepoInfo(user),
1332 Pull: pull,
1333 })
1334 return
1335 case http.MethodPost:
1336 if pull.IsPatchBased() {
1337 s.resubmitPatch(w, r)
1338 return
1339 } else if pull.IsBranchBased() {
1340 s.resubmitBranch(w, r)
1341 return
1342 } else if pull.IsForkBased() {
1343 s.resubmitFork(w, r)
1344 return
1345 }
1346 }
1347}
1348
1349func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1350 user := s.oauth.GetUser(r)
1351
1352 pull, ok := r.Context().Value("pull").(*db.Pull)
1353 if !ok {
1354 log.Println("failed to get pull")
1355 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1356 return
1357 }
1358
1359 f, err := s.repoResolver.Resolve(r)
1360 if err != nil {
1361 log.Println("failed to get repo and knot", err)
1362 return
1363 }
1364
1365 if user.Did != pull.OwnerDid {
1366 log.Println("unauthorized user")
1367 w.WriteHeader(http.StatusUnauthorized)
1368 return
1369 }
1370
1371 patch := r.FormValue("patch")
1372
1373 s.resubmitPullHelper(w, r, f, user, pull, patch, "")
1374}
1375
1376func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1377 user := s.oauth.GetUser(r)
1378
1379 pull, ok := r.Context().Value("pull").(*db.Pull)
1380 if !ok {
1381 log.Println("failed to get pull")
1382 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1383 return
1384 }
1385
1386 f, err := s.repoResolver.Resolve(r)
1387 if err != nil {
1388 log.Println("failed to get repo and knot", err)
1389 return
1390 }
1391
1392 if user.Did != pull.OwnerDid {
1393 log.Println("unauthorized user")
1394 w.WriteHeader(http.StatusUnauthorized)
1395 return
1396 }
1397
1398 if !f.RepoInfo(user).Roles.IsPushAllowed() {
1399 log.Println("unauthorized user")
1400 w.WriteHeader(http.StatusUnauthorized)
1401 return
1402 }
1403
1404 ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1405 if err != nil {
1406 log.Printf("failed to create client for %s: %s", f.Knot, err)
1407 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1408 return
1409 }
1410
1411 comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch)
1412 if err != nil {
1413 log.Printf("compare request failed: %s", err)
1414 s.pages.Notice(w, "resubmit-error", err.Error())
1415 return
1416 }
1417
1418 sourceRev := comparison.Rev2
1419 patch := comparison.Patch
1420
1421 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1422}
1423
1424func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
1425 user := s.oauth.GetUser(r)
1426
1427 pull, ok := r.Context().Value("pull").(*db.Pull)
1428 if !ok {
1429 log.Println("failed to get pull")
1430 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1431 return
1432 }
1433
1434 f, err := s.repoResolver.Resolve(r)
1435 if err != nil {
1436 log.Println("failed to get repo and knot", err)
1437 return
1438 }
1439
1440 if user.Did != pull.OwnerDid {
1441 log.Println("unauthorized user")
1442 w.WriteHeader(http.StatusUnauthorized)
1443 return
1444 }
1445
1446 forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
1447 if err != nil {
1448 log.Println("failed to get source repo", err)
1449 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1450 return
1451 }
1452
1453 // extract patch by performing compare
1454 ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
1455 if err != nil {
1456 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
1457 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1458 return
1459 }
1460
1461 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
1462 if err != nil {
1463 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
1464 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1465 return
1466 }
1467
1468 // update the hidden tracking branch to latest
1469 signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
1470 if err != nil {
1471 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
1472 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1473 return
1474 }
1475
1476 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
1477 if err != nil || resp.StatusCode != http.StatusNoContent {
1478 log.Printf("failed to update tracking branch: %s", err)
1479 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1480 return
1481 }
1482
1483 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)
1484 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
1485 if err != nil {
1486 log.Printf("failed to compare branches: %s", err)
1487 s.pages.Notice(w, "resubmit-error", err.Error())
1488 return
1489 }
1490
1491 sourceRev := comparison.Rev2
1492 patch := comparison.Patch
1493
1494 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1495}
1496
1497// validate a resubmission against a pull request
1498func validateResubmittedPatch(pull *db.Pull, patch string) error {
1499 if patch == "" {
1500 return fmt.Errorf("Patch is empty.")
1501 }
1502
1503 if patch == pull.LatestPatch() {
1504 return fmt.Errorf("Patch is identical to previous submission.")
1505 }
1506
1507 if !patchutil.IsPatchValid(patch) {
1508 return fmt.Errorf("Invalid patch format. Please provide a valid diff.")
1509 }
1510
1511 return nil
1512}
1513
1514func (s *Pulls) resubmitPullHelper(
1515 w http.ResponseWriter,
1516 r *http.Request,
1517 f *reporesolver.ResolvedRepo,
1518 user *oauth.User,
1519 pull *db.Pull,
1520 patch string,
1521 sourceRev string,
1522) {
1523 if pull.IsStacked() {
1524 log.Println("resubmitting stacked PR")
1525 s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1526 return
1527 }
1528
1529 if err := validateResubmittedPatch(pull, patch); err != nil {
1530 s.pages.Notice(w, "resubmit-error", err.Error())
1531 return
1532 }
1533
1534 // validate sourceRev if branch/fork based
1535 if pull.IsBranchBased() || pull.IsForkBased() {
1536 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
1537 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
1538 return
1539 }
1540 }
1541
1542 tx, err := s.db.BeginTx(r.Context(), nil)
1543 if err != nil {
1544 log.Println("failed to start tx")
1545 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1546 return
1547 }
1548 defer tx.Rollback()
1549
1550 err = db.ResubmitPull(tx, pull, patch, sourceRev)
1551 if err != nil {
1552 log.Println("failed to create pull request", err)
1553 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1554 return
1555 }
1556 client, err := s.oauth.AuthorizedClient(r)
1557 if err != nil {
1558 log.Println("failed to authorize client")
1559 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1560 return
1561 }
1562
1563 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1564 if err != nil {
1565 // failed to get record
1566 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1567 return
1568 }
1569
1570 var recordPullSource *tangled.RepoPull_Source
1571 if pull.IsBranchBased() {
1572 recordPullSource = &tangled.RepoPull_Source{
1573 Branch: pull.PullSource.Branch,
1574 Sha: sourceRev,
1575 }
1576 }
1577 if pull.IsForkBased() {
1578 repoAt := pull.PullSource.RepoAt.String()
1579 recordPullSource = &tangled.RepoPull_Source{
1580 Branch: pull.PullSource.Branch,
1581 Repo: &repoAt,
1582 Sha: sourceRev,
1583 }
1584 }
1585
1586 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1587 Collection: tangled.RepoPullNSID,
1588 Repo: user.Did,
1589 Rkey: pull.Rkey,
1590 SwapRecord: ex.Cid,
1591 Record: &lexutil.LexiconTypeDecoder{
1592 Val: &tangled.RepoPull{
1593 Title: pull.Title,
1594 PullId: int64(pull.PullId),
1595 TargetRepo: string(f.RepoAt()),
1596 TargetBranch: pull.TargetBranch,
1597 Patch: patch, // new patch
1598 Source: recordPullSource,
1599 },
1600 },
1601 })
1602 if err != nil {
1603 log.Println("failed to update record", err)
1604 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
1605 return
1606 }
1607
1608 if err = tx.Commit(); err != nil {
1609 log.Println("failed to commit transaction", err)
1610 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
1611 return
1612 }
1613
1614 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1615}
1616
1617func (s *Pulls) resubmitStackedPullHelper(
1618 w http.ResponseWriter,
1619 r *http.Request,
1620 f *reporesolver.ResolvedRepo,
1621 user *oauth.User,
1622 pull *db.Pull,
1623 patch string,
1624 stackId string,
1625) {
1626 targetBranch := pull.TargetBranch
1627
1628 origStack, _ := r.Context().Value("stack").(db.Stack)
1629 newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
1630 if err != nil {
1631 log.Println("failed to create resubmitted stack", err)
1632 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1633 return
1634 }
1635
1636 // find the diff between the stacks, first, map them by changeId
1637 origById := make(map[string]*db.Pull)
1638 newById := make(map[string]*db.Pull)
1639 for _, p := range origStack {
1640 origById[p.ChangeId] = p
1641 }
1642 for _, p := range newStack {
1643 newById[p.ChangeId] = p
1644 }
1645
1646 // commits that got deleted: corresponding pull is closed
1647 // commits that got added: new pull is created
1648 // commits that got updated: corresponding pull is resubmitted & new round begins
1649 //
1650 // for commits that were unchanged: no changes, parent-change-id is updated as necessary
1651 additions := make(map[string]*db.Pull)
1652 deletions := make(map[string]*db.Pull)
1653 unchanged := make(map[string]struct{})
1654 updated := make(map[string]struct{})
1655
1656 // pulls in orignal stack but not in new one
1657 for _, op := range origStack {
1658 if _, ok := newById[op.ChangeId]; !ok {
1659 deletions[op.ChangeId] = op
1660 }
1661 }
1662
1663 // pulls in new stack but not in original one
1664 for _, np := range newStack {
1665 if _, ok := origById[np.ChangeId]; !ok {
1666 additions[np.ChangeId] = np
1667 }
1668 }
1669
1670 // NOTE: this loop can be written in any of above blocks,
1671 // but is written separately in the interest of simpler code
1672 for _, np := range newStack {
1673 if op, ok := origById[np.ChangeId]; ok {
1674 // pull exists in both stacks
1675 // TODO: can we avoid reparse?
1676 origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch()))
1677 newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch()))
1678
1679 origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr)
1680 newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr)
1681
1682 patchutil.SortPatch(newFiles)
1683 patchutil.SortPatch(origFiles)
1684
1685 // text content of patch may be identical, but a jj rebase might have forwarded it
1686 //
1687 // we still need to update the hash in submission.Patch and submission.SourceRev
1688 if patchutil.Equal(newFiles, origFiles) &&
1689 origHeader.Title == newHeader.Title &&
1690 origHeader.Body == newHeader.Body {
1691 unchanged[op.ChangeId] = struct{}{}
1692 } else {
1693 updated[op.ChangeId] = struct{}{}
1694 }
1695 }
1696 }
1697
1698 tx, err := s.db.Begin()
1699 if err != nil {
1700 log.Println("failed to start transaction", err)
1701 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1702 return
1703 }
1704 defer tx.Rollback()
1705
1706 // pds updates to make
1707 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1708
1709 // deleted pulls are marked as deleted in the DB
1710 for _, p := range deletions {
1711 // do not do delete already merged PRs
1712 if p.State == db.PullMerged {
1713 continue
1714 }
1715
1716 err := db.DeletePull(tx, p.RepoAt, p.PullId)
1717 if err != nil {
1718 log.Println("failed to delete pull", err, p.PullId)
1719 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1720 return
1721 }
1722 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1723 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
1724 Collection: tangled.RepoPullNSID,
1725 Rkey: p.Rkey,
1726 },
1727 })
1728 }
1729
1730 // new pulls are created
1731 for _, p := range additions {
1732 err := db.NewPull(tx, p)
1733 if err != nil {
1734 log.Println("failed to create pull", err, p.PullId)
1735 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1736 return
1737 }
1738
1739 record := p.AsRecord()
1740 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1741 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1742 Collection: tangled.RepoPullNSID,
1743 Rkey: &p.Rkey,
1744 Value: &lexutil.LexiconTypeDecoder{
1745 Val: &record,
1746 },
1747 },
1748 })
1749 }
1750
1751 // updated pulls are, well, updated; to start a new round
1752 for id := range updated {
1753 op, _ := origById[id]
1754 np, _ := newById[id]
1755
1756 // do not update already merged PRs
1757 if op.State == db.PullMerged {
1758 continue
1759 }
1760
1761 submission := np.Submissions[np.LastRoundNumber()]
1762
1763 // resubmit the old pull
1764 err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev)
1765
1766 if err != nil {
1767 log.Println("failed to update pull", err, op.PullId)
1768 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1769 return
1770 }
1771
1772 record := op.AsRecord()
1773 record.Patch = submission.Patch
1774
1775 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1776 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1777 Collection: tangled.RepoPullNSID,
1778 Rkey: op.Rkey,
1779 Value: &lexutil.LexiconTypeDecoder{
1780 Val: &record,
1781 },
1782 },
1783 })
1784 }
1785
1786 // unchanged pulls are edited without starting a new round
1787 //
1788 // update source-revs & patches without advancing rounds
1789 for changeId := range unchanged {
1790 op, _ := origById[changeId]
1791 np, _ := newById[changeId]
1792
1793 origSubmission := op.Submissions[op.LastRoundNumber()]
1794 newSubmission := np.Submissions[np.LastRoundNumber()]
1795
1796 log.Println("moving unchanged change id : ", changeId)
1797
1798 err := db.UpdatePull(
1799 tx,
1800 newSubmission.Patch,
1801 newSubmission.SourceRev,
1802 db.FilterEq("id", origSubmission.ID),
1803 )
1804
1805 if err != nil {
1806 log.Println("failed to update pull", err, op.PullId)
1807 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1808 return
1809 }
1810
1811 record := op.AsRecord()
1812 record.Patch = newSubmission.Patch
1813
1814 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1815 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1816 Collection: tangled.RepoPullNSID,
1817 Rkey: op.Rkey,
1818 Value: &lexutil.LexiconTypeDecoder{
1819 Val: &record,
1820 },
1821 },
1822 })
1823 }
1824
1825 // update parent-change-id relations for the entire stack
1826 for _, p := range newStack {
1827 err := db.SetPullParentChangeId(
1828 tx,
1829 p.ParentChangeId,
1830 // these should be enough filters to be unique per-stack
1831 db.FilterEq("repo_at", p.RepoAt.String()),
1832 db.FilterEq("owner_did", p.OwnerDid),
1833 db.FilterEq("change_id", p.ChangeId),
1834 )
1835
1836 if err != nil {
1837 log.Println("failed to update pull", err, p.PullId)
1838 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1839 return
1840 }
1841 }
1842
1843 err = tx.Commit()
1844 if err != nil {
1845 log.Println("failed to resubmit pull", err)
1846 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1847 return
1848 }
1849
1850 client, err := s.oauth.AuthorizedClient(r)
1851 if err != nil {
1852 log.Println("failed to authorize client")
1853 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1854 return
1855 }
1856
1857 _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1858 Repo: user.Did,
1859 Writes: writes,
1860 })
1861 if err != nil {
1862 log.Println("failed to create stacked pull request", err)
1863 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.")
1864 return
1865 }
1866
1867 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1868}
1869
1870func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
1871 f, err := s.repoResolver.Resolve(r)
1872 if err != nil {
1873 log.Println("failed to resolve repo:", err)
1874 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1875 return
1876 }
1877
1878 pull, ok := r.Context().Value("pull").(*db.Pull)
1879 if !ok {
1880 log.Println("failed to get pull")
1881 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1882 return
1883 }
1884
1885 var pullsToMerge db.Stack
1886 pullsToMerge = append(pullsToMerge, pull)
1887 if pull.IsStacked() {
1888 stack, ok := r.Context().Value("stack").(db.Stack)
1889 if !ok {
1890 log.Println("failed to get stack")
1891 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1892 return
1893 }
1894
1895 // combine patches of substack
1896 subStack := stack.StrictlyBelow(pull)
1897 // collect the portion of the stack that is mergeable
1898 mergeable := subStack.Mergeable()
1899 // add to total patch
1900 pullsToMerge = append(pullsToMerge, mergeable...)
1901 }
1902
1903 patch := pullsToMerge.CombinedPatch()
1904
1905 secret, err := db.GetRegistrationKey(s.db, f.Knot)
1906 if err != nil {
1907 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
1908 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1909 return
1910 }
1911
1912 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid)
1913 if err != nil {
1914 log.Printf("resolving identity: %s", err)
1915 w.WriteHeader(http.StatusNotFound)
1916 return
1917 }
1918
1919 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
1920 if err != nil {
1921 log.Printf("failed to get primary email: %s", err)
1922 }
1923
1924 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
1925 if err != nil {
1926 log.Printf("failed to create signed client for %s: %s", f.Knot, err)
1927 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1928 return
1929 }
1930
1931 // Merge the pull request
1932 resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address)
1933 if err != nil {
1934 log.Printf("failed to merge pull request: %s", err)
1935 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1936 return
1937 }
1938
1939 if resp.StatusCode != http.StatusOK {
1940 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
1941 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1942 return
1943 }
1944
1945 tx, err := s.db.Begin()
1946 if err != nil {
1947 log.Println("failed to start transcation", err)
1948 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1949 return
1950 }
1951 defer tx.Rollback()
1952
1953 for _, p := range pullsToMerge {
1954 err := db.MergePull(tx, f.RepoAt(), p.PullId)
1955 if err != nil {
1956 log.Printf("failed to update pull request status in database: %s", err)
1957 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1958 return
1959 }
1960 }
1961
1962 err = tx.Commit()
1963 if err != nil {
1964 // TODO: this is unsound, we should also revert the merge from the knotserver here
1965 log.Printf("failed to update pull request status in database: %s", err)
1966 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1967 return
1968 }
1969
1970 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId))
1971}
1972
1973func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
1974 user := s.oauth.GetUser(r)
1975
1976 f, err := s.repoResolver.Resolve(r)
1977 if err != nil {
1978 log.Println("malformed middleware")
1979 return
1980 }
1981
1982 pull, ok := r.Context().Value("pull").(*db.Pull)
1983 if !ok {
1984 log.Println("failed to get pull")
1985 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1986 return
1987 }
1988
1989 // auth filter: only owner or collaborators can close
1990 roles := f.RolesInRepo(user)
1991 isOwner := roles.IsOwner()
1992 isCollaborator := roles.IsCollaborator()
1993 isPullAuthor := user.Did == pull.OwnerDid
1994 isCloseAllowed := isOwner || isCollaborator || isPullAuthor
1995 if !isCloseAllowed {
1996 log.Println("failed to close pull")
1997 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1998 return
1999 }
2000
2001 // Start a transaction
2002 tx, err := s.db.BeginTx(r.Context(), nil)
2003 if err != nil {
2004 log.Println("failed to start transaction", err)
2005 s.pages.Notice(w, "pull-close", "Failed to close pull.")
2006 return
2007 }
2008 defer tx.Rollback()
2009
2010 var pullsToClose []*db.Pull
2011 pullsToClose = append(pullsToClose, pull)
2012
2013 // if this PR is stacked, then we want to close all PRs below this one on the stack
2014 if pull.IsStacked() {
2015 stack := r.Context().Value("stack").(db.Stack)
2016 subStack := stack.StrictlyBelow(pull)
2017 pullsToClose = append(pullsToClose, subStack...)
2018 }
2019
2020 for _, p := range pullsToClose {
2021 // Close the pull in the database
2022 err = db.ClosePull(tx, f.RepoAt(), p.PullId)
2023 if err != nil {
2024 log.Println("failed to close pull", err)
2025 s.pages.Notice(w, "pull-close", "Failed to close pull.")
2026 return
2027 }
2028 }
2029
2030 // Commit the transaction
2031 if err = tx.Commit(); err != nil {
2032 log.Println("failed to commit transaction", err)
2033 s.pages.Notice(w, "pull-close", "Failed to close pull.")
2034 return
2035 }
2036
2037 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2038}
2039
2040func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
2041 user := s.oauth.GetUser(r)
2042
2043 f, err := s.repoResolver.Resolve(r)
2044 if err != nil {
2045 log.Println("failed to resolve repo", err)
2046 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2047 return
2048 }
2049
2050 pull, ok := r.Context().Value("pull").(*db.Pull)
2051 if !ok {
2052 log.Println("failed to get pull")
2053 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
2054 return
2055 }
2056
2057 // auth filter: only owner or collaborators can close
2058 roles := f.RolesInRepo(user)
2059 isOwner := roles.IsOwner()
2060 isCollaborator := roles.IsCollaborator()
2061 isPullAuthor := user.Did == pull.OwnerDid
2062 isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2063 if !isCloseAllowed {
2064 log.Println("failed to close pull")
2065 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
2066 return
2067 }
2068
2069 // Start a transaction
2070 tx, err := s.db.BeginTx(r.Context(), nil)
2071 if err != nil {
2072 log.Println("failed to start transaction", err)
2073 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2074 return
2075 }
2076 defer tx.Rollback()
2077
2078 var pullsToReopen []*db.Pull
2079 pullsToReopen = append(pullsToReopen, pull)
2080
2081 // if this PR is stacked, then we want to reopen all PRs above this one on the stack
2082 if pull.IsStacked() {
2083 stack := r.Context().Value("stack").(db.Stack)
2084 subStack := stack.StrictlyAbove(pull)
2085 pullsToReopen = append(pullsToReopen, subStack...)
2086 }
2087
2088 for _, p := range pullsToReopen {
2089 // Close the pull in the database
2090 err = db.ReopenPull(tx, f.RepoAt(), p.PullId)
2091 if err != nil {
2092 log.Println("failed to close pull", err)
2093 s.pages.Notice(w, "pull-close", "Failed to close pull.")
2094 return
2095 }
2096 }
2097
2098 // Commit the transaction
2099 if err = tx.Commit(); err != nil {
2100 log.Println("failed to commit transaction", err)
2101 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2102 return
2103 }
2104
2105 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2106}
2107
2108func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) {
2109 formatPatches, err := patchutil.ExtractPatches(patch)
2110 if err != nil {
2111 return nil, fmt.Errorf("Failed to extract patches: %v", err)
2112 }
2113
2114 // must have atleast 1 patch to begin with
2115 if len(formatPatches) == 0 {
2116 return nil, fmt.Errorf("No patches found in the generated format-patch.")
2117 }
2118
2119 // the stack is identified by a UUID
2120 var stack db.Stack
2121 parentChangeId := ""
2122 for _, fp := range formatPatches {
2123 // all patches must have a jj change-id
2124 changeId, err := fp.ChangeId()
2125 if err != nil {
2126 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.")
2127 }
2128
2129 title := fp.Title
2130 body := fp.Body
2131 rkey := tid.TID()
2132
2133 initialSubmission := db.PullSubmission{
2134 Patch: fp.Raw,
2135 SourceRev: fp.SHA,
2136 }
2137 pull := db.Pull{
2138 Title: title,
2139 Body: body,
2140 TargetBranch: targetBranch,
2141 OwnerDid: user.Did,
2142 RepoAt: f.RepoAt(),
2143 Rkey: rkey,
2144 Submissions: []*db.PullSubmission{
2145 &initialSubmission,
2146 },
2147 PullSource: pullSource,
2148 Created: time.Now(),
2149
2150 StackId: stackId,
2151 ChangeId: changeId,
2152 ParentChangeId: parentChangeId,
2153 }
2154
2155 stack = append(stack, &pull)
2156
2157 parentChangeId = changeId
2158 }
2159
2160 return stack, nil
2161}