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