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