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