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