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