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