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