forked from tangled.org/core
this repo has no description
at master 64 kB view raw
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(s.db, forkOwnerDid, forkName) 1368 if err != nil { 1369 log.Println("failed to get repo", user.Did, forkVal) 1370 return 1371 } 1372 1373 sourceScheme := "http" 1374 if !s.config.Core.Dev { 1375 sourceScheme = "https" 1376 } 1377 sourceHost := fmt.Sprintf("%s://%s", sourceScheme, repo.Knot) 1378 sourceXrpcc := &indigoxrpc.Client{ 1379 Host: sourceHost, 1380 } 1381 1382 sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name) 1383 sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo) 1384 if err != nil { 1385 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1386 log.Println("failed to call XRPC repo.branches for source", xrpcerr) 1387 s.pages.Error503(w) 1388 return 1389 } 1390 log.Println("failed to fetch source branches", err) 1391 return 1392 } 1393 1394 // Decode source branches 1395 var sourceBranches types.RepoBranchesResponse 1396 if err := json.Unmarshal(sourceXrpcBytes, &sourceBranches); err != nil { 1397 log.Println("failed to decode source branches XRPC response", err) 1398 s.pages.Error503(w) 1399 return 1400 } 1401 1402 targetScheme := "http" 1403 if !s.config.Core.Dev { 1404 targetScheme = "https" 1405 } 1406 targetHost := fmt.Sprintf("%s://%s", targetScheme, f.Knot) 1407 targetXrpcc := &indigoxrpc.Client{ 1408 Host: targetHost, 1409 } 1410 1411 targetRepo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1412 targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo) 1413 if err != nil { 1414 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1415 log.Println("failed to call XRPC repo.branches for target", xrpcerr) 1416 s.pages.Error503(w) 1417 return 1418 } 1419 log.Println("failed to fetch target branches", err) 1420 return 1421 } 1422 1423 // Decode target branches 1424 var targetBranches types.RepoBranchesResponse 1425 if err := json.Unmarshal(targetXrpcBytes, &targetBranches); err != nil { 1426 log.Println("failed to decode target branches XRPC response", err) 1427 s.pages.Error503(w) 1428 return 1429 } 1430 1431 sort.Slice(sourceBranches.Branches, func(i int, j int) bool { 1432 return sourceBranches.Branches[i].Commit.Committer.When.After(sourceBranches.Branches[j].Commit.Committer.When) 1433 }) 1434 1435 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1436 RepoInfo: f.RepoInfo(user), 1437 SourceBranches: sourceBranches.Branches, 1438 TargetBranches: targetBranches.Branches, 1439 }) 1440} 1441 1442func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1443 user := s.oauth.GetUser(r) 1444 f, err := s.repoResolver.Resolve(r) 1445 if err != nil { 1446 log.Println("failed to get repo and knot", err) 1447 return 1448 } 1449 1450 pull, ok := r.Context().Value("pull").(*db.Pull) 1451 if !ok { 1452 log.Println("failed to get pull") 1453 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1454 return 1455 } 1456 1457 switch r.Method { 1458 case http.MethodGet: 1459 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1460 RepoInfo: f.RepoInfo(user), 1461 Pull: pull, 1462 }) 1463 return 1464 case http.MethodPost: 1465 if pull.IsPatchBased() { 1466 s.resubmitPatch(w, r) 1467 return 1468 } else if pull.IsBranchBased() { 1469 s.resubmitBranch(w, r) 1470 return 1471 } else if pull.IsForkBased() { 1472 s.resubmitFork(w, r) 1473 return 1474 } 1475 } 1476} 1477 1478func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1479 user := s.oauth.GetUser(r) 1480 1481 pull, ok := r.Context().Value("pull").(*db.Pull) 1482 if !ok { 1483 log.Println("failed to get pull") 1484 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1485 return 1486 } 1487 1488 f, err := s.repoResolver.Resolve(r) 1489 if err != nil { 1490 log.Println("failed to get repo and knot", err) 1491 return 1492 } 1493 1494 if user.Did != pull.OwnerDid { 1495 log.Println("unauthorized user") 1496 w.WriteHeader(http.StatusUnauthorized) 1497 return 1498 } 1499 1500 patch := r.FormValue("patch") 1501 1502 s.resubmitPullHelper(w, r, f, user, pull, patch, "") 1503} 1504 1505func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1506 user := s.oauth.GetUser(r) 1507 1508 pull, ok := r.Context().Value("pull").(*db.Pull) 1509 if !ok { 1510 log.Println("failed to get pull") 1511 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1512 return 1513 } 1514 1515 f, err := s.repoResolver.Resolve(r) 1516 if err != nil { 1517 log.Println("failed to get repo and knot", err) 1518 return 1519 } 1520 1521 if user.Did != pull.OwnerDid { 1522 log.Println("unauthorized user") 1523 w.WriteHeader(http.StatusUnauthorized) 1524 return 1525 } 1526 1527 if !f.RepoInfo(user).Roles.IsPushAllowed() { 1528 log.Println("unauthorized user") 1529 w.WriteHeader(http.StatusUnauthorized) 1530 return 1531 } 1532 1533 scheme := "http" 1534 if !s.config.Core.Dev { 1535 scheme = "https" 1536 } 1537 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1538 xrpcc := &indigoxrpc.Client{ 1539 Host: host, 1540 } 1541 1542 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1543 xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch) 1544 if err != nil { 1545 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1546 log.Println("failed to call XRPC repo.compare", xrpcerr) 1547 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1548 return 1549 } 1550 log.Printf("compare request failed: %s", err) 1551 s.pages.Notice(w, "resubmit-error", err.Error()) 1552 return 1553 } 1554 1555 var comparison types.RepoFormatPatchResponse 1556 if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 1557 log.Println("failed to decode XRPC compare response", err) 1558 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1559 return 1560 } 1561 1562 sourceRev := comparison.Rev2 1563 patch := comparison.Patch 1564 1565 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1566} 1567 1568func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1569 user := s.oauth.GetUser(r) 1570 1571 pull, ok := r.Context().Value("pull").(*db.Pull) 1572 if !ok { 1573 log.Println("failed to get pull") 1574 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1575 return 1576 } 1577 1578 f, err := s.repoResolver.Resolve(r) 1579 if err != nil { 1580 log.Println("failed to get repo and knot", err) 1581 return 1582 } 1583 1584 if user.Did != pull.OwnerDid { 1585 log.Println("unauthorized user") 1586 w.WriteHeader(http.StatusUnauthorized) 1587 return 1588 } 1589 1590 forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1591 if err != nil { 1592 log.Println("failed to get source repo", err) 1593 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1594 return 1595 } 1596 1597 // extract patch by performing compare 1598 forkScheme := "http" 1599 if !s.config.Core.Dev { 1600 forkScheme = "https" 1601 } 1602 forkHost := fmt.Sprintf("%s://%s", forkScheme, forkRepo.Knot) 1603 forkRepoId := fmt.Sprintf("%s/%s", forkRepo.Did, forkRepo.Name) 1604 forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepoId, pull.TargetBranch, pull.PullSource.Branch) 1605 if err != nil { 1606 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1607 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) 1608 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1609 return 1610 } 1611 log.Printf("failed to compare branches: %s", err) 1612 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1613 return 1614 } 1615 1616 var forkComparison types.RepoFormatPatchResponse 1617 if err := json.Unmarshal(forkXrpcBytes, &forkComparison); err != nil { 1618 log.Println("failed to decode XRPC compare response for fork", err) 1619 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1620 return 1621 } 1622 1623 // update the hidden tracking branch to latest 1624 client, err := s.oauth.ServiceClient( 1625 r, 1626 oauth.WithService(forkRepo.Knot), 1627 oauth.WithLxm(tangled.RepoHiddenRefNSID), 1628 oauth.WithDev(s.config.Core.Dev), 1629 ) 1630 if err != nil { 1631 log.Printf("failed to connect to knot server: %v", err) 1632 return 1633 } 1634 1635 resp, err := tangled.RepoHiddenRef( 1636 r.Context(), 1637 client, 1638 &tangled.RepoHiddenRef_Input{ 1639 ForkRef: pull.PullSource.Branch, 1640 RemoteRef: pull.TargetBranch, 1641 Repo: forkRepo.RepoAt().String(), 1642 }, 1643 ) 1644 if err := xrpcclient.HandleXrpcErr(err); err != nil { 1645 s.pages.Notice(w, "resubmit-error", err.Error()) 1646 return 1647 } 1648 if !resp.Success { 1649 log.Println("Failed to update tracking ref.", "err", resp.Error) 1650 s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 1651 return 1652 } 1653 1654 // Use the fork comparison we already made 1655 comparison := forkComparison 1656 1657 sourceRev := comparison.Rev2 1658 patch := comparison.Patch 1659 1660 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1661} 1662 1663// validate a resubmission against a pull request 1664func validateResubmittedPatch(pull *db.Pull, patch string) error { 1665 if patch == "" { 1666 return fmt.Errorf("Patch is empty.") 1667 } 1668 1669 if patch == pull.LatestPatch() { 1670 return fmt.Errorf("Patch is identical to previous submission.") 1671 } 1672 1673 if !patchutil.IsPatchValid(patch) { 1674 return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1675 } 1676 1677 return nil 1678} 1679 1680func (s *Pulls) resubmitPullHelper( 1681 w http.ResponseWriter, 1682 r *http.Request, 1683 f *reporesolver.ResolvedRepo, 1684 user *oauth.User, 1685 pull *db.Pull, 1686 patch string, 1687 sourceRev string, 1688) { 1689 if pull.IsStacked() { 1690 log.Println("resubmitting stacked PR") 1691 s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId) 1692 return 1693 } 1694 1695 if err := validateResubmittedPatch(pull, patch); err != nil { 1696 s.pages.Notice(w, "resubmit-error", err.Error()) 1697 return 1698 } 1699 1700 // validate sourceRev if branch/fork based 1701 if pull.IsBranchBased() || pull.IsForkBased() { 1702 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1703 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1704 return 1705 } 1706 } 1707 1708 tx, err := s.db.BeginTx(r.Context(), nil) 1709 if err != nil { 1710 log.Println("failed to start tx") 1711 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1712 return 1713 } 1714 defer tx.Rollback() 1715 1716 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1717 if err != nil { 1718 log.Println("failed to create pull request", err) 1719 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1720 return 1721 } 1722 client, err := s.oauth.AuthorizedClient(r) 1723 if err != nil { 1724 log.Println("failed to authorize client") 1725 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1726 return 1727 } 1728 1729 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1730 if err != nil { 1731 // failed to get record 1732 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1733 return 1734 } 1735 1736 var recordPullSource *tangled.RepoPull_Source 1737 if pull.IsBranchBased() { 1738 recordPullSource = &tangled.RepoPull_Source{ 1739 Branch: pull.PullSource.Branch, 1740 Sha: sourceRev, 1741 } 1742 } 1743 if pull.IsForkBased() { 1744 repoAt := pull.PullSource.RepoAt.String() 1745 recordPullSource = &tangled.RepoPull_Source{ 1746 Branch: pull.PullSource.Branch, 1747 Repo: &repoAt, 1748 Sha: sourceRev, 1749 } 1750 } 1751 1752 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1753 Collection: tangled.RepoPullNSID, 1754 Repo: user.Did, 1755 Rkey: pull.Rkey, 1756 SwapRecord: ex.Cid, 1757 Record: &lexutil.LexiconTypeDecoder{ 1758 Val: &tangled.RepoPull{ 1759 Title: pull.Title, 1760 Target: &tangled.RepoPull_Target{ 1761 Repo: string(f.RepoAt()), 1762 Branch: pull.TargetBranch, 1763 }, 1764 Patch: patch, // new patch 1765 Source: recordPullSource, 1766 }, 1767 }, 1768 }) 1769 if err != nil { 1770 log.Println("failed to update record", err) 1771 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1772 return 1773 } 1774 1775 if err = tx.Commit(); err != nil { 1776 log.Println("failed to commit transaction", err) 1777 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1778 return 1779 } 1780 1781 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1782} 1783 1784func (s *Pulls) resubmitStackedPullHelper( 1785 w http.ResponseWriter, 1786 r *http.Request, 1787 f *reporesolver.ResolvedRepo, 1788 user *oauth.User, 1789 pull *db.Pull, 1790 patch string, 1791 stackId string, 1792) { 1793 targetBranch := pull.TargetBranch 1794 1795 origStack, _ := r.Context().Value("stack").(db.Stack) 1796 newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1797 if err != nil { 1798 log.Println("failed to create resubmitted stack", err) 1799 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1800 return 1801 } 1802 1803 // find the diff between the stacks, first, map them by changeId 1804 origById := make(map[string]*db.Pull) 1805 newById := make(map[string]*db.Pull) 1806 for _, p := range origStack { 1807 origById[p.ChangeId] = p 1808 } 1809 for _, p := range newStack { 1810 newById[p.ChangeId] = p 1811 } 1812 1813 // commits that got deleted: corresponding pull is closed 1814 // commits that got added: new pull is created 1815 // commits that got updated: corresponding pull is resubmitted & new round begins 1816 // 1817 // for commits that were unchanged: no changes, parent-change-id is updated as necessary 1818 additions := make(map[string]*db.Pull) 1819 deletions := make(map[string]*db.Pull) 1820 unchanged := make(map[string]struct{}) 1821 updated := make(map[string]struct{}) 1822 1823 // pulls in orignal stack but not in new one 1824 for _, op := range origStack { 1825 if _, ok := newById[op.ChangeId]; !ok { 1826 deletions[op.ChangeId] = op 1827 } 1828 } 1829 1830 // pulls in new stack but not in original one 1831 for _, np := range newStack { 1832 if _, ok := origById[np.ChangeId]; !ok { 1833 additions[np.ChangeId] = np 1834 } 1835 } 1836 1837 // NOTE: this loop can be written in any of above blocks, 1838 // but is written separately in the interest of simpler code 1839 for _, np := range newStack { 1840 if op, ok := origById[np.ChangeId]; ok { 1841 // pull exists in both stacks 1842 // TODO: can we avoid reparse? 1843 origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch())) 1844 newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch())) 1845 1846 origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr) 1847 newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr) 1848 1849 patchutil.SortPatch(newFiles) 1850 patchutil.SortPatch(origFiles) 1851 1852 // text content of patch may be identical, but a jj rebase might have forwarded it 1853 // 1854 // we still need to update the hash in submission.Patch and submission.SourceRev 1855 if patchutil.Equal(newFiles, origFiles) && 1856 origHeader.Title == newHeader.Title && 1857 origHeader.Body == newHeader.Body { 1858 unchanged[op.ChangeId] = struct{}{} 1859 } else { 1860 updated[op.ChangeId] = struct{}{} 1861 } 1862 } 1863 } 1864 1865 tx, err := s.db.Begin() 1866 if err != nil { 1867 log.Println("failed to start transaction", err) 1868 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1869 return 1870 } 1871 defer tx.Rollback() 1872 1873 // pds updates to make 1874 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1875 1876 // deleted pulls are marked as deleted in the DB 1877 for _, p := range deletions { 1878 // do not do delete already merged PRs 1879 if p.State == db.PullMerged { 1880 continue 1881 } 1882 1883 err := db.DeletePull(tx, p.RepoAt, p.PullId) 1884 if err != nil { 1885 log.Println("failed to delete pull", err, p.PullId) 1886 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1887 return 1888 } 1889 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1890 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 1891 Collection: tangled.RepoPullNSID, 1892 Rkey: p.Rkey, 1893 }, 1894 }) 1895 } 1896 1897 // new pulls are created 1898 for _, p := range additions { 1899 err := db.NewPull(tx, p) 1900 if err != nil { 1901 log.Println("failed to create pull", err, p.PullId) 1902 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1903 return 1904 } 1905 1906 record := p.AsRecord() 1907 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1908 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1909 Collection: tangled.RepoPullNSID, 1910 Rkey: &p.Rkey, 1911 Value: &lexutil.LexiconTypeDecoder{ 1912 Val: &record, 1913 }, 1914 }, 1915 }) 1916 } 1917 1918 // updated pulls are, well, updated; to start a new round 1919 for id := range updated { 1920 op, _ := origById[id] 1921 np, _ := newById[id] 1922 1923 // do not update already merged PRs 1924 if op.State == db.PullMerged { 1925 continue 1926 } 1927 1928 submission := np.Submissions[np.LastRoundNumber()] 1929 1930 // resubmit the old pull 1931 err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev) 1932 1933 if err != nil { 1934 log.Println("failed to update pull", err, op.PullId) 1935 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1936 return 1937 } 1938 1939 record := op.AsRecord() 1940 record.Patch = submission.Patch 1941 1942 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1943 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1944 Collection: tangled.RepoPullNSID, 1945 Rkey: op.Rkey, 1946 Value: &lexutil.LexiconTypeDecoder{ 1947 Val: &record, 1948 }, 1949 }, 1950 }) 1951 } 1952 1953 // unchanged pulls are edited without starting a new round 1954 // 1955 // update source-revs & patches without advancing rounds 1956 for changeId := range unchanged { 1957 op, _ := origById[changeId] 1958 np, _ := newById[changeId] 1959 1960 origSubmission := op.Submissions[op.LastRoundNumber()] 1961 newSubmission := np.Submissions[np.LastRoundNumber()] 1962 1963 log.Println("moving unchanged change id : ", changeId) 1964 1965 err := db.UpdatePull( 1966 tx, 1967 newSubmission.Patch, 1968 newSubmission.SourceRev, 1969 db.FilterEq("id", origSubmission.ID), 1970 ) 1971 1972 if err != nil { 1973 log.Println("failed to update pull", err, op.PullId) 1974 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1975 return 1976 } 1977 1978 record := op.AsRecord() 1979 record.Patch = newSubmission.Patch 1980 1981 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1982 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1983 Collection: tangled.RepoPullNSID, 1984 Rkey: op.Rkey, 1985 Value: &lexutil.LexiconTypeDecoder{ 1986 Val: &record, 1987 }, 1988 }, 1989 }) 1990 } 1991 1992 // update parent-change-id relations for the entire stack 1993 for _, p := range newStack { 1994 err := db.SetPullParentChangeId( 1995 tx, 1996 p.ParentChangeId, 1997 // these should be enough filters to be unique per-stack 1998 db.FilterEq("repo_at", p.RepoAt.String()), 1999 db.FilterEq("owner_did", p.OwnerDid), 2000 db.FilterEq("change_id", p.ChangeId), 2001 ) 2002 2003 if err != nil { 2004 log.Println("failed to update pull", err, p.PullId) 2005 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2006 return 2007 } 2008 } 2009 2010 err = tx.Commit() 2011 if err != nil { 2012 log.Println("failed to resubmit pull", err) 2013 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2014 return 2015 } 2016 2017 client, err := s.oauth.AuthorizedClient(r) 2018 if err != nil { 2019 log.Println("failed to authorize client") 2020 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2021 return 2022 } 2023 2024 _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 2025 Repo: user.Did, 2026 Writes: writes, 2027 }) 2028 if err != nil { 2029 log.Println("failed to create stacked pull request", err) 2030 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 2031 return 2032 } 2033 2034 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2035} 2036 2037func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 2038 f, err := s.repoResolver.Resolve(r) 2039 if err != nil { 2040 log.Println("failed to resolve repo:", err) 2041 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2042 return 2043 } 2044 2045 pull, ok := r.Context().Value("pull").(*db.Pull) 2046 if !ok { 2047 log.Println("failed to get pull") 2048 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 2049 return 2050 } 2051 2052 var pullsToMerge db.Stack 2053 pullsToMerge = append(pullsToMerge, pull) 2054 if pull.IsStacked() { 2055 stack, ok := r.Context().Value("stack").(db.Stack) 2056 if !ok { 2057 log.Println("failed to get stack") 2058 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 2059 return 2060 } 2061 2062 // combine patches of substack 2063 subStack := stack.StrictlyBelow(pull) 2064 // collect the portion of the stack that is mergeable 2065 mergeable := subStack.Mergeable() 2066 // add to total patch 2067 pullsToMerge = append(pullsToMerge, mergeable...) 2068 } 2069 2070 patch := pullsToMerge.CombinedPatch() 2071 2072 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 2073 if err != nil { 2074 log.Printf("resolving identity: %s", err) 2075 w.WriteHeader(http.StatusNotFound) 2076 return 2077 } 2078 2079 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 2080 if err != nil { 2081 log.Printf("failed to get primary email: %s", err) 2082 } 2083 2084 authorName := ident.Handle.String() 2085 mergeInput := &tangled.RepoMerge_Input{ 2086 Did: f.OwnerDid(), 2087 Name: f.Name, 2088 Branch: pull.TargetBranch, 2089 Patch: patch, 2090 CommitMessage: &pull.Title, 2091 AuthorName: &authorName, 2092 } 2093 2094 if pull.Body != "" { 2095 mergeInput.CommitBody = &pull.Body 2096 } 2097 2098 if email.Address != "" { 2099 mergeInput.AuthorEmail = &email.Address 2100 } 2101 2102 client, err := s.oauth.ServiceClient( 2103 r, 2104 oauth.WithService(f.Knot), 2105 oauth.WithLxm(tangled.RepoMergeNSID), 2106 oauth.WithDev(s.config.Core.Dev), 2107 ) 2108 if err != nil { 2109 log.Printf("failed to connect to knot server: %v", err) 2110 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2111 return 2112 } 2113 2114 err = tangled.RepoMerge(r.Context(), client, mergeInput) 2115 if err := xrpcclient.HandleXrpcErr(err); err != nil { 2116 s.pages.Notice(w, "pull-merge-error", err.Error()) 2117 return 2118 } 2119 2120 tx, err := s.db.Begin() 2121 if err != nil { 2122 log.Println("failed to start transcation", err) 2123 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2124 return 2125 } 2126 defer tx.Rollback() 2127 2128 for _, p := range pullsToMerge { 2129 err := db.MergePull(tx, f.RepoAt(), p.PullId) 2130 if err != nil { 2131 log.Printf("failed to update pull request status in database: %s", err) 2132 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2133 return 2134 } 2135 } 2136 2137 err = tx.Commit() 2138 if err != nil { 2139 // TODO: this is unsound, we should also revert the merge from the knotserver here 2140 log.Printf("failed to update pull request status in database: %s", err) 2141 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2142 return 2143 } 2144 2145 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 2146} 2147 2148func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2149 user := s.oauth.GetUser(r) 2150 2151 f, err := s.repoResolver.Resolve(r) 2152 if err != nil { 2153 log.Println("malformed middleware") 2154 return 2155 } 2156 2157 pull, ok := r.Context().Value("pull").(*db.Pull) 2158 if !ok { 2159 log.Println("failed to get pull") 2160 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2161 return 2162 } 2163 2164 // auth filter: only owner or collaborators can close 2165 roles := f.RolesInRepo(user) 2166 isOwner := roles.IsOwner() 2167 isCollaborator := roles.IsCollaborator() 2168 isPullAuthor := user.Did == pull.OwnerDid 2169 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2170 if !isCloseAllowed { 2171 log.Println("failed to close pull") 2172 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2173 return 2174 } 2175 2176 // Start a transaction 2177 tx, err := s.db.BeginTx(r.Context(), nil) 2178 if err != nil { 2179 log.Println("failed to start transaction", err) 2180 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2181 return 2182 } 2183 defer tx.Rollback() 2184 2185 var pullsToClose []*db.Pull 2186 pullsToClose = append(pullsToClose, pull) 2187 2188 // if this PR is stacked, then we want to close all PRs below this one on the stack 2189 if pull.IsStacked() { 2190 stack := r.Context().Value("stack").(db.Stack) 2191 subStack := stack.StrictlyBelow(pull) 2192 pullsToClose = append(pullsToClose, subStack...) 2193 } 2194 2195 for _, p := range pullsToClose { 2196 // Close the pull in the database 2197 err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2198 if err != nil { 2199 log.Println("failed to close pull", err) 2200 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2201 return 2202 } 2203 } 2204 2205 // Commit the transaction 2206 if err = tx.Commit(); err != nil { 2207 log.Println("failed to commit transaction", err) 2208 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2209 return 2210 } 2211 2212 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2213} 2214 2215func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2216 user := s.oauth.GetUser(r) 2217 2218 f, err := s.repoResolver.Resolve(r) 2219 if err != nil { 2220 log.Println("failed to resolve repo", err) 2221 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2222 return 2223 } 2224 2225 pull, ok := r.Context().Value("pull").(*db.Pull) 2226 if !ok { 2227 log.Println("failed to get pull") 2228 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2229 return 2230 } 2231 2232 // auth filter: only owner or collaborators can close 2233 roles := f.RolesInRepo(user) 2234 isOwner := roles.IsOwner() 2235 isCollaborator := roles.IsCollaborator() 2236 isPullAuthor := user.Did == pull.OwnerDid 2237 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2238 if !isCloseAllowed { 2239 log.Println("failed to close pull") 2240 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2241 return 2242 } 2243 2244 // Start a transaction 2245 tx, err := s.db.BeginTx(r.Context(), nil) 2246 if err != nil { 2247 log.Println("failed to start transaction", err) 2248 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2249 return 2250 } 2251 defer tx.Rollback() 2252 2253 var pullsToReopen []*db.Pull 2254 pullsToReopen = append(pullsToReopen, pull) 2255 2256 // if this PR is stacked, then we want to reopen all PRs above this one on the stack 2257 if pull.IsStacked() { 2258 stack := r.Context().Value("stack").(db.Stack) 2259 subStack := stack.StrictlyAbove(pull) 2260 pullsToReopen = append(pullsToReopen, subStack...) 2261 } 2262 2263 for _, p := range pullsToReopen { 2264 // Close the pull in the database 2265 err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2266 if err != nil { 2267 log.Println("failed to close pull", err) 2268 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2269 return 2270 } 2271 } 2272 2273 // Commit the transaction 2274 if err = tx.Commit(); err != nil { 2275 log.Println("failed to commit transaction", err) 2276 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2277 return 2278 } 2279 2280 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2281} 2282 2283func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2284 formatPatches, err := patchutil.ExtractPatches(patch) 2285 if err != nil { 2286 return nil, fmt.Errorf("Failed to extract patches: %v", err) 2287 } 2288 2289 // must have atleast 1 patch to begin with 2290 if len(formatPatches) == 0 { 2291 return nil, fmt.Errorf("No patches found in the generated format-patch.") 2292 } 2293 2294 // the stack is identified by a UUID 2295 var stack db.Stack 2296 parentChangeId := "" 2297 for _, fp := range formatPatches { 2298 // all patches must have a jj change-id 2299 changeId, err := fp.ChangeId() 2300 if err != nil { 2301 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 2302 } 2303 2304 title := fp.Title 2305 body := fp.Body 2306 rkey := tid.TID() 2307 2308 initialSubmission := db.PullSubmission{ 2309 Patch: fp.Raw, 2310 SourceRev: fp.SHA, 2311 } 2312 pull := db.Pull{ 2313 Title: title, 2314 Body: body, 2315 TargetBranch: targetBranch, 2316 OwnerDid: user.Did, 2317 RepoAt: f.RepoAt(), 2318 Rkey: rkey, 2319 Submissions: []*db.PullSubmission{ 2320 &initialSubmission, 2321 }, 2322 PullSource: pullSource, 2323 Created: time.Now(), 2324 2325 StackId: stackId, 2326 ChangeId: changeId, 2327 ParentChangeId: parentChangeId, 2328 } 2329 2330 stack = append(stack, &pull) 2331 2332 parentChangeId = changeId 2333 } 2334 2335 return stack, nil 2336}