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