forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at pr-actions 22 kB view raw
1package state 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "log" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/go-chi/chi/v5" 14 "github.com/sotangled/tangled/api/tangled" 15 "github.com/sotangled/tangled/appview/db" 16 "github.com/sotangled/tangled/appview/pages" 17 "github.com/sotangled/tangled/types" 18 19 comatproto "github.com/bluesky-social/indigo/api/atproto" 20 lexutil "github.com/bluesky-social/indigo/lex/util" 21) 22 23// htmx fragment 24func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 25 switch r.Method { 26 case http.MethodGet: 27 user := s.auth.GetUser(r) 28 f, err := fullyResolvedRepo(r) 29 if err != nil { 30 log.Println("failed to get repo and knot", err) 31 return 32 } 33 34 pull, ok := r.Context().Value("pull").(*db.Pull) 35 if !ok { 36 log.Println("failed to get pull") 37 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 38 return 39 } 40 41 roundNumberStr := chi.URLParam(r, "round") 42 roundNumber, err := strconv.Atoi(roundNumberStr) 43 if err != nil { 44 roundNumber = pull.LastRoundNumber() 45 } 46 if roundNumber >= len(pull.Submissions) { 47 http.Error(w, "bad round id", http.StatusBadRequest) 48 log.Println("failed to parse round id", err) 49 return 50 } 51 52 mergeCheckResponse := s.mergeCheck(f, pull) 53 54 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 55 LoggedInUser: user, 56 RepoInfo: f.RepoInfo(s, user), 57 Pull: pull, 58 RoundNumber: roundNumber, 59 MergeCheck: mergeCheckResponse, 60 }) 61 return 62 } 63} 64 65func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 66 user := s.auth.GetUser(r) 67 f, err := fullyResolvedRepo(r) 68 if err != nil { 69 log.Println("failed to get repo and knot", err) 70 return 71 } 72 73 pull, ok := r.Context().Value("pull").(*db.Pull) 74 if !ok { 75 log.Println("failed to get pull") 76 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 77 return 78 } 79 80 totalIdents := 1 81 for _, submission := range pull.Submissions { 82 totalIdents += len(submission.Comments) 83 } 84 85 identsToResolve := make([]string, totalIdents) 86 87 // populate idents 88 identsToResolve[0] = pull.OwnerDid 89 idx := 1 90 for _, submission := range pull.Submissions { 91 for _, comment := range submission.Comments { 92 identsToResolve[idx] = comment.OwnerDid 93 idx += 1 94 } 95 } 96 97 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 98 didHandleMap := make(map[string]string) 99 for _, identity := range resolvedIds { 100 if !identity.Handle.IsInvalidHandle() { 101 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 102 } else { 103 didHandleMap[identity.DID.String()] = identity.DID.String() 104 } 105 } 106 107 mergeCheckResponse := s.mergeCheck(f, pull) 108 109 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 110 LoggedInUser: user, 111 RepoInfo: f.RepoInfo(s, user), 112 DidHandleMap: didHandleMap, 113 Pull: *pull, 114 MergeCheck: mergeCheckResponse, 115 }) 116} 117 118func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse { 119 if pull.State == db.PullMerged { 120 return types.MergeCheckResponse{} 121 } 122 123 secret, err := db.GetRegistrationKey(s.db, f.Knot) 124 if err != nil { 125 log.Printf("failed to get registration key: %w", err) 126 return types.MergeCheckResponse{ 127 Error: "failed to check merge status: this knot is unregistered", 128 } 129 } 130 131 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 132 if err != nil { 133 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 134 return types.MergeCheckResponse{ 135 Error: "failed to check merge status", 136 } 137 } 138 139 resp, err := ksClient.MergeCheck([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch) 140 if err != nil { 141 log.Println("failed to check for mergeability:", err) 142 return types.MergeCheckResponse{ 143 Error: "failed to check merge status", 144 } 145 } 146 switch resp.StatusCode { 147 case 404: 148 return types.MergeCheckResponse{ 149 Error: "failed to check merge status: this knot does not support PRs", 150 } 151 case 400: 152 return types.MergeCheckResponse{ 153 Error: "failed to check merge status: does this knot support PRs?", 154 } 155 } 156 157 respBody, err := io.ReadAll(resp.Body) 158 if err != nil { 159 log.Println("failed to read merge check response body") 160 return types.MergeCheckResponse{ 161 Error: "failed to check merge status: knot is not speaking the right language", 162 } 163 } 164 defer resp.Body.Close() 165 166 var mergeCheckResponse types.MergeCheckResponse 167 err = json.Unmarshal(respBody, &mergeCheckResponse) 168 if err != nil { 169 log.Println("failed to unmarshal merge check response", err) 170 return types.MergeCheckResponse{ 171 Error: "failed to check merge status: knot is not speaking the right language", 172 } 173 } 174 175 return mergeCheckResponse 176} 177 178func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 179 user := s.auth.GetUser(r) 180 f, err := fullyResolvedRepo(r) 181 if err != nil { 182 log.Println("failed to get repo and knot", err) 183 return 184 } 185 186 pull, ok := r.Context().Value("pull").(*db.Pull) 187 if !ok { 188 log.Println("failed to get pull") 189 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 190 return 191 } 192 193 roundId := chi.URLParam(r, "round") 194 roundIdInt, err := strconv.Atoi(roundId) 195 if err != nil || roundIdInt >= len(pull.Submissions) { 196 http.Error(w, "bad round id", http.StatusBadRequest) 197 log.Println("failed to parse round id", err) 198 return 199 } 200 201 identsToResolve := []string{pull.OwnerDid} 202 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 203 didHandleMap := make(map[string]string) 204 for _, identity := range resolvedIds { 205 if !identity.Handle.IsInvalidHandle() { 206 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 207 } else { 208 didHandleMap[identity.DID.String()] = identity.DID.String() 209 } 210 } 211 212 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 213 LoggedInUser: user, 214 DidHandleMap: didHandleMap, 215 RepoInfo: f.RepoInfo(s, user), 216 Pull: pull, 217 Round: roundIdInt, 218 Submission: pull.Submissions[roundIdInt], 219 Diff: pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch), 220 }) 221 222} 223 224func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 225 user := s.auth.GetUser(r) 226 params := r.URL.Query() 227 228 state := db.PullOpen 229 switch params.Get("state") { 230 case "closed": 231 state = db.PullClosed 232 case "merged": 233 state = db.PullMerged 234 } 235 236 f, err := fullyResolvedRepo(r) 237 if err != nil { 238 log.Println("failed to get repo and knot", err) 239 return 240 } 241 242 pulls, err := db.GetPulls(s.db, f.RepoAt, state) 243 if err != nil { 244 log.Println("failed to get pulls", err) 245 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 246 return 247 } 248 249 identsToResolve := make([]string, len(pulls)) 250 for i, pull := range pulls { 251 identsToResolve[i] = pull.OwnerDid 252 } 253 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 254 didHandleMap := make(map[string]string) 255 for _, identity := range resolvedIds { 256 if !identity.Handle.IsInvalidHandle() { 257 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 258 } else { 259 didHandleMap[identity.DID.String()] = identity.DID.String() 260 } 261 } 262 263 s.pages.RepoPulls(w, pages.RepoPullsParams{ 264 LoggedInUser: s.auth.GetUser(r), 265 RepoInfo: f.RepoInfo(s, user), 266 Pulls: pulls, 267 DidHandleMap: didHandleMap, 268 FilteringBy: state, 269 }) 270 return 271} 272 273func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 274 user := s.auth.GetUser(r) 275 f, err := fullyResolvedRepo(r) 276 if err != nil { 277 log.Println("failed to get repo and knot", err) 278 return 279 } 280 281 pull, ok := r.Context().Value("pull").(*db.Pull) 282 if !ok { 283 log.Println("failed to get pull") 284 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 285 return 286 } 287 288 roundNumberStr := chi.URLParam(r, "round") 289 roundNumber, err := strconv.Atoi(roundNumberStr) 290 if err != nil || roundNumber >= len(pull.Submissions) { 291 http.Error(w, "bad round id", http.StatusBadRequest) 292 log.Println("failed to parse round id", err) 293 return 294 } 295 296 switch r.Method { 297 case http.MethodGet: 298 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 299 LoggedInUser: user, 300 RepoInfo: f.RepoInfo(s, user), 301 Pull: pull, 302 RoundNumber: roundNumber, 303 }) 304 return 305 case http.MethodPost: 306 body := r.FormValue("body") 307 if body == "" { 308 s.pages.Notice(w, "pull", "Comment body is required") 309 return 310 } 311 312 // Start a transaction 313 tx, err := s.db.BeginTx(r.Context(), nil) 314 if err != nil { 315 log.Println("failed to start transaction", err) 316 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 317 return 318 } 319 defer tx.Rollback() 320 321 createdAt := time.Now().Format(time.RFC3339) 322 ownerDid := user.Did 323 324 pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 325 if err != nil { 326 log.Println("failed to get pull at", err) 327 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 328 return 329 } 330 331 atUri := f.RepoAt.String() 332 client, _ := s.auth.AuthorizedClient(r) 333 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 334 Collection: tangled.RepoPullCommentNSID, 335 Repo: user.Did, 336 Rkey: s.TID(), 337 Record: &lexutil.LexiconTypeDecoder{ 338 Val: &tangled.RepoPullComment{ 339 Repo: &atUri, 340 Pull: pullAt, 341 Owner: &ownerDid, 342 Body: &body, 343 CreatedAt: &createdAt, 344 }, 345 }, 346 }) 347 log.Println(atResp.Uri) 348 if err != nil { 349 log.Println("failed to create pull comment", err) 350 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 351 return 352 } 353 354 // Create the pull comment in the database with the commentAt field 355 commentId, err := db.NewPullComment(tx, &db.PullComment{ 356 OwnerDid: user.Did, 357 RepoAt: f.RepoAt.String(), 358 PullId: pull.PullId, 359 Body: body, 360 CommentAt: atResp.Uri, 361 SubmissionId: pull.Submissions[roundNumber].ID, 362 }) 363 if err != nil { 364 log.Println("failed to create pull comment", err) 365 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 366 return 367 } 368 369 // Commit the transaction 370 if err = tx.Commit(); err != nil { 371 log.Println("failed to commit transaction", err) 372 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 373 return 374 } 375 376 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 377 return 378 } 379} 380 381func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 382 user := s.auth.GetUser(r) 383 f, err := fullyResolvedRepo(r) 384 if err != nil { 385 log.Println("failed to get repo and knot", err) 386 return 387 } 388 389 switch r.Method { 390 case http.MethodGet: 391 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 392 if err != nil { 393 log.Printf("failed to create unsigned client for %s", f.Knot) 394 s.pages.Error503(w) 395 return 396 } 397 398 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 399 if err != nil { 400 log.Println("failed to reach knotserver", err) 401 return 402 } 403 404 body, err := io.ReadAll(resp.Body) 405 if err != nil { 406 log.Printf("Error reading response body: %v", err) 407 return 408 } 409 410 var result types.RepoBranchesResponse 411 err = json.Unmarshal(body, &result) 412 if err != nil { 413 log.Println("failed to parse response:", err) 414 return 415 } 416 417 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 418 LoggedInUser: user, 419 RepoInfo: f.RepoInfo(s, user), 420 Branches: result.Branches, 421 }) 422 case http.MethodPost: 423 title := r.FormValue("title") 424 body := r.FormValue("body") 425 targetBranch := r.FormValue("targetBranch") 426 patch := r.FormValue("patch") 427 428 if title == "" || body == "" || patch == "" || targetBranch == "" { 429 s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 430 return 431 } 432 433 // Validate patch format 434 if !isPatchValid(patch) { 435 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 436 return 437 } 438 439 tx, err := s.db.BeginTx(r.Context(), nil) 440 if err != nil { 441 log.Println("failed to start tx") 442 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 443 return 444 } 445 defer tx.Rollback() 446 447 rkey := s.TID() 448 initialSubmission := db.PullSubmission{ 449 Patch: patch, 450 } 451 err = db.NewPull(tx, &db.Pull{ 452 Title: title, 453 Body: body, 454 TargetBranch: targetBranch, 455 OwnerDid: user.Did, 456 RepoAt: f.RepoAt, 457 Rkey: rkey, 458 Submissions: []*db.PullSubmission{ 459 &initialSubmission, 460 }, 461 }) 462 if err != nil { 463 log.Println("failed to create pull request", err) 464 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 465 return 466 } 467 client, _ := s.auth.AuthorizedClient(r) 468 pullId, err := db.NextPullId(s.db, f.RepoAt) 469 if err != nil { 470 log.Println("failed to get pull id", err) 471 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 472 return 473 } 474 475 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 476 Collection: tangled.RepoPullNSID, 477 Repo: user.Did, 478 Rkey: rkey, 479 Record: &lexutil.LexiconTypeDecoder{ 480 Val: &tangled.RepoPull{ 481 Title: title, 482 PullId: int64(pullId), 483 TargetRepo: string(f.RepoAt), 484 TargetBranch: targetBranch, 485 Patch: patch, 486 }, 487 }, 488 }) 489 490 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 491 if err != nil { 492 log.Println("failed to get pull id", err) 493 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 494 return 495 } 496 497 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 498 return 499 } 500} 501 502func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 503 user := s.auth.GetUser(r) 504 f, err := fullyResolvedRepo(r) 505 if err != nil { 506 log.Println("failed to get repo and knot", err) 507 return 508 } 509 510 pull, ok := r.Context().Value("pull").(*db.Pull) 511 if !ok { 512 log.Println("failed to get pull") 513 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 514 return 515 } 516 517 switch r.Method { 518 case http.MethodGet: 519 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 520 RepoInfo: f.RepoInfo(s, user), 521 Pull: pull, 522 }) 523 return 524 case http.MethodPost: 525 patch := r.FormValue("patch") 526 527 if patch == "" { 528 s.pages.Notice(w, "resubmit-error", "Patch is empty.") 529 return 530 } 531 532 if patch == pull.LatestPatch() { 533 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 534 return 535 } 536 537 // Validate patch format 538 if !isPatchValid(patch) { 539 s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") 540 return 541 } 542 543 tx, err := s.db.BeginTx(r.Context(), nil) 544 if err != nil { 545 log.Println("failed to start tx") 546 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 547 return 548 } 549 defer tx.Rollback() 550 551 err = db.ResubmitPull(tx, pull, patch) 552 if err != nil { 553 log.Println("failed to create pull request", err) 554 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 555 return 556 } 557 client, _ := s.auth.AuthorizedClient(r) 558 559 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 560 if err != nil { 561 // failed to get record 562 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 563 return 564 } 565 566 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 567 Collection: tangled.RepoPullNSID, 568 Repo: user.Did, 569 Rkey: pull.Rkey, 570 SwapRecord: ex.Cid, 571 Record: &lexutil.LexiconTypeDecoder{ 572 Val: &tangled.RepoPull{ 573 Title: pull.Title, 574 PullId: int64(pull.PullId), 575 TargetRepo: string(f.RepoAt), 576 TargetBranch: pull.TargetBranch, 577 Patch: patch, // new patch 578 }, 579 }, 580 }) 581 if err != nil { 582 log.Println("failed to update record", err) 583 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 584 return 585 } 586 587 if err = tx.Commit(); err != nil { 588 log.Println("failed to commit transaction", err) 589 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 590 return 591 } 592 593 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 594 return 595 } 596} 597 598func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 599 f, err := fullyResolvedRepo(r) 600 if err != nil { 601 log.Println("failed to resolve repo:", err) 602 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 603 return 604 } 605 606 pull, ok := r.Context().Value("pull").(*db.Pull) 607 if !ok { 608 log.Println("failed to get pull") 609 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 610 return 611 } 612 613 secret, err := db.GetRegistrationKey(s.db, f.Knot) 614 if err != nil { 615 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 616 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 617 return 618 } 619 620 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 621 if err != nil { 622 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 623 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 624 return 625 } 626 627 // Merge the pull request 628 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, "", "") 629 if err != nil { 630 log.Printf("failed to merge pull request: %s", err) 631 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 632 return 633 } 634 635 if resp.StatusCode == http.StatusOK { 636 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 637 if err != nil { 638 log.Printf("failed to update pull request status in database: %s", err) 639 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 640 return 641 } 642 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 643 } else { 644 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 645 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 646 } 647} 648 649func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 650 user := s.auth.GetUser(r) 651 652 f, err := fullyResolvedRepo(r) 653 if err != nil { 654 log.Println("malformed middleware") 655 return 656 } 657 658 pull, ok := r.Context().Value("pull").(*db.Pull) 659 if !ok { 660 log.Println("failed to get pull") 661 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 662 return 663 } 664 665 // auth filter: only owner or collaborators can close 666 roles := RolesInRepo(s, user, f) 667 isCollaborator := roles.IsCollaborator() 668 isPullAuthor := user.Did == pull.OwnerDid 669 isCloseAllowed := isCollaborator || isPullAuthor 670 if !isCloseAllowed { 671 log.Println("failed to close pull") 672 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 673 return 674 } 675 676 // Start a transaction 677 tx, err := s.db.BeginTx(r.Context(), nil) 678 if err != nil { 679 log.Println("failed to start transaction", err) 680 s.pages.Notice(w, "pull-close", "Failed to close pull.") 681 return 682 } 683 684 // Close the pull in the database 685 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 686 if err != nil { 687 log.Println("failed to close pull", err) 688 s.pages.Notice(w, "pull-close", "Failed to close pull.") 689 return 690 } 691 692 // Commit the transaction 693 if err = tx.Commit(); err != nil { 694 log.Println("failed to commit transaction", err) 695 s.pages.Notice(w, "pull-close", "Failed to close pull.") 696 return 697 } 698 699 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 700 return 701} 702 703func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 704 user := s.auth.GetUser(r) 705 706 f, err := fullyResolvedRepo(r) 707 if err != nil { 708 log.Println("failed to resolve repo", err) 709 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 710 return 711 } 712 713 pull, ok := r.Context().Value("pull").(*db.Pull) 714 if !ok { 715 log.Println("failed to get pull") 716 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 717 return 718 } 719 720 // auth filter: only owner or collaborators can close 721 roles := RolesInRepo(s, user, f) 722 isCollaborator := roles.IsCollaborator() 723 isPullAuthor := user.Did == pull.OwnerDid 724 isCloseAllowed := isCollaborator || isPullAuthor 725 if !isCloseAllowed { 726 log.Println("failed to close pull") 727 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 728 return 729 } 730 731 // Start a transaction 732 tx, err := s.db.BeginTx(r.Context(), nil) 733 if err != nil { 734 log.Println("failed to start transaction", err) 735 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 736 return 737 } 738 739 // Reopen the pull in the database 740 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 741 if err != nil { 742 log.Println("failed to reopen pull", err) 743 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 744 return 745 } 746 747 // Commit the transaction 748 if err = tx.Commit(); err != nil { 749 log.Println("failed to commit transaction", err) 750 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 751 return 752 } 753 754 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 755 return 756} 757 758// Very basic validation to check if it looks like a diff/patch 759// A valid patch usually starts with diff or --- lines 760func isPatchValid(patch string) bool { 761 // Basic validation to check if it looks like a diff/patch 762 // A valid patch usually starts with diff or --- lines 763 if len(patch) == 0 { 764 return false 765 } 766 767 lines := strings.Split(patch, "\n") 768 if len(lines) < 2 { 769 return false 770 } 771 772 // Check for common patch format markers 773 firstLine := strings.TrimSpace(lines[0]) 774 return strings.HasPrefix(firstLine, "diff ") || 775 strings.HasPrefix(firstLine, "--- ") || 776 strings.HasPrefix(firstLine, "Index: ") || 777 strings.HasPrefix(firstLine, "+++ ") || 778 strings.HasPrefix(firstLine, "@@ ") 779}