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