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