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