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