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