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