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