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 "strings" 14 "time" 15 16 "github.com/go-chi/chi/v5" 17 "tangled.sh/tangled.sh/core/api/tangled" 18 "tangled.sh/tangled.sh/core/appview/auth" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 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 var resubmitResult pages.ResubmitResult 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 var resubmitResult pages.ResubmitResult 119 if 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 return pages.ShouldResubmit 258 } 259 260 return pages.ShouldNotResubmit 261} 262 263func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 264 user := s.auth.GetUser(r) 265 f, err := fullyResolvedRepo(r) 266 if err != nil { 267 log.Println("failed to get repo and knot", err) 268 return 269 } 270 271 pull, ok := r.Context().Value("pull").(*db.Pull) 272 if !ok { 273 log.Println("failed to get pull") 274 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 275 return 276 } 277 278 roundId := chi.URLParam(r, "round") 279 roundIdInt, err := strconv.Atoi(roundId) 280 if err != nil || roundIdInt >= len(pull.Submissions) { 281 http.Error(w, "bad round id", http.StatusBadRequest) 282 log.Println("failed to parse round id", err) 283 return 284 } 285 286 identsToResolve := []string{pull.OwnerDid} 287 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 288 didHandleMap := make(map[string]string) 289 for _, identity := range resolvedIds { 290 if !identity.Handle.IsInvalidHandle() { 291 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 292 } else { 293 didHandleMap[identity.DID.String()] = identity.DID.String() 294 } 295 } 296 297 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 298 LoggedInUser: user, 299 DidHandleMap: didHandleMap, 300 RepoInfo: f.RepoInfo(s, user), 301 Pull: pull, 302 Round: roundIdInt, 303 Submission: pull.Submissions[roundIdInt], 304 Diff: pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch), 305 }) 306 307} 308 309func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 310 pull, ok := r.Context().Value("pull").(*db.Pull) 311 if !ok { 312 log.Println("failed to get pull") 313 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 314 return 315 } 316 317 roundId := chi.URLParam(r, "round") 318 roundIdInt, err := strconv.Atoi(roundId) 319 if err != nil || roundIdInt >= len(pull.Submissions) { 320 http.Error(w, "bad round id", http.StatusBadRequest) 321 log.Println("failed to parse round id", err) 322 return 323 } 324 325 identsToResolve := []string{pull.OwnerDid} 326 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 327 didHandleMap := make(map[string]string) 328 for _, identity := range resolvedIds { 329 if !identity.Handle.IsInvalidHandle() { 330 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 331 } else { 332 didHandleMap[identity.DID.String()] = identity.DID.String() 333 } 334 } 335 336 w.Header().Set("Content-Type", "text/plain") 337 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 338} 339 340func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 341 user := s.auth.GetUser(r) 342 params := r.URL.Query() 343 344 state := db.PullOpen 345 switch params.Get("state") { 346 case "closed": 347 state = db.PullClosed 348 case "merged": 349 state = db.PullMerged 350 } 351 352 f, err := fullyResolvedRepo(r) 353 if err != nil { 354 log.Println("failed to get repo and knot", err) 355 return 356 } 357 358 pulls, err := db.GetPulls(s.db, f.RepoAt, state) 359 if err != nil { 360 log.Println("failed to get pulls", err) 361 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 362 return 363 } 364 365 for _, p := range pulls { 366 var pullSourceRepo *db.Repo 367 if p.PullSource != nil { 368 if p.PullSource.RepoAt != nil { 369 pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 370 if err != nil { 371 log.Printf("failed to get repo by at uri: %v", err) 372 return 373 } 374 } 375 } 376 377 p.PullSource.Repo = pullSourceRepo 378 } 379 380 identsToResolve := make([]string, len(pulls)) 381 for i, pull := range pulls { 382 identsToResolve[i] = pull.OwnerDid 383 } 384 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 385 didHandleMap := make(map[string]string) 386 for _, identity := range resolvedIds { 387 if !identity.Handle.IsInvalidHandle() { 388 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 389 } else { 390 didHandleMap[identity.DID.String()] = identity.DID.String() 391 } 392 } 393 394 s.pages.RepoPulls(w, pages.RepoPullsParams{ 395 LoggedInUser: s.auth.GetUser(r), 396 RepoInfo: f.RepoInfo(s, user), 397 Pulls: pulls, 398 DidHandleMap: didHandleMap, 399 FilteringBy: state, 400 }) 401 return 402} 403 404func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 405 user := s.auth.GetUser(r) 406 f, err := fullyResolvedRepo(r) 407 if err != nil { 408 log.Println("failed to get repo and knot", err) 409 return 410 } 411 412 pull, ok := r.Context().Value("pull").(*db.Pull) 413 if !ok { 414 log.Println("failed to get pull") 415 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 416 return 417 } 418 419 roundNumberStr := chi.URLParam(r, "round") 420 roundNumber, err := strconv.Atoi(roundNumberStr) 421 if err != nil || roundNumber >= len(pull.Submissions) { 422 http.Error(w, "bad round id", http.StatusBadRequest) 423 log.Println("failed to parse round id", err) 424 return 425 } 426 427 switch r.Method { 428 case http.MethodGet: 429 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 430 LoggedInUser: user, 431 RepoInfo: f.RepoInfo(s, user), 432 Pull: pull, 433 RoundNumber: roundNumber, 434 }) 435 return 436 case http.MethodPost: 437 body := r.FormValue("body") 438 if body == "" { 439 s.pages.Notice(w, "pull", "Comment body is required") 440 return 441 } 442 443 // Start a transaction 444 tx, err := s.db.BeginTx(r.Context(), nil) 445 if err != nil { 446 log.Println("failed to start transaction", err) 447 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 448 return 449 } 450 defer tx.Rollback() 451 452 createdAt := time.Now().Format(time.RFC3339) 453 ownerDid := user.Did 454 455 pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 456 if err != nil { 457 log.Println("failed to get pull at", err) 458 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 459 return 460 } 461 462 atUri := f.RepoAt.String() 463 client, _ := s.auth.AuthorizedClient(r) 464 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 465 Collection: tangled.RepoPullCommentNSID, 466 Repo: user.Did, 467 Rkey: s.TID(), 468 Record: &lexutil.LexiconTypeDecoder{ 469 Val: &tangled.RepoPullComment{ 470 Repo: &atUri, 471 Pull: pullAt, 472 Owner: &ownerDid, 473 Body: &body, 474 CreatedAt: &createdAt, 475 }, 476 }, 477 }) 478 if err != nil { 479 log.Println("failed to create pull comment", err) 480 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 481 return 482 } 483 484 // Create the pull comment in the database with the commentAt field 485 commentId, err := db.NewPullComment(tx, &db.PullComment{ 486 OwnerDid: user.Did, 487 RepoAt: f.RepoAt.String(), 488 PullId: pull.PullId, 489 Body: body, 490 CommentAt: atResp.Uri, 491 SubmissionId: pull.Submissions[roundNumber].ID, 492 }) 493 if err != nil { 494 log.Println("failed to create pull comment", err) 495 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 496 return 497 } 498 499 // Commit the transaction 500 if err = tx.Commit(); err != nil { 501 log.Println("failed to commit transaction", err) 502 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 503 return 504 } 505 506 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 507 return 508 } 509} 510 511func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 512 user := s.auth.GetUser(r) 513 f, err := fullyResolvedRepo(r) 514 if err != nil { 515 log.Println("failed to get repo and knot", err) 516 return 517 } 518 519 switch r.Method { 520 case http.MethodGet: 521 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 522 if err != nil { 523 log.Printf("failed to create unsigned client for %s", f.Knot) 524 s.pages.Error503(w) 525 return 526 } 527 528 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 529 if err != nil { 530 log.Println("failed to reach knotserver", err) 531 return 532 } 533 534 body, err := io.ReadAll(resp.Body) 535 if err != nil { 536 log.Printf("Error reading response body: %v", err) 537 return 538 } 539 540 var result types.RepoBranchesResponse 541 err = json.Unmarshal(body, &result) 542 if err != nil { 543 log.Println("failed to parse response:", err) 544 return 545 } 546 547 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 548 LoggedInUser: user, 549 RepoInfo: f.RepoInfo(s, user), 550 Branches: result.Branches, 551 }) 552 case http.MethodPost: 553 title := r.FormValue("title") 554 body := r.FormValue("body") 555 targetBranch := r.FormValue("targetBranch") 556 fromFork := r.FormValue("fork") 557 sourceBranch := r.FormValue("sourceBranch") 558 patch := r.FormValue("patch") 559 560 // Validate required fields for all PR types 561 if title == "" || body == "" || targetBranch == "" { 562 s.pages.Notice(w, "pull", "Title, body and target branch are required.") 563 return 564 } 565 566 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 567 if err != nil { 568 log.Println("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 resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 639 switch resp.StatusCode { 640 case 404: 641 case 400: 642 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 643 return 644 } 645 646 respBody, err := io.ReadAll(resp.Body) 647 if err != nil { 648 log.Println("failed to compare across branches") 649 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 650 return 651 } 652 defer resp.Body.Close() 653 654 var diffTreeResponse types.RepoDiffTreeResponse 655 err = json.Unmarshal(respBody, &diffTreeResponse) 656 if err != nil { 657 log.Println("failed to unmarshal diff tree response", err) 658 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 659 return 660 } 661 662 sourceRev := diffTreeResponse.DiffTree.Rev2 663 patch := diffTreeResponse.DiffTree.Patch 664 665 if !isPatchValid(patch) { 666 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 667 return 668 } 669 670 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 671} 672 673func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 674 if !isPatchValid(patch) { 675 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 676 return 677 } 678 679 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 680} 681 682func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 683 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 684 if errors.Is(err, sql.ErrNoRows) { 685 s.pages.Notice(w, "pull", "No such fork.") 686 return 687 } else if err != nil { 688 log.Println("failed to fetch fork:", err) 689 s.pages.Notice(w, "pull", "Failed to fetch fork.") 690 return 691 } 692 693 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 694 if err != nil { 695 log.Println("failed to fetch registration key:", err) 696 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 697 return 698 } 699 700 sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 701 if err != nil { 702 log.Println("failed to create signed client:", err) 703 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 704 return 705 } 706 707 us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 708 if err != nil { 709 log.Println("failed to create unsigned client:", err) 710 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 711 return 712 } 713 714 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 715 if err != nil { 716 log.Println("failed to create hidden ref:", err, resp.StatusCode) 717 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 718 return 719 } 720 721 switch resp.StatusCode { 722 case 404: 723 case 400: 724 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 725 return 726 } 727 728 hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 729 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 730 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 731 // hiddenRef: hidden/feature-1/main (on repo-fork) 732 // targetBranch: main (on repo-1) 733 // sourceBranch: feature-1 (on repo-fork) 734 diffResp, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 735 if err != nil { 736 log.Println("failed to compare across branches", err) 737 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 738 return 739 } 740 741 respBody, err := io.ReadAll(diffResp.Body) 742 if err != nil { 743 log.Println("failed to read response body", err) 744 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 745 return 746 } 747 748 defer resp.Body.Close() 749 750 var diffTreeResponse types.RepoDiffTreeResponse 751 err = json.Unmarshal(respBody, &diffTreeResponse) 752 if err != nil { 753 log.Println("failed to unmarshal diff tree response", err) 754 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 755 return 756 } 757 758 sourceRev := diffTreeResponse.DiffTree.Rev2 759 patch := diffTreeResponse.DiffTree.Patch 760 761 if !isPatchValid(patch) { 762 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 763 return 764 } 765 766 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 767 if err != nil { 768 log.Println("failed to parse fork AT URI", err) 769 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 770 return 771 } 772 773 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 774 Branch: sourceBranch, 775 RepoAt: &forkAtUri, 776 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) 777} 778 779func (s *State) createPullRequest(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch, sourceRev string, pullSource *db.PullSource, recordPullSource *tangled.RepoPull_Source) { 780 tx, err := s.db.BeginTx(r.Context(), nil) 781 if err != nil { 782 log.Println("failed to start tx") 783 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 784 return 785 } 786 defer tx.Rollback() 787 788 rkey := s.TID() 789 initialSubmission := db.PullSubmission{ 790 Patch: patch, 791 SourceRev: sourceRev, 792 } 793 err = db.NewPull(tx, &db.Pull{ 794 Title: title, 795 Body: body, 796 TargetBranch: targetBranch, 797 OwnerDid: user.Did, 798 RepoAt: f.RepoAt, 799 Rkey: rkey, 800 Submissions: []*db.PullSubmission{ 801 &initialSubmission, 802 }, 803 PullSource: pullSource, 804 }) 805 if err != nil { 806 log.Println("failed to create pull request", err) 807 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 808 return 809 } 810 client, _ := s.auth.AuthorizedClient(r) 811 pullId, err := db.NextPullId(s.db, f.RepoAt) 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 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 819 Collection: tangled.RepoPullNSID, 820 Repo: user.Did, 821 Rkey: rkey, 822 Record: &lexutil.LexiconTypeDecoder{ 823 Val: &tangled.RepoPull{ 824 Title: title, 825 PullId: int64(pullId), 826 TargetRepo: string(f.RepoAt), 827 TargetBranch: targetBranch, 828 Patch: patch, 829 Source: recordPullSource, 830 }, 831 }, 832 }) 833 834 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 835 if err != nil { 836 log.Println("failed to get pull id", err) 837 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 838 return 839 } 840 841 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 842} 843 844func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 845 user := s.auth.GetUser(r) 846 f, err := fullyResolvedRepo(r) 847 if err != nil { 848 log.Println("failed to get repo and knot", err) 849 return 850 } 851 852 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 853 RepoInfo: f.RepoInfo(s, user), 854 }) 855} 856 857func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 858 user := s.auth.GetUser(r) 859 f, err := fullyResolvedRepo(r) 860 if err != nil { 861 log.Println("failed to get repo and knot", err) 862 return 863 } 864 865 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 866 if err != nil { 867 log.Printf("failed to create unsigned client for %s", f.Knot) 868 s.pages.Error503(w) 869 return 870 } 871 872 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 873 if err != nil { 874 log.Println("failed to reach knotserver", err) 875 return 876 } 877 878 body, err := io.ReadAll(resp.Body) 879 if err != nil { 880 log.Printf("Error reading response body: %v", err) 881 return 882 } 883 884 var result types.RepoBranchesResponse 885 err = json.Unmarshal(body, &result) 886 if err != nil { 887 log.Println("failed to parse response:", err) 888 return 889 } 890 891 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 892 RepoInfo: f.RepoInfo(s, user), 893 Branches: result.Branches, 894 }) 895} 896 897func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 898 user := s.auth.GetUser(r) 899 f, err := fullyResolvedRepo(r) 900 if err != nil { 901 log.Println("failed to get repo and knot", err) 902 return 903 } 904 905 forks, err := db.GetForksByDid(s.db, user.Did) 906 if err != nil { 907 log.Println("failed to get forks", err) 908 return 909 } 910 911 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 912 RepoInfo: f.RepoInfo(s, user), 913 Forks: forks, 914 }) 915} 916 917func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 918 user := s.auth.GetUser(r) 919 920 f, err := fullyResolvedRepo(r) 921 if err != nil { 922 log.Println("failed to get repo and knot", err) 923 return 924 } 925 926 forkVal := r.URL.Query().Get("fork") 927 928 // fork repo 929 repo, err := db.GetRepo(s.db, user.Did, forkVal) 930 if err != nil { 931 log.Println("failed to get repo", user.Did, forkVal) 932 return 933 } 934 935 sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 936 if err != nil { 937 log.Printf("failed to create unsigned client for %s", repo.Knot) 938 s.pages.Error503(w) 939 return 940 } 941 942 sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name) 943 if err != nil { 944 log.Println("failed to reach knotserver for source branches", err) 945 return 946 } 947 948 sourceBody, err := io.ReadAll(sourceResp.Body) 949 if err != nil { 950 log.Println("failed to read source response body", err) 951 return 952 } 953 defer sourceResp.Body.Close() 954 955 var sourceResult types.RepoBranchesResponse 956 err = json.Unmarshal(sourceBody, &sourceResult) 957 if err != nil { 958 log.Println("failed to parse source branches response:", err) 959 return 960 } 961 962 targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 963 if err != nil { 964 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 965 s.pages.Error503(w) 966 return 967 } 968 969 targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 970 if err != nil { 971 log.Println("failed to reach knotserver for target branches", err) 972 return 973 } 974 975 targetBody, err := io.ReadAll(targetResp.Body) 976 if err != nil { 977 log.Println("failed to read target response body", err) 978 return 979 } 980 defer targetResp.Body.Close() 981 982 var targetResult types.RepoBranchesResponse 983 err = json.Unmarshal(targetBody, &targetResult) 984 if err != nil { 985 log.Println("failed to parse target branches response:", err) 986 return 987 } 988 989 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 990 RepoInfo: f.RepoInfo(s, user), 991 SourceBranches: sourceResult.Branches, 992 TargetBranches: targetResult.Branches, 993 }) 994} 995 996func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 997 user := s.auth.GetUser(r) 998 f, err := fullyResolvedRepo(r) 999 if err != nil { 1000 log.Println("failed to get repo and knot", err) 1001 return 1002 } 1003 1004 pull, ok := r.Context().Value("pull").(*db.Pull) 1005 if !ok { 1006 log.Println("failed to get pull") 1007 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1008 return 1009 } 1010 1011 switch r.Method { 1012 case http.MethodGet: 1013 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1014 RepoInfo: f.RepoInfo(s, user), 1015 Pull: pull, 1016 }) 1017 return 1018 case http.MethodPost: 1019 patch := r.FormValue("patch") 1020 var sourceRev string 1021 var recordPullSource *tangled.RepoPull_Source 1022 1023 var ownerDid, repoName, knotName string 1024 var isSameRepo bool = pull.IsSameRepoBranch() 1025 sourceBranch := pull.PullSource.Branch 1026 targetBranch := pull.TargetBranch 1027 recordPullSource = &tangled.RepoPull_Source{ 1028 Branch: sourceBranch, 1029 } 1030 1031 isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 1032 if isSameRepo && isPushAllowed { 1033 ownerDid = f.OwnerDid() 1034 repoName = f.RepoName 1035 knotName = f.Knot 1036 } else if !isSameRepo { 1037 sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1038 if err != nil { 1039 log.Println("failed to get source repo", err) 1040 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1041 return 1042 } 1043 ownerDid = sourceRepo.Did 1044 repoName = sourceRepo.Name 1045 knotName = sourceRepo.Knot 1046 } 1047 1048 if sourceBranch != "" && knotName != "" { 1049 // extract patch by performing compare 1050 ksClient, err := NewUnsignedClient(knotName, s.config.Dev) 1051 if err != nil { 1052 log.Printf("failed to create client for %s: %s", knotName, err) 1053 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1054 return 1055 } 1056 1057 if !isSameRepo { 1058 secret, err := db.GetRegistrationKey(s.db, knotName) 1059 if err != nil { 1060 log.Printf("failed to get registration key for %s: %s", knotName, err) 1061 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1062 return 1063 } 1064 // update the hidden tracking branch to latest 1065 signedClient, err := NewSignedClient(knotName, secret, s.config.Dev) 1066 if err != nil { 1067 log.Printf("failed to create signed client for %s: %s", knotName, err) 1068 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1069 return 1070 } 1071 resp, err := signedClient.NewHiddenRef(ownerDid, repoName, sourceBranch, targetBranch) 1072 if err != nil || resp.StatusCode != http.StatusNoContent { 1073 log.Printf("failed to update tracking branch: %s", err) 1074 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1075 return 1076 } 1077 } 1078 1079 var compareResp *http.Response 1080 if !isSameRepo { 1081 hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 1082 compareResp, err = ksClient.Compare(ownerDid, repoName, hiddenRef, sourceBranch) 1083 } else { 1084 compareResp, err = ksClient.Compare(ownerDid, repoName, targetBranch, sourceBranch) 1085 } 1086 if err != nil { 1087 log.Printf("failed to compare branches: %s", err) 1088 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1089 return 1090 } 1091 defer compareResp.Body.Close() 1092 1093 switch compareResp.StatusCode { 1094 case 404: 1095 case 400: 1096 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 1097 return 1098 } 1099 1100 respBody, err := io.ReadAll(compareResp.Body) 1101 if err != nil { 1102 log.Println("failed to compare across branches") 1103 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1104 return 1105 } 1106 defer compareResp.Body.Close() 1107 1108 var diffTreeResponse types.RepoDiffTreeResponse 1109 err = json.Unmarshal(respBody, &diffTreeResponse) 1110 if err != nil { 1111 log.Println("failed to unmarshal diff tree response", err) 1112 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1113 return 1114 } 1115 1116 sourceRev = diffTreeResponse.DiffTree.Rev2 1117 patch = diffTreeResponse.DiffTree.Patch 1118 } 1119 1120 if patch == "" { 1121 s.pages.Notice(w, "resubmit-error", "Patch is empty.") 1122 return 1123 } 1124 1125 if patch == pull.LatestPatch() { 1126 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 1127 return 1128 } 1129 1130 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1131 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1132 return 1133 } 1134 1135 if !isPatchValid(patch) { 1136 s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") 1137 return 1138 } 1139 1140 tx, err := s.db.BeginTx(r.Context(), nil) 1141 if err != nil { 1142 log.Println("failed to start tx") 1143 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1144 return 1145 } 1146 defer tx.Rollback() 1147 1148 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1149 if err != nil { 1150 log.Println("failed to create pull request", err) 1151 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1152 return 1153 } 1154 client, _ := s.auth.AuthorizedClient(r) 1155 1156 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1157 if err != nil { 1158 // failed to get record 1159 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1160 return 1161 } 1162 1163 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1164 Collection: tangled.RepoPullNSID, 1165 Repo: user.Did, 1166 Rkey: pull.Rkey, 1167 SwapRecord: ex.Cid, 1168 Record: &lexutil.LexiconTypeDecoder{ 1169 Val: &tangled.RepoPull{ 1170 Title: pull.Title, 1171 PullId: int64(pull.PullId), 1172 TargetRepo: string(f.RepoAt), 1173 TargetBranch: pull.TargetBranch, 1174 Patch: patch, // new patch 1175 Source: recordPullSource, 1176 }, 1177 }, 1178 }) 1179 if err != nil { 1180 log.Println("failed to update record", err) 1181 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1182 return 1183 } 1184 1185 if err = tx.Commit(); err != nil { 1186 log.Println("failed to commit transaction", err) 1187 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1188 return 1189 } 1190 1191 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1192 return 1193 } 1194} 1195 1196func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1197 f, err := fullyResolvedRepo(r) 1198 if err != nil { 1199 log.Println("failed to resolve repo:", err) 1200 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1201 return 1202 } 1203 1204 pull, ok := r.Context().Value("pull").(*db.Pull) 1205 if !ok { 1206 log.Println("failed to get pull") 1207 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1208 return 1209 } 1210 1211 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1212 if err != nil { 1213 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1214 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1215 return 1216 } 1217 1218 ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid) 1219 if err != nil { 1220 log.Printf("resolving identity: %s", err) 1221 w.WriteHeader(http.StatusNotFound) 1222 return 1223 } 1224 1225 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1226 if err != nil { 1227 log.Printf("failed to get primary email: %s", err) 1228 } 1229 1230 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1231 if err != nil { 1232 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1233 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1234 return 1235 } 1236 1237 // Merge the pull request 1238 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1239 if err != nil { 1240 log.Printf("failed to merge pull request: %s", err) 1241 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1242 return 1243 } 1244 1245 if resp.StatusCode == http.StatusOK { 1246 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1247 if err != nil { 1248 log.Printf("failed to update pull request status in database: %s", err) 1249 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1250 return 1251 } 1252 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1253 } else { 1254 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1255 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1256 } 1257} 1258 1259func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1260 user := s.auth.GetUser(r) 1261 1262 f, err := fullyResolvedRepo(r) 1263 if err != nil { 1264 log.Println("malformed middleware") 1265 return 1266 } 1267 1268 pull, ok := r.Context().Value("pull").(*db.Pull) 1269 if !ok { 1270 log.Println("failed to get pull") 1271 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1272 return 1273 } 1274 1275 // auth filter: only owner or collaborators can close 1276 roles := RolesInRepo(s, user, f) 1277 isCollaborator := roles.IsCollaborator() 1278 isPullAuthor := user.Did == pull.OwnerDid 1279 isCloseAllowed := isCollaborator || isPullAuthor 1280 if !isCloseAllowed { 1281 log.Println("failed to close pull") 1282 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1283 return 1284 } 1285 1286 // Start a transaction 1287 tx, err := s.db.BeginTx(r.Context(), nil) 1288 if err != nil { 1289 log.Println("failed to start transaction", err) 1290 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1291 return 1292 } 1293 1294 // Close the pull in the database 1295 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 1296 if err != nil { 1297 log.Println("failed to close pull", err) 1298 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1299 return 1300 } 1301 1302 // Commit the transaction 1303 if err = tx.Commit(); err != nil { 1304 log.Println("failed to commit transaction", err) 1305 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1306 return 1307 } 1308 1309 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1310 return 1311} 1312 1313func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1314 user := s.auth.GetUser(r) 1315 1316 f, err := fullyResolvedRepo(r) 1317 if err != nil { 1318 log.Println("failed to resolve repo", err) 1319 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1320 return 1321 } 1322 1323 pull, ok := r.Context().Value("pull").(*db.Pull) 1324 if !ok { 1325 log.Println("failed to get pull") 1326 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1327 return 1328 } 1329 1330 // auth filter: only owner or collaborators can close 1331 roles := RolesInRepo(s, user, f) 1332 isCollaborator := roles.IsCollaborator() 1333 isPullAuthor := user.Did == pull.OwnerDid 1334 isCloseAllowed := isCollaborator || isPullAuthor 1335 if !isCloseAllowed { 1336 log.Println("failed to close pull") 1337 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1338 return 1339 } 1340 1341 // Start a transaction 1342 tx, err := s.db.BeginTx(r.Context(), nil) 1343 if err != nil { 1344 log.Println("failed to start transaction", err) 1345 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1346 return 1347 } 1348 1349 // Reopen the pull in the database 1350 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 1351 if err != nil { 1352 log.Println("failed to reopen pull", err) 1353 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1354 return 1355 } 1356 1357 // Commit the transaction 1358 if err = tx.Commit(); err != nil { 1359 log.Println("failed to commit transaction", err) 1360 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1361 return 1362 } 1363 1364 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1365 return 1366} 1367 1368// Very basic validation to check if it looks like a diff/patch 1369// A valid patch usually starts with diff or --- lines 1370func isPatchValid(patch string) bool { 1371 // Basic validation to check if it looks like a diff/patch 1372 // A valid patch usually starts with diff or --- lines 1373 if len(patch) == 0 { 1374 return false 1375 } 1376 1377 lines := strings.Split(patch, "\n") 1378 if len(lines) < 2 { 1379 return false 1380 } 1381 1382 // Check for common patch format markers 1383 firstLine := strings.TrimSpace(lines[0]) 1384 return strings.HasPrefix(firstLine, "diff ") || 1385 strings.HasPrefix(firstLine, "--- ") || 1386 strings.HasPrefix(firstLine, "Index: ") || 1387 strings.HasPrefix(firstLine, "+++ ") || 1388 strings.HasPrefix(firstLine, "@@ ") 1389}