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