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