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