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