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