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 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") 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 pullSource := &db.PullSource{ 802 Branch: sourceBranch, 803 } 804 recordPullSource := &tangled.RepoPull_Source{ 805 Branch: sourceBranch, 806 } 807 808 // Generate a patch using /compare 809 ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 810 if err != nil { 811 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 812 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 813 return 814 } 815 816 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 817 if err != nil { 818 log.Println("failed to compare", err) 819 s.pages.Notice(w, "pull", err.Error()) 820 return 821 } 822 823 sourceRev := comparison.Rev2 824 patch := comparison.Patch 825 826 if !patchutil.IsPatchValid(patch) { 827 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 828 return 829 } 830 831 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked) 832} 833 834func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 835 if !patchutil.IsPatchValid(patch) { 836 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 837 return 838 } 839 840 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked) 841} 842 843func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 844 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 845 if errors.Is(err, sql.ErrNoRows) { 846 s.pages.Notice(w, "pull", "No such fork.") 847 return 848 } else if err != nil { 849 log.Println("failed to fetch fork:", err) 850 s.pages.Notice(w, "pull", "Failed to fetch fork.") 851 return 852 } 853 854 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 855 if err != nil { 856 log.Println("failed to fetch registration key:", err) 857 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 858 return 859 } 860 861 sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 862 if err != nil { 863 log.Println("failed to create signed client:", err) 864 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 865 return 866 } 867 868 us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 869 if err != nil { 870 log.Println("failed to create unsigned client:", err) 871 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 872 return 873 } 874 875 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 876 if err != nil { 877 log.Println("failed to create hidden ref:", err, resp.StatusCode) 878 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 879 return 880 } 881 882 switch resp.StatusCode { 883 case 404: 884 case 400: 885 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 886 return 887 } 888 889 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 890 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 891 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 892 // hiddenRef: hidden/feature-1/main (on repo-fork) 893 // targetBranch: main (on repo-1) 894 // sourceBranch: feature-1 (on repo-fork) 895 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 896 if err != nil { 897 log.Println("failed to compare across branches", err) 898 s.pages.Notice(w, "pull", err.Error()) 899 return 900 } 901 902 sourceRev := comparison.Rev2 903 patch := comparison.Patch 904 905 if !patchutil.IsPatchValid(patch) { 906 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 907 return 908 } 909 910 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 911 if err != nil { 912 log.Println("failed to parse fork AT URI", err) 913 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 914 return 915 } 916 917 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 918 Branch: sourceBranch, 919 RepoAt: &forkAtUri, 920 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked) 921} 922 923func (s *Pulls) createPullRequest( 924 w http.ResponseWriter, 925 r *http.Request, 926 f *reporesolver.ResolvedRepo, 927 user *oauth.User, 928 title, body, targetBranch string, 929 patch string, 930 sourceRev string, 931 pullSource *db.PullSource, 932 recordPullSource *tangled.RepoPull_Source, 933 isStacked bool, 934) { 935 if isStacked { 936 // creates a series of PRs, each linking to the previous, identified by jj's change-id 937 s.createStackedPulLRequest( 938 w, 939 r, 940 f, 941 user, 942 targetBranch, 943 patch, 944 sourceRev, 945 pullSource, 946 ) 947 return 948 } 949 950 client, err := s.oauth.AuthorizedClient(r) 951 if err != nil { 952 log.Println("failed to get authorized client", err) 953 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 954 return 955 } 956 957 tx, err := s.db.BeginTx(r.Context(), nil) 958 if err != nil { 959 log.Println("failed to start tx") 960 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 961 return 962 } 963 defer tx.Rollback() 964 965 // We've already checked earlier if it's diff-based and title is empty, 966 // so if it's still empty now, it's intentionally skipped owing to format-patch. 967 if title == "" { 968 formatPatches, err := patchutil.ExtractPatches(patch) 969 if err != nil { 970 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 971 return 972 } 973 if len(formatPatches) == 0 { 974 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 975 return 976 } 977 978 title = formatPatches[0].Title 979 body = formatPatches[0].Body 980 } 981 982 rkey := appview.TID() 983 initialSubmission := db.PullSubmission{ 984 Patch: patch, 985 SourceRev: sourceRev, 986 } 987 err = db.NewPull(tx, &db.Pull{ 988 Title: title, 989 Body: body, 990 TargetBranch: targetBranch, 991 OwnerDid: user.Did, 992 RepoAt: f.RepoAt, 993 Rkey: rkey, 994 Submissions: []*db.PullSubmission{ 995 &initialSubmission, 996 }, 997 PullSource: pullSource, 998 }) 999 if err != nil { 1000 log.Println("failed to create pull request", err) 1001 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1002 return 1003 } 1004 pullId, err := db.NextPullId(tx, f.RepoAt) 1005 if err != nil { 1006 log.Println("failed to get pull id", err) 1007 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1008 return 1009 } 1010 1011 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1012 Collection: tangled.RepoPullNSID, 1013 Repo: user.Did, 1014 Rkey: rkey, 1015 Record: &lexutil.LexiconTypeDecoder{ 1016 Val: &tangled.RepoPull{ 1017 Title: title, 1018 PullId: int64(pullId), 1019 TargetRepo: string(f.RepoAt), 1020 TargetBranch: targetBranch, 1021 Patch: patch, 1022 Source: recordPullSource, 1023 }, 1024 }, 1025 }) 1026 if err != nil { 1027 log.Println("failed to create pull request", err) 1028 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1029 return 1030 } 1031 1032 if err = tx.Commit(); err != nil { 1033 log.Println("failed to create pull request", err) 1034 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1035 return 1036 } 1037 1038 if !s.config.Core.Dev { 1039 err = s.posthog.Enqueue(posthog.Capture{ 1040 DistinctId: user.Did, 1041 Event: "new_pull", 1042 Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId}, 1043 }) 1044 if err != nil { 1045 log.Println("failed to enqueue posthog event:", err) 1046 } 1047 } 1048 1049 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 1050} 1051 1052func (s *Pulls) createStackedPulLRequest( 1053 w http.ResponseWriter, 1054 r *http.Request, 1055 f *reporesolver.ResolvedRepo, 1056 user *oauth.User, 1057 targetBranch string, 1058 patch string, 1059 sourceRev string, 1060 pullSource *db.PullSource, 1061) { 1062 // run some necessary checks for stacked-prs first 1063 1064 // must be branch or fork based 1065 if sourceRev == "" { 1066 log.Println("stacked PR from patch-based pull") 1067 s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.") 1068 return 1069 } 1070 1071 formatPatches, err := patchutil.ExtractPatches(patch) 1072 if err != nil { 1073 log.Println("failed to extract patches", err) 1074 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1075 return 1076 } 1077 1078 // must have atleast 1 patch to begin with 1079 if len(formatPatches) == 0 { 1080 log.Println("empty patches") 1081 s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 1082 return 1083 } 1084 1085 // build a stack out of this patch 1086 stackId := uuid.New() 1087 stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String()) 1088 if err != nil { 1089 log.Println("failed to create stack", err) 1090 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 1091 return 1092 } 1093 1094 client, err := s.oauth.AuthorizedClient(r) 1095 if err != nil { 1096 log.Println("failed to get authorized client", err) 1097 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1098 return 1099 } 1100 1101 // apply all record creations at once 1102 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1103 for _, p := range stack { 1104 record := p.AsRecord() 1105 write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1106 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1107 Collection: tangled.RepoPullNSID, 1108 Rkey: &p.Rkey, 1109 Value: &lexutil.LexiconTypeDecoder{ 1110 Val: &record, 1111 }, 1112 }, 1113 } 1114 writes = append(writes, &write) 1115 } 1116 _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1117 Repo: user.Did, 1118 Writes: writes, 1119 }) 1120 if err != nil { 1121 log.Println("failed to create stacked pull request", err) 1122 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1123 return 1124 } 1125 1126 // create all pulls at once 1127 tx, err := s.db.BeginTx(r.Context(), nil) 1128 if err != nil { 1129 log.Println("failed to start tx") 1130 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1131 return 1132 } 1133 defer tx.Rollback() 1134 1135 for _, p := range stack { 1136 err = db.NewPull(tx, p) 1137 if err != nil { 1138 log.Println("failed to create pull request", err) 1139 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1140 return 1141 } 1142 } 1143 1144 if err = tx.Commit(); err != nil { 1145 log.Println("failed to create pull request", err) 1146 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1147 return 1148 } 1149 1150 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo())) 1151} 1152 1153func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1154 _, err := s.repoResolver.Resolve(r) 1155 if err != nil { 1156 log.Println("failed to get repo and knot", err) 1157 return 1158 } 1159 1160 patch := r.FormValue("patch") 1161 if patch == "" { 1162 s.pages.Notice(w, "patch-error", "Patch is required.") 1163 return 1164 } 1165 1166 if patch == "" || !patchutil.IsPatchValid(patch) { 1167 s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 1168 return 1169 } 1170 1171 if patchutil.IsFormatPatch(patch) { 1172 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.") 1173 } else { 1174 s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") 1175 } 1176} 1177 1178func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1179 user := s.oauth.GetUser(r) 1180 f, err := s.repoResolver.Resolve(r) 1181 if err != nil { 1182 log.Println("failed to get repo and knot", err) 1183 return 1184 } 1185 1186 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1187 RepoInfo: f.RepoInfo(user), 1188 }) 1189} 1190 1191func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1192 user := s.oauth.GetUser(r) 1193 f, err := s.repoResolver.Resolve(r) 1194 if err != nil { 1195 log.Println("failed to get repo and knot", err) 1196 return 1197 } 1198 1199 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1200 if err != nil { 1201 log.Printf("failed to create unsigned client for %s", f.Knot) 1202 s.pages.Error503(w) 1203 return 1204 } 1205 1206 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1207 if err != nil { 1208 log.Println("failed to reach knotserver", err) 1209 return 1210 } 1211 1212 branches := result.Branches 1213 sort.Slice(branches, func(i int, j int) bool { 1214 return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When) 1215 }) 1216 1217 withoutDefault := []types.Branch{} 1218 for _, b := range branches { 1219 if b.IsDefault { 1220 continue 1221 } 1222 withoutDefault = append(withoutDefault, b) 1223 } 1224 1225 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1226 RepoInfo: f.RepoInfo(user), 1227 Branches: withoutDefault, 1228 }) 1229} 1230 1231func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1232 user := s.oauth.GetUser(r) 1233 f, err := s.repoResolver.Resolve(r) 1234 if err != nil { 1235 log.Println("failed to get repo and knot", err) 1236 return 1237 } 1238 1239 forks, err := db.GetForksByDid(s.db, user.Did) 1240 if err != nil { 1241 log.Println("failed to get forks", err) 1242 return 1243 } 1244 1245 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1246 RepoInfo: f.RepoInfo(user), 1247 Forks: forks, 1248 Selected: r.URL.Query().Get("fork"), 1249 }) 1250} 1251 1252func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1253 user := s.oauth.GetUser(r) 1254 1255 f, err := s.repoResolver.Resolve(r) 1256 if err != nil { 1257 log.Println("failed to get repo and knot", err) 1258 return 1259 } 1260 1261 forkVal := r.URL.Query().Get("fork") 1262 1263 // fork repo 1264 repo, err := db.GetRepo(s.db, user.Did, forkVal) 1265 if err != nil { 1266 log.Println("failed to get repo", user.Did, forkVal) 1267 return 1268 } 1269 1270 sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1271 if err != nil { 1272 log.Printf("failed to create unsigned client for %s", repo.Knot) 1273 s.pages.Error503(w) 1274 return 1275 } 1276 1277 sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1278 if err != nil { 1279 log.Println("failed to reach knotserver for source branches", err) 1280 return 1281 } 1282 1283 targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1284 if err != nil { 1285 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1286 s.pages.Error503(w) 1287 return 1288 } 1289 1290 targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1291 if err != nil { 1292 log.Println("failed to reach knotserver for target branches", err) 1293 return 1294 } 1295 1296 sourceBranches := sourceResult.Branches 1297 sort.Slice(sourceBranches, func(i int, j int) bool { 1298 return sourceBranches[i].Commit.Committer.When.After(sourceBranches[j].Commit.Committer.When) 1299 }) 1300 1301 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1302 RepoInfo: f.RepoInfo(user), 1303 SourceBranches: sourceBranches, 1304 TargetBranches: targetResult.Branches, 1305 }) 1306} 1307 1308func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1309 user := s.oauth.GetUser(r) 1310 f, err := s.repoResolver.Resolve(r) 1311 if err != nil { 1312 log.Println("failed to get repo and knot", err) 1313 return 1314 } 1315 1316 pull, ok := r.Context().Value("pull").(*db.Pull) 1317 if !ok { 1318 log.Println("failed to get pull") 1319 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1320 return 1321 } 1322 1323 switch r.Method { 1324 case http.MethodGet: 1325 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1326 RepoInfo: f.RepoInfo(user), 1327 Pull: pull, 1328 }) 1329 return 1330 case http.MethodPost: 1331 if pull.IsPatchBased() { 1332 s.resubmitPatch(w, r) 1333 return 1334 } else if pull.IsBranchBased() { 1335 s.resubmitBranch(w, r) 1336 return 1337 } else if pull.IsForkBased() { 1338 s.resubmitFork(w, r) 1339 return 1340 } 1341 } 1342} 1343 1344func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1345 user := s.oauth.GetUser(r) 1346 1347 pull, ok := r.Context().Value("pull").(*db.Pull) 1348 if !ok { 1349 log.Println("failed to get pull") 1350 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1351 return 1352 } 1353 1354 f, err := s.repoResolver.Resolve(r) 1355 if err != nil { 1356 log.Println("failed to get repo and knot", err) 1357 return 1358 } 1359 1360 if user.Did != pull.OwnerDid { 1361 log.Println("unauthorized user") 1362 w.WriteHeader(http.StatusUnauthorized) 1363 return 1364 } 1365 1366 patch := r.FormValue("patch") 1367 1368 s.resubmitPullHelper(w, r, f, user, pull, patch, "") 1369} 1370 1371func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1372 user := s.oauth.GetUser(r) 1373 1374 pull, ok := r.Context().Value("pull").(*db.Pull) 1375 if !ok { 1376 log.Println("failed to get pull") 1377 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1378 return 1379 } 1380 1381 f, err := s.repoResolver.Resolve(r) 1382 if err != nil { 1383 log.Println("failed to get repo and knot", err) 1384 return 1385 } 1386 1387 if user.Did != pull.OwnerDid { 1388 log.Println("unauthorized user") 1389 w.WriteHeader(http.StatusUnauthorized) 1390 return 1391 } 1392 1393 if !f.RepoInfo(user).Roles.IsPushAllowed() { 1394 log.Println("unauthorized user") 1395 w.WriteHeader(http.StatusUnauthorized) 1396 return 1397 } 1398 1399 ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1400 if err != nil { 1401 log.Printf("failed to create client for %s: %s", f.Knot, err) 1402 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1403 return 1404 } 1405 1406 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1407 if err != nil { 1408 log.Printf("compare request failed: %s", err) 1409 s.pages.Notice(w, "resubmit-error", err.Error()) 1410 return 1411 } 1412 1413 sourceRev := comparison.Rev2 1414 patch := comparison.Patch 1415 1416 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1417} 1418 1419func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1420 user := s.oauth.GetUser(r) 1421 1422 pull, ok := r.Context().Value("pull").(*db.Pull) 1423 if !ok { 1424 log.Println("failed to get pull") 1425 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1426 return 1427 } 1428 1429 f, err := s.repoResolver.Resolve(r) 1430 if err != nil { 1431 log.Println("failed to get repo and knot", err) 1432 return 1433 } 1434 1435 if user.Did != pull.OwnerDid { 1436 log.Println("unauthorized user") 1437 w.WriteHeader(http.StatusUnauthorized) 1438 return 1439 } 1440 1441 forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1442 if err != nil { 1443 log.Println("failed to get source repo", err) 1444 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1445 return 1446 } 1447 1448 // extract patch by performing compare 1449 ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1450 if err != nil { 1451 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1452 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1453 return 1454 } 1455 1456 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1457 if err != nil { 1458 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1459 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1460 return 1461 } 1462 1463 // update the hidden tracking branch to latest 1464 signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1465 if err != nil { 1466 log.Printf("failed to create signed client 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 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1472 if err != nil || resp.StatusCode != http.StatusNoContent { 1473 log.Printf("failed to update tracking branch: %s", err) 1474 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1475 return 1476 } 1477 1478 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1479 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1480 if err != nil { 1481 log.Printf("failed to compare branches: %s", err) 1482 s.pages.Notice(w, "resubmit-error", err.Error()) 1483 return 1484 } 1485 1486 sourceRev := comparison.Rev2 1487 patch := comparison.Patch 1488 1489 s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1490} 1491 1492// validate a resubmission against a pull request 1493func validateResubmittedPatch(pull *db.Pull, patch string) error { 1494 if patch == "" { 1495 return fmt.Errorf("Patch is empty.") 1496 } 1497 1498 if patch == pull.LatestPatch() { 1499 return fmt.Errorf("Patch is identical to previous submission.") 1500 } 1501 1502 if !patchutil.IsPatchValid(patch) { 1503 return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1504 } 1505 1506 return nil 1507} 1508 1509func (s *Pulls) resubmitPullHelper( 1510 w http.ResponseWriter, 1511 r *http.Request, 1512 f *reporesolver.ResolvedRepo, 1513 user *oauth.User, 1514 pull *db.Pull, 1515 patch string, 1516 sourceRev string, 1517) { 1518 if pull.IsStacked() { 1519 log.Println("resubmitting stacked PR") 1520 s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId) 1521 return 1522 } 1523 1524 if err := validateResubmittedPatch(pull, patch); err != nil { 1525 s.pages.Notice(w, "resubmit-error", err.Error()) 1526 return 1527 } 1528 1529 // validate sourceRev if branch/fork based 1530 if pull.IsBranchBased() || pull.IsForkBased() { 1531 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1532 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1533 return 1534 } 1535 } 1536 1537 tx, err := s.db.BeginTx(r.Context(), nil) 1538 if err != nil { 1539 log.Println("failed to start tx") 1540 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1541 return 1542 } 1543 defer tx.Rollback() 1544 1545 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1546 if err != nil { 1547 log.Println("failed to create pull request", err) 1548 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1549 return 1550 } 1551 client, err := s.oauth.AuthorizedClient(r) 1552 if err != nil { 1553 log.Println("failed to authorize client") 1554 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1555 return 1556 } 1557 1558 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1559 if err != nil { 1560 // failed to get record 1561 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1562 return 1563 } 1564 1565 var recordPullSource *tangled.RepoPull_Source 1566 if pull.IsBranchBased() { 1567 recordPullSource = &tangled.RepoPull_Source{ 1568 Branch: pull.PullSource.Branch, 1569 } 1570 } 1571 if pull.IsForkBased() { 1572 repoAt := pull.PullSource.RepoAt.String() 1573 recordPullSource = &tangled.RepoPull_Source{ 1574 Branch: pull.PullSource.Branch, 1575 Repo: &repoAt, 1576 } 1577 } 1578 1579 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1580 Collection: tangled.RepoPullNSID, 1581 Repo: user.Did, 1582 Rkey: pull.Rkey, 1583 SwapRecord: ex.Cid, 1584 Record: &lexutil.LexiconTypeDecoder{ 1585 Val: &tangled.RepoPull{ 1586 Title: pull.Title, 1587 PullId: int64(pull.PullId), 1588 TargetRepo: string(f.RepoAt), 1589 TargetBranch: pull.TargetBranch, 1590 Patch: patch, // new patch 1591 Source: recordPullSource, 1592 }, 1593 }, 1594 }) 1595 if err != nil { 1596 log.Println("failed to update record", err) 1597 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1598 return 1599 } 1600 1601 if err = tx.Commit(); err != nil { 1602 log.Println("failed to commit transaction", err) 1603 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1604 return 1605 } 1606 1607 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1608 return 1609} 1610 1611func (s *Pulls) resubmitStackedPullHelper( 1612 w http.ResponseWriter, 1613 r *http.Request, 1614 f *reporesolver.ResolvedRepo, 1615 user *oauth.User, 1616 pull *db.Pull, 1617 patch string, 1618 stackId string, 1619) { 1620 targetBranch := pull.TargetBranch 1621 1622 origStack, _ := r.Context().Value("stack").(db.Stack) 1623 newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1624 if err != nil { 1625 log.Println("failed to create resubmitted stack", err) 1626 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1627 return 1628 } 1629 1630 // find the diff between the stacks, first, map them by changeId 1631 origById := make(map[string]*db.Pull) 1632 newById := make(map[string]*db.Pull) 1633 for _, p := range origStack { 1634 origById[p.ChangeId] = p 1635 } 1636 for _, p := range newStack { 1637 newById[p.ChangeId] = p 1638 } 1639 1640 // commits that got deleted: corresponding pull is closed 1641 // commits that got added: new pull is created 1642 // commits that got updated: corresponding pull is resubmitted & new round begins 1643 // 1644 // for commits that were unchanged: no changes, parent-change-id is updated as necessary 1645 additions := make(map[string]*db.Pull) 1646 deletions := make(map[string]*db.Pull) 1647 unchanged := make(map[string]struct{}) 1648 updated := make(map[string]struct{}) 1649 1650 // pulls in orignal stack but not in new one 1651 for _, op := range origStack { 1652 if _, ok := newById[op.ChangeId]; !ok { 1653 deletions[op.ChangeId] = op 1654 } 1655 } 1656 1657 // pulls in new stack but not in original one 1658 for _, np := range newStack { 1659 if _, ok := origById[np.ChangeId]; !ok { 1660 additions[np.ChangeId] = np 1661 } 1662 } 1663 1664 // NOTE: this loop can be written in any of above blocks, 1665 // but is written separately in the interest of simpler code 1666 for _, np := range newStack { 1667 if op, ok := origById[np.ChangeId]; ok { 1668 // pull exists in both stacks 1669 // TODO: can we avoid reparse? 1670 origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch())) 1671 newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch())) 1672 1673 origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr) 1674 newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr) 1675 1676 patchutil.SortPatch(newFiles) 1677 patchutil.SortPatch(origFiles) 1678 1679 // text content of patch may be identical, but a jj rebase might have forwarded it 1680 // 1681 // we still need to update the hash in submission.Patch and submission.SourceRev 1682 if patchutil.Equal(newFiles, origFiles) && 1683 origHeader.Title == newHeader.Title && 1684 origHeader.Body == newHeader.Body { 1685 unchanged[op.ChangeId] = struct{}{} 1686 } else { 1687 updated[op.ChangeId] = struct{}{} 1688 } 1689 } 1690 } 1691 1692 tx, err := s.db.Begin() 1693 if err != nil { 1694 log.Println("failed to start transaction", err) 1695 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1696 return 1697 } 1698 defer tx.Rollback() 1699 1700 // pds updates to make 1701 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1702 1703 // deleted pulls are marked as deleted in the DB 1704 for _, p := range deletions { 1705 err := db.DeletePull(tx, p.RepoAt, p.PullId) 1706 if err != nil { 1707 log.Println("failed to delete pull", err, p.PullId) 1708 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1709 return 1710 } 1711 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1712 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 1713 Collection: tangled.RepoPullNSID, 1714 Rkey: p.Rkey, 1715 }, 1716 }) 1717 } 1718 1719 // new pulls are created 1720 for _, p := range additions { 1721 err := db.NewPull(tx, p) 1722 if err != nil { 1723 log.Println("failed to create pull", err, p.PullId) 1724 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1725 return 1726 } 1727 1728 record := p.AsRecord() 1729 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1730 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1731 Collection: tangled.RepoPullNSID, 1732 Rkey: &p.Rkey, 1733 Value: &lexutil.LexiconTypeDecoder{ 1734 Val: &record, 1735 }, 1736 }, 1737 }) 1738 } 1739 1740 // updated pulls are, well, updated; to start a new round 1741 for id := range updated { 1742 op, _ := origById[id] 1743 np, _ := newById[id] 1744 1745 submission := np.Submissions[np.LastRoundNumber()] 1746 1747 // resubmit the old pull 1748 err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev) 1749 1750 if err != nil { 1751 log.Println("failed to update pull", err, op.PullId) 1752 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1753 return 1754 } 1755 1756 record := op.AsRecord() 1757 record.Patch = submission.Patch 1758 1759 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1760 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1761 Collection: tangled.RepoPullNSID, 1762 Rkey: op.Rkey, 1763 Value: &lexutil.LexiconTypeDecoder{ 1764 Val: &record, 1765 }, 1766 }, 1767 }) 1768 } 1769 1770 // unchanged pulls are edited without starting a new round 1771 // 1772 // update source-revs & patches without advancing rounds 1773 for changeId := range unchanged { 1774 op, _ := origById[changeId] 1775 np, _ := newById[changeId] 1776 1777 origSubmission := op.Submissions[op.LastRoundNumber()] 1778 newSubmission := np.Submissions[np.LastRoundNumber()] 1779 1780 log.Println("moving unchanged change id : ", changeId) 1781 1782 err := db.UpdatePull( 1783 tx, 1784 newSubmission.Patch, 1785 newSubmission.SourceRev, 1786 db.FilterEq("id", origSubmission.ID), 1787 ) 1788 1789 if err != nil { 1790 log.Println("failed to update pull", err, op.PullId) 1791 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1792 return 1793 } 1794 1795 record := op.AsRecord() 1796 record.Patch = newSubmission.Patch 1797 1798 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1799 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1800 Collection: tangled.RepoPullNSID, 1801 Rkey: op.Rkey, 1802 Value: &lexutil.LexiconTypeDecoder{ 1803 Val: &record, 1804 }, 1805 }, 1806 }) 1807 } 1808 1809 // update parent-change-id relations for the entire stack 1810 for _, p := range newStack { 1811 err := db.SetPullParentChangeId( 1812 tx, 1813 p.ParentChangeId, 1814 // these should be enough filters to be unique per-stack 1815 db.FilterEq("repo_at", p.RepoAt.String()), 1816 db.FilterEq("owner_did", p.OwnerDid), 1817 db.FilterEq("change_id", p.ChangeId), 1818 ) 1819 1820 if err != nil { 1821 log.Println("failed to update pull", err, p.PullId) 1822 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1823 return 1824 } 1825 } 1826 1827 err = tx.Commit() 1828 if err != nil { 1829 log.Println("failed to resubmit pull", err) 1830 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1831 return 1832 } 1833 1834 client, err := s.oauth.AuthorizedClient(r) 1835 if err != nil { 1836 log.Println("failed to authorize client") 1837 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1838 return 1839 } 1840 1841 _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1842 Repo: user.Did, 1843 Writes: writes, 1844 }) 1845 if err != nil { 1846 log.Println("failed to create stacked pull request", err) 1847 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1848 return 1849 } 1850 1851 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1852 return 1853} 1854 1855func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 1856 f, err := s.repoResolver.Resolve(r) 1857 if err != nil { 1858 log.Println("failed to resolve repo:", err) 1859 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1860 return 1861 } 1862 1863 pull, ok := r.Context().Value("pull").(*db.Pull) 1864 if !ok { 1865 log.Println("failed to get pull") 1866 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1867 return 1868 } 1869 1870 var pullsToMerge db.Stack 1871 pullsToMerge = append(pullsToMerge, pull) 1872 if pull.IsStacked() { 1873 stack, ok := r.Context().Value("stack").(db.Stack) 1874 if !ok { 1875 log.Println("failed to get stack") 1876 s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1877 return 1878 } 1879 1880 // combine patches of substack 1881 subStack := stack.StrictlyBelow(pull) 1882 // collect the portion of the stack that is mergeable 1883 mergeable := subStack.Mergeable() 1884 // add to total patch 1885 pullsToMerge = append(pullsToMerge, mergeable...) 1886 } 1887 1888 patch := pullsToMerge.CombinedPatch() 1889 1890 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1891 if err != nil { 1892 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1893 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1894 return 1895 } 1896 1897 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 1898 if err != nil { 1899 log.Printf("resolving identity: %s", err) 1900 w.WriteHeader(http.StatusNotFound) 1901 return 1902 } 1903 1904 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1905 if err != nil { 1906 log.Printf("failed to get primary email: %s", err) 1907 } 1908 1909 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1910 if err != nil { 1911 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1912 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1913 return 1914 } 1915 1916 // Merge the pull request 1917 resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1918 if err != nil { 1919 log.Printf("failed to merge pull request: %s", err) 1920 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1921 return 1922 } 1923 1924 if resp.StatusCode != http.StatusOK { 1925 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1926 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1927 return 1928 } 1929 1930 tx, err := s.db.Begin() 1931 if err != nil { 1932 log.Println("failed to start transcation", err) 1933 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1934 return 1935 } 1936 defer tx.Rollback() 1937 1938 for _, p := range pullsToMerge { 1939 err := db.MergePull(tx, f.RepoAt, p.PullId) 1940 if err != nil { 1941 log.Printf("failed to update pull request status in database: %s", err) 1942 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1943 return 1944 } 1945 } 1946 1947 err = tx.Commit() 1948 if err != nil { 1949 // TODO: this is unsound, we should also revert the merge from the knotserver here 1950 log.Printf("failed to update pull request status in database: %s", err) 1951 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1952 return 1953 } 1954 1955 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1956} 1957 1958func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 1959 user := s.oauth.GetUser(r) 1960 1961 f, err := s.repoResolver.Resolve(r) 1962 if err != nil { 1963 log.Println("malformed middleware") 1964 return 1965 } 1966 1967 pull, ok := r.Context().Value("pull").(*db.Pull) 1968 if !ok { 1969 log.Println("failed to get pull") 1970 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1971 return 1972 } 1973 1974 // auth filter: only owner or collaborators can close 1975 roles := f.RolesInRepo(user) 1976 isCollaborator := roles.IsCollaborator() 1977 isPullAuthor := user.Did == pull.OwnerDid 1978 isCloseAllowed := isCollaborator || isPullAuthor 1979 if !isCloseAllowed { 1980 log.Println("failed to close pull") 1981 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1982 return 1983 } 1984 1985 // Start a transaction 1986 tx, err := s.db.BeginTx(r.Context(), nil) 1987 if err != nil { 1988 log.Println("failed to start transaction", err) 1989 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1990 return 1991 } 1992 defer tx.Rollback() 1993 1994 var pullsToClose []*db.Pull 1995 pullsToClose = append(pullsToClose, pull) 1996 1997 // if this PR is stacked, then we want to close all PRs below this one on the stack 1998 if pull.IsStacked() { 1999 stack := r.Context().Value("stack").(db.Stack) 2000 subStack := stack.StrictlyBelow(pull) 2001 pullsToClose = append(pullsToClose, subStack...) 2002 } 2003 2004 for _, p := range pullsToClose { 2005 // Close the pull in the database 2006 err = db.ClosePull(tx, f.RepoAt, p.PullId) 2007 if err != nil { 2008 log.Println("failed to close pull", err) 2009 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2010 return 2011 } 2012 } 2013 2014 // Commit the transaction 2015 if err = tx.Commit(); err != nil { 2016 log.Println("failed to commit transaction", err) 2017 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2018 return 2019 } 2020 2021 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2022 return 2023} 2024 2025func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2026 user := s.oauth.GetUser(r) 2027 2028 f, err := s.repoResolver.Resolve(r) 2029 if err != nil { 2030 log.Println("failed to resolve repo", err) 2031 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2032 return 2033 } 2034 2035 pull, ok := r.Context().Value("pull").(*db.Pull) 2036 if !ok { 2037 log.Println("failed to get pull") 2038 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2039 return 2040 } 2041 2042 // auth filter: only owner or collaborators can close 2043 roles := f.RolesInRepo(user) 2044 isCollaborator := roles.IsCollaborator() 2045 isPullAuthor := user.Did == pull.OwnerDid 2046 isCloseAllowed := isCollaborator || isPullAuthor 2047 if !isCloseAllowed { 2048 log.Println("failed to close pull") 2049 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2050 return 2051 } 2052 2053 // Start a transaction 2054 tx, err := s.db.BeginTx(r.Context(), nil) 2055 if err != nil { 2056 log.Println("failed to start transaction", err) 2057 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2058 return 2059 } 2060 defer tx.Rollback() 2061 2062 var pullsToReopen []*db.Pull 2063 pullsToReopen = append(pullsToReopen, pull) 2064 2065 // if this PR is stacked, then we want to reopen all PRs above this one on the stack 2066 if pull.IsStacked() { 2067 stack := r.Context().Value("stack").(db.Stack) 2068 subStack := stack.StrictlyAbove(pull) 2069 pullsToReopen = append(pullsToReopen, subStack...) 2070 } 2071 2072 for _, p := range pullsToReopen { 2073 // Close the pull in the database 2074 err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2075 if err != nil { 2076 log.Println("failed to close pull", err) 2077 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2078 return 2079 } 2080 } 2081 2082 // Commit the transaction 2083 if err = tx.Commit(); err != nil { 2084 log.Println("failed to commit transaction", err) 2085 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2086 return 2087 } 2088 2089 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2090 return 2091} 2092 2093func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2094 formatPatches, err := patchutil.ExtractPatches(patch) 2095 if err != nil { 2096 return nil, fmt.Errorf("Failed to extract patches: %v", err) 2097 } 2098 2099 // must have atleast 1 patch to begin with 2100 if len(formatPatches) == 0 { 2101 return nil, fmt.Errorf("No patches found in the generated format-patch.") 2102 } 2103 2104 // the stack is identified by a UUID 2105 var stack db.Stack 2106 parentChangeId := "" 2107 for _, fp := range formatPatches { 2108 // all patches must have a jj change-id 2109 changeId, err := fp.ChangeId() 2110 if err != nil { 2111 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 2112 } 2113 2114 title := fp.Title 2115 body := fp.Body 2116 rkey := appview.TID() 2117 2118 initialSubmission := db.PullSubmission{ 2119 Patch: fp.Raw, 2120 SourceRev: fp.SHA, 2121 } 2122 pull := db.Pull{ 2123 Title: title, 2124 Body: body, 2125 TargetBranch: targetBranch, 2126 OwnerDid: user.Did, 2127 RepoAt: f.RepoAt, 2128 Rkey: rkey, 2129 Submissions: []*db.PullSubmission{ 2130 &initialSubmission, 2131 }, 2132 PullSource: pullSource, 2133 Created: time.Now(), 2134 2135 StackId: stackId, 2136 ChangeId: changeId, 2137 ParentChangeId: parentChangeId, 2138 } 2139 2140 stack = append(stack, &pull) 2141 2142 parentChangeId = changeId 2143 } 2144 2145 return stack, nil 2146}