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