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