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