forked from tangled.org/core
this repo has no description
1package state 2 3import ( 4 "database/sql" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "net/url" 12 "strconv" 13 "time" 14 15 "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 // Validate required fields for all PR types 562 if title == "" || body == "" || targetBranch == "" { 563 s.pages.Notice(w, "pull", "Title, body and target branch are required.") 564 return 565 } 566 567 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 568 if err != nil { 569 log.Println("failed to create unsigned client to %s: %v", f.Knot, err) 570 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 571 return 572 } 573 574 caps, err := us.Capabilities() 575 if err != nil { 576 log.Println("error fetching knot caps", f.Knot, err) 577 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 578 return 579 } 580 581 // Determine PR type based on input parameters 582 isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 583 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 584 isForkBased := fromFork != "" && sourceBranch != "" 585 isPatchBased := patch != "" && !isBranchBased && !isForkBased 586 587 // Validate we have at least one valid PR creation method 588 if !isBranchBased && !isPatchBased && !isForkBased { 589 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 590 return 591 } 592 593 // Can't mix branch-based and patch-based approaches 594 if isBranchBased && patch != "" { 595 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 596 return 597 } 598 599 // Handle the PR creation based on the type 600 if isBranchBased { 601 if !caps.PullRequests.BranchSubmissions { 602 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 603 return 604 } 605 s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch) 606 } else if isForkBased { 607 if !caps.PullRequests.ForkSubmissions { 608 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 609 return 610 } 611 s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch) 612 } else if isPatchBased { 613 if !caps.PullRequests.PatchSubmissions { 614 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 615 return 616 } 617 s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch) 618 } 619 return 620 } 621} 622 623func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 624 pullSource := &db.PullSource{ 625 Branch: sourceBranch, 626 } 627 recordPullSource := &tangled.RepoPull_Source{ 628 Branch: sourceBranch, 629 } 630 631 // Generate a patch using /compare 632 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 633 if err != nil { 634 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 635 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 636 return 637 } 638 639 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 640 if err != nil { 641 log.Println("failed to compare", err) 642 s.pages.Notice(w, "pull", err.Error()) 643 return 644 } 645 646 sourceRev := comparison.Rev2 647 patch := comparison.Patch 648 649 if !patchutil.IsPatchValid(patch) { 650 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 651 return 652 } 653 654 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 655} 656 657func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 658 if !patchutil.IsPatchValid(patch) { 659 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 660 return 661 } 662 663 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 664} 665 666func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 667 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 668 if errors.Is(err, sql.ErrNoRows) { 669 s.pages.Notice(w, "pull", "No such fork.") 670 return 671 } else if err != nil { 672 log.Println("failed to fetch fork:", err) 673 s.pages.Notice(w, "pull", "Failed to fetch fork.") 674 return 675 } 676 677 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 678 if err != nil { 679 log.Println("failed to fetch registration key:", err) 680 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 681 return 682 } 683 684 sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 685 if err != nil { 686 log.Println("failed to create signed client:", err) 687 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 688 return 689 } 690 691 us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 692 if err != nil { 693 log.Println("failed to create unsigned client:", err) 694 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 695 return 696 } 697 698 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 699 if err != nil { 700 log.Println("failed to create hidden ref:", err, resp.StatusCode) 701 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 702 return 703 } 704 705 switch resp.StatusCode { 706 case 404: 707 case 400: 708 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 709 return 710 } 711 712 hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 713 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 714 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 715 // hiddenRef: hidden/feature-1/main (on repo-fork) 716 // targetBranch: main (on repo-1) 717 // sourceBranch: feature-1 (on repo-fork) 718 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 719 if err != nil { 720 log.Println("failed to compare across branches", err) 721 s.pages.Notice(w, "pull", err.Error()) 722 return 723 } 724 725 sourceRev := comparison.Rev2 726 patch := comparison.Patch 727 728 if patchutil.IsPatchValid(patch) { 729 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 730 return 731 } 732 733 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 734 if err != nil { 735 log.Println("failed to parse fork AT URI", err) 736 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 737 return 738 } 739 740 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 741 Branch: sourceBranch, 742 RepoAt: &forkAtUri, 743 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) 744} 745 746func (s *State) createPullRequest( 747 w http.ResponseWriter, 748 r *http.Request, 749 f *FullyResolvedRepo, 750 user *auth.User, 751 title, body, targetBranch string, 752 patch string, 753 sourceRev string, 754 pullSource *db.PullSource, 755 recordPullSource *tangled.RepoPull_Source, 756) { 757 tx, err := s.db.BeginTx(r.Context(), nil) 758 if err != nil { 759 log.Println("failed to start tx") 760 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 761 return 762 } 763 defer tx.Rollback() 764 765 rkey := s.TID() 766 initialSubmission := db.PullSubmission{ 767 Patch: patch, 768 SourceRev: sourceRev, 769 } 770 err = db.NewPull(tx, &db.Pull{ 771 Title: title, 772 Body: body, 773 TargetBranch: targetBranch, 774 OwnerDid: user.Did, 775 RepoAt: f.RepoAt, 776 Rkey: rkey, 777 Submissions: []*db.PullSubmission{ 778 &initialSubmission, 779 }, 780 PullSource: pullSource, 781 }) 782 if err != nil { 783 log.Println("failed to create pull request", err) 784 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 785 return 786 } 787 client, _ := s.auth.AuthorizedClient(r) 788 pullId, err := db.NextPullId(s.db, f.RepoAt) 789 if err != nil { 790 log.Println("failed to get pull id", err) 791 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 792 return 793 } 794 795 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 796 Collection: tangled.RepoPullNSID, 797 Repo: user.Did, 798 Rkey: rkey, 799 Record: &lexutil.LexiconTypeDecoder{ 800 Val: &tangled.RepoPull{ 801 Title: title, 802 PullId: int64(pullId), 803 TargetRepo: string(f.RepoAt), 804 TargetBranch: targetBranch, 805 Patch: patch, 806 Source: recordPullSource, 807 }, 808 }, 809 }) 810 811 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 812 if err != nil { 813 log.Println("failed to get pull id", err) 814 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 815 return 816 } 817 818 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 819} 820 821func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 822 user := s.auth.GetUser(r) 823 f, err := fullyResolvedRepo(r) 824 if err != nil { 825 log.Println("failed to get repo and knot", err) 826 return 827 } 828 829 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 830 RepoInfo: f.RepoInfo(s, user), 831 }) 832} 833 834func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 835 user := s.auth.GetUser(r) 836 f, err := fullyResolvedRepo(r) 837 if err != nil { 838 log.Println("failed to get repo and knot", err) 839 return 840 } 841 842 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 843 if err != nil { 844 log.Printf("failed to create unsigned client for %s", f.Knot) 845 s.pages.Error503(w) 846 return 847 } 848 849 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 850 if err != nil { 851 log.Println("failed to reach knotserver", err) 852 return 853 } 854 855 body, err := io.ReadAll(resp.Body) 856 if err != nil { 857 log.Printf("Error reading response body: %v", err) 858 return 859 } 860 861 var result types.RepoBranchesResponse 862 err = json.Unmarshal(body, &result) 863 if err != nil { 864 log.Println("failed to parse response:", err) 865 return 866 } 867 868 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 869 RepoInfo: f.RepoInfo(s, user), 870 Branches: result.Branches, 871 }) 872} 873 874func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 875 user := s.auth.GetUser(r) 876 f, err := fullyResolvedRepo(r) 877 if err != nil { 878 log.Println("failed to get repo and knot", err) 879 return 880 } 881 882 forks, err := db.GetForksByDid(s.db, user.Did) 883 if err != nil { 884 log.Println("failed to get forks", err) 885 return 886 } 887 888 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 889 RepoInfo: f.RepoInfo(s, user), 890 Forks: forks, 891 }) 892} 893 894func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 895 user := s.auth.GetUser(r) 896 897 f, err := fullyResolvedRepo(r) 898 if err != nil { 899 log.Println("failed to get repo and knot", err) 900 return 901 } 902 903 forkVal := r.URL.Query().Get("fork") 904 905 // fork repo 906 repo, err := db.GetRepo(s.db, user.Did, forkVal) 907 if err != nil { 908 log.Println("failed to get repo", user.Did, forkVal) 909 return 910 } 911 912 sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 913 if err != nil { 914 log.Printf("failed to create unsigned client for %s", repo.Knot) 915 s.pages.Error503(w) 916 return 917 } 918 919 sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name) 920 if err != nil { 921 log.Println("failed to reach knotserver for source branches", err) 922 return 923 } 924 925 sourceBody, err := io.ReadAll(sourceResp.Body) 926 if err != nil { 927 log.Println("failed to read source response body", err) 928 return 929 } 930 defer sourceResp.Body.Close() 931 932 var sourceResult types.RepoBranchesResponse 933 err = json.Unmarshal(sourceBody, &sourceResult) 934 if err != nil { 935 log.Println("failed to parse source branches response:", err) 936 return 937 } 938 939 targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 940 if err != nil { 941 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 942 s.pages.Error503(w) 943 return 944 } 945 946 targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 947 if err != nil { 948 log.Println("failed to reach knotserver for target branches", err) 949 return 950 } 951 952 targetBody, err := io.ReadAll(targetResp.Body) 953 if err != nil { 954 log.Println("failed to read target response body", err) 955 return 956 } 957 defer targetResp.Body.Close() 958 959 var targetResult types.RepoBranchesResponse 960 err = json.Unmarshal(targetBody, &targetResult) 961 if err != nil { 962 log.Println("failed to parse target branches response:", err) 963 return 964 } 965 966 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 967 RepoInfo: f.RepoInfo(s, user), 968 SourceBranches: sourceResult.Branches, 969 TargetBranches: targetResult.Branches, 970 }) 971} 972 973func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 974 user := s.auth.GetUser(r) 975 f, err := fullyResolvedRepo(r) 976 if err != nil { 977 log.Println("failed to get repo and knot", err) 978 return 979 } 980 981 pull, ok := r.Context().Value("pull").(*db.Pull) 982 if !ok { 983 log.Println("failed to get pull") 984 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 985 return 986 } 987 988 switch r.Method { 989 case http.MethodGet: 990 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 991 RepoInfo: f.RepoInfo(s, user), 992 Pull: pull, 993 }) 994 return 995 case http.MethodPost: 996 if pull.IsPatchBased() { 997 s.resubmitPatch(w, r) 998 return 999 } else if pull.IsBranchBased() { 1000 s.resubmitBranch(w, r) 1001 return 1002 } else if pull.IsForkBased() { 1003 s.resubmitFork(w, r) 1004 return 1005 } 1006 } 1007} 1008 1009func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1010 user := s.auth.GetUser(r) 1011 1012 pull, ok := r.Context().Value("pull").(*db.Pull) 1013 if !ok { 1014 log.Println("failed to get pull") 1015 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1016 return 1017 } 1018 1019 f, err := fullyResolvedRepo(r) 1020 if err != nil { 1021 log.Println("failed to get repo and knot", err) 1022 return 1023 } 1024 1025 if user.Did != pull.OwnerDid { 1026 log.Println("unauthorized user") 1027 w.WriteHeader(http.StatusUnauthorized) 1028 return 1029 } 1030 1031 patch := r.FormValue("patch") 1032 1033 if err = validateResubmittedPatch(pull, patch); err != nil { 1034 s.pages.Notice(w, "resubmit-error", err.Error()) 1035 } 1036 1037 tx, err := s.db.BeginTx(r.Context(), nil) 1038 if err != nil { 1039 log.Println("failed to start tx") 1040 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1041 return 1042 } 1043 defer tx.Rollback() 1044 1045 err = db.ResubmitPull(tx, pull, patch, "") 1046 if err != nil { 1047 log.Println("failed to resubmit pull request", err) 1048 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1049 return 1050 } 1051 client, _ := s.auth.AuthorizedClient(r) 1052 1053 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1054 if err != nil { 1055 // failed to get record 1056 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1057 return 1058 } 1059 1060 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1061 Collection: tangled.RepoPullNSID, 1062 Repo: user.Did, 1063 Rkey: pull.Rkey, 1064 SwapRecord: ex.Cid, 1065 Record: &lexutil.LexiconTypeDecoder{ 1066 Val: &tangled.RepoPull{ 1067 Title: pull.Title, 1068 PullId: int64(pull.PullId), 1069 TargetRepo: string(f.RepoAt), 1070 TargetBranch: pull.TargetBranch, 1071 Patch: patch, // new patch 1072 }, 1073 }, 1074 }) 1075 if err != nil { 1076 log.Println("failed to update record", err) 1077 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1078 return 1079 } 1080 1081 if err = tx.Commit(); err != nil { 1082 log.Println("failed to commit transaction", err) 1083 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1084 return 1085 } 1086 1087 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1088 return 1089} 1090 1091func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1092 user := s.auth.GetUser(r) 1093 1094 pull, ok := r.Context().Value("pull").(*db.Pull) 1095 if !ok { 1096 log.Println("failed to get pull") 1097 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1098 return 1099 } 1100 1101 f, err := fullyResolvedRepo(r) 1102 if err != nil { 1103 log.Println("failed to get repo and knot", err) 1104 return 1105 } 1106 1107 if user.Did != pull.OwnerDid { 1108 log.Println("unauthorized user") 1109 w.WriteHeader(http.StatusUnauthorized) 1110 return 1111 } 1112 1113 if !f.RepoInfo(s, user).Roles.IsPushAllowed() { 1114 log.Println("unauthorized user") 1115 w.WriteHeader(http.StatusUnauthorized) 1116 return 1117 } 1118 1119 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1120 if err != nil { 1121 log.Printf("failed to create client for %s: %s", f.Knot, err) 1122 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1123 return 1124 } 1125 1126 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1127 if err != nil { 1128 log.Printf("compare request failed: %s", err) 1129 s.pages.Notice(w, "resubmit-error", err.Error()) 1130 return 1131 } 1132 1133 sourceRev := comparison.Rev2 1134 patch := comparison.Patch 1135 1136 if err = validateResubmittedPatch(pull, patch); err != nil { 1137 s.pages.Notice(w, "resubmit-error", err.Error()) 1138 } 1139 1140 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1141 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1142 return 1143 } 1144 1145 tx, err := s.db.BeginTx(r.Context(), nil) 1146 if err != nil { 1147 log.Println("failed to start tx") 1148 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1149 return 1150 } 1151 defer tx.Rollback() 1152 1153 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1154 if err != nil { 1155 log.Println("failed to create pull request", err) 1156 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1157 return 1158 } 1159 client, _ := s.auth.AuthorizedClient(r) 1160 1161 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1162 if err != nil { 1163 // failed to get record 1164 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1165 return 1166 } 1167 1168 recordPullSource := &tangled.RepoPull_Source{ 1169 Branch: pull.PullSource.Branch, 1170 } 1171 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1172 Collection: tangled.RepoPullNSID, 1173 Repo: user.Did, 1174 Rkey: pull.Rkey, 1175 SwapRecord: ex.Cid, 1176 Record: &lexutil.LexiconTypeDecoder{ 1177 Val: &tangled.RepoPull{ 1178 Title: pull.Title, 1179 PullId: int64(pull.PullId), 1180 TargetRepo: string(f.RepoAt), 1181 TargetBranch: pull.TargetBranch, 1182 Patch: patch, // new patch 1183 Source: recordPullSource, 1184 }, 1185 }, 1186 }) 1187 if err != nil { 1188 log.Println("failed to update record", err) 1189 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1190 return 1191 } 1192 1193 if err = tx.Commit(); err != nil { 1194 log.Println("failed to commit transaction", err) 1195 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1196 return 1197 } 1198 1199 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1200 return 1201} 1202 1203func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1204 user := s.auth.GetUser(r) 1205 1206 pull, ok := r.Context().Value("pull").(*db.Pull) 1207 if !ok { 1208 log.Println("failed to get pull") 1209 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1210 return 1211 } 1212 1213 f, err := fullyResolvedRepo(r) 1214 if err != nil { 1215 log.Println("failed to get repo and knot", err) 1216 return 1217 } 1218 1219 if user.Did != pull.OwnerDid { 1220 log.Println("unauthorized user") 1221 w.WriteHeader(http.StatusUnauthorized) 1222 return 1223 } 1224 1225 forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1226 if err != nil { 1227 log.Println("failed to get source repo", err) 1228 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1229 return 1230 } 1231 1232 // extract patch by performing compare 1233 ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1234 if err != nil { 1235 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1236 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1237 return 1238 } 1239 1240 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1241 if err != nil { 1242 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1243 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1244 return 1245 } 1246 1247 // update the hidden tracking branch to latest 1248 signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1249 if err != nil { 1250 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1251 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1252 return 1253 } 1254 1255 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1256 if err != nil || resp.StatusCode != http.StatusNoContent { 1257 log.Printf("failed to update tracking branch: %s", err) 1258 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1259 return 1260 } 1261 1262 hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)) 1263 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1264 if err != nil { 1265 log.Printf("failed to compare branches: %s", err) 1266 s.pages.Notice(w, "resubmit-error", err.Error()) 1267 return 1268 } 1269 1270 sourceRev := comparison.Rev2 1271 patch := comparison.Patch 1272 1273 if err = validateResubmittedPatch(pull, patch); err != nil { 1274 s.pages.Notice(w, "resubmit-error", err.Error()) 1275 } 1276 1277 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1278 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1279 return 1280 } 1281 1282 tx, err := s.db.BeginTx(r.Context(), nil) 1283 if err != nil { 1284 log.Println("failed to start tx") 1285 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1286 return 1287 } 1288 defer tx.Rollback() 1289 1290 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1291 if err != nil { 1292 log.Println("failed to create pull request", err) 1293 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1294 return 1295 } 1296 client, _ := s.auth.AuthorizedClient(r) 1297 1298 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1299 if err != nil { 1300 // failed to get record 1301 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1302 return 1303 } 1304 1305 repoAt := pull.PullSource.RepoAt.String() 1306 recordPullSource := &tangled.RepoPull_Source{ 1307 Branch: pull.PullSource.Branch, 1308 Repo: &repoAt, 1309 } 1310 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1311 Collection: tangled.RepoPullNSID, 1312 Repo: user.Did, 1313 Rkey: pull.Rkey, 1314 SwapRecord: ex.Cid, 1315 Record: &lexutil.LexiconTypeDecoder{ 1316 Val: &tangled.RepoPull{ 1317 Title: pull.Title, 1318 PullId: int64(pull.PullId), 1319 TargetRepo: string(f.RepoAt), 1320 TargetBranch: pull.TargetBranch, 1321 Patch: patch, // new patch 1322 Source: recordPullSource, 1323 }, 1324 }, 1325 }) 1326 if err != nil { 1327 log.Println("failed to update record", err) 1328 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1329 return 1330 } 1331 1332 if err = tx.Commit(); err != nil { 1333 log.Println("failed to commit transaction", err) 1334 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1335 return 1336 } 1337 1338 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1339 return 1340} 1341 1342// validate a resubmission against a pull request 1343func validateResubmittedPatch(pull *db.Pull, patch string) error { 1344 if patch == "" { 1345 return fmt.Errorf("Patch is empty.") 1346 } 1347 1348 if patch == pull.LatestPatch() { 1349 return fmt.Errorf("Patch is identical to previous submission.") 1350 } 1351 1352 if patchutil.IsPatchValid(patch) { 1353 return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1354 } 1355 1356 return nil 1357} 1358 1359func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1360 f, err := fullyResolvedRepo(r) 1361 if err != nil { 1362 log.Println("failed to resolve repo:", err) 1363 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1364 return 1365 } 1366 1367 pull, ok := r.Context().Value("pull").(*db.Pull) 1368 if !ok { 1369 log.Println("failed to get pull") 1370 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1371 return 1372 } 1373 1374 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1375 if err != nil { 1376 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1377 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1378 return 1379 } 1380 1381 ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid) 1382 if err != nil { 1383 log.Printf("resolving identity: %s", err) 1384 w.WriteHeader(http.StatusNotFound) 1385 return 1386 } 1387 1388 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1389 if err != nil { 1390 log.Printf("failed to get primary email: %s", err) 1391 } 1392 1393 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1394 if err != nil { 1395 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1396 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1397 return 1398 } 1399 1400 // Merge the pull request 1401 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1402 if err != nil { 1403 log.Printf("failed to merge pull request: %s", err) 1404 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1405 return 1406 } 1407 1408 if resp.StatusCode == http.StatusOK { 1409 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1410 if err != nil { 1411 log.Printf("failed to update pull request status in database: %s", err) 1412 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1413 return 1414 } 1415 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1416 } else { 1417 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1418 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1419 } 1420} 1421 1422func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1423 user := s.auth.GetUser(r) 1424 1425 f, err := fullyResolvedRepo(r) 1426 if err != nil { 1427 log.Println("malformed middleware") 1428 return 1429 } 1430 1431 pull, ok := r.Context().Value("pull").(*db.Pull) 1432 if !ok { 1433 log.Println("failed to get pull") 1434 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1435 return 1436 } 1437 1438 // auth filter: only owner or collaborators can close 1439 roles := RolesInRepo(s, user, f) 1440 isCollaborator := roles.IsCollaborator() 1441 isPullAuthor := user.Did == pull.OwnerDid 1442 isCloseAllowed := isCollaborator || isPullAuthor 1443 if !isCloseAllowed { 1444 log.Println("failed to close pull") 1445 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1446 return 1447 } 1448 1449 // Start a transaction 1450 tx, err := s.db.BeginTx(r.Context(), nil) 1451 if err != nil { 1452 log.Println("failed to start transaction", err) 1453 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1454 return 1455 } 1456 1457 // Close the pull in the database 1458 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 1459 if err != nil { 1460 log.Println("failed to close pull", err) 1461 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1462 return 1463 } 1464 1465 // Commit the transaction 1466 if err = tx.Commit(); err != nil { 1467 log.Println("failed to commit transaction", err) 1468 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1469 return 1470 } 1471 1472 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1473 return 1474} 1475 1476func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1477 user := s.auth.GetUser(r) 1478 1479 f, err := fullyResolvedRepo(r) 1480 if err != nil { 1481 log.Println("failed to resolve repo", err) 1482 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1483 return 1484 } 1485 1486 pull, ok := r.Context().Value("pull").(*db.Pull) 1487 if !ok { 1488 log.Println("failed to get pull") 1489 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1490 return 1491 } 1492 1493 // auth filter: only owner or collaborators can close 1494 roles := RolesInRepo(s, user, f) 1495 isCollaborator := roles.IsCollaborator() 1496 isPullAuthor := user.Did == pull.OwnerDid 1497 isCloseAllowed := isCollaborator || isPullAuthor 1498 if !isCloseAllowed { 1499 log.Println("failed to close pull") 1500 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1501 return 1502 } 1503 1504 // Start a transaction 1505 tx, err := s.db.BeginTx(r.Context(), nil) 1506 if err != nil { 1507 log.Println("failed to start transaction", err) 1508 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1509 return 1510 } 1511 1512 // Reopen the pull in the database 1513 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 1514 if err != nil { 1515 log.Println("failed to reopen pull", err) 1516 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1517 return 1518 } 1519 1520 // Commit the transaction 1521 if err = tx.Commit(); err != nil { 1522 log.Println("failed to commit transaction", err) 1523 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1524 return 1525 } 1526 1527 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1528 return 1529}