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