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