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