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