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