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