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