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