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