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