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