forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state 2 3import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "log" 11 "net/http" 12 "strconv" 13 "time" 14 15 "go.opentelemetry.io/otel/attribute" 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/appview" 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/patchutil" 22 "tangled.sh/tangled.sh/core/telemetry" 23 "tangled.sh/tangled.sh/core/types" 24 25 comatproto "github.com/bluesky-social/indigo/api/atproto" 26 "github.com/bluesky-social/indigo/atproto/syntax" 27 lexutil "github.com/bluesky-social/indigo/lex/util" 28 "github.com/go-chi/chi/v5" 29) 30 31// htmx fragment 32func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 33 ctx, span := s.t.TraceStart(r.Context(), "PullActions") 34 defer span.End() 35 36 switch r.Method { 37 case http.MethodGet: 38 user := s.auth.GetUser(r) 39 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 40 if err != nil { 41 log.Println("failed to get repo and knot", err) 42 return 43 } 44 45 pull, ok := ctx.Value("pull").(*db.Pull) 46 if !ok { 47 log.Println("failed to get pull") 48 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 49 return 50 } 51 52 roundNumberStr := chi.URLParam(r, "round") 53 roundNumber, err := strconv.Atoi(roundNumberStr) 54 if err != nil { 55 roundNumber = pull.LastRoundNumber() 56 } 57 if roundNumber >= len(pull.Submissions) { 58 http.Error(w, "bad round id", http.StatusBadRequest) 59 log.Println("failed to parse round id", err) 60 return 61 } 62 63 _, mergeSpan := s.t.TraceStart(ctx, "mergeCheck") 64 mergeCheckResponse := s.mergeCheck(ctx, f, pull) 65 mergeSpan.End() 66 67 resubmitResult := pages.Unknown 68 if user.Did == pull.OwnerDid { 69 _, resubmitSpan := s.t.TraceStart(ctx, "resubmitCheck") 70 resubmitResult = s.resubmitCheck(ctx, f, pull) 71 resubmitSpan.End() 72 } 73 74 _, renderSpan := s.t.TraceStart(ctx, "renderPullActions") 75 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 76 LoggedInUser: user, 77 RepoInfo: f.RepoInfo(ctx, s, user), 78 Pull: pull, 79 RoundNumber: roundNumber, 80 MergeCheck: mergeCheckResponse, 81 ResubmitCheck: resubmitResult, 82 }) 83 renderSpan.End() 84 return 85 } 86} 87 88func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 89 ctx, span := s.t.TraceStart(r.Context(), "RepoSinglePull") 90 defer span.End() 91 92 user := s.auth.GetUser(r) 93 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 94 if err != nil { 95 log.Println("failed to get repo and knot", err) 96 span.RecordError(err) 97 return 98 } 99 100 pull, ok := ctx.Value("pull").(*db.Pull) 101 if !ok { 102 err := errors.New("failed to get pull from context") 103 log.Println(err) 104 span.RecordError(err) 105 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 106 return 107 } 108 109 attrs := telemetry.MapAttrs[string](map[string]string{ 110 "pull.id": fmt.Sprintf("%d", pull.PullId), 111 "pull.owner": pull.OwnerDid, 112 }) 113 114 span.SetAttributes(attrs...) 115 116 totalIdents := 1 117 for _, submission := range pull.Submissions { 118 totalIdents += len(submission.Comments) 119 } 120 121 identsToResolve := make([]string, totalIdents) 122 123 // populate idents 124 identsToResolve[0] = pull.OwnerDid 125 idx := 1 126 for _, submission := range pull.Submissions { 127 for _, comment := range submission.Comments { 128 identsToResolve[idx] = comment.OwnerDid 129 idx += 1 130 } 131 } 132 133 resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 134 didHandleMap := make(map[string]string) 135 for _, identity := range resolvedIds { 136 if !identity.Handle.IsInvalidHandle() { 137 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 138 } else { 139 didHandleMap[identity.DID.String()] = identity.DID.String() 140 } 141 } 142 span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds))) 143 144 mergeCheckResponse := s.mergeCheck(ctx, f, pull) 145 146 resubmitResult := pages.Unknown 147 if user != nil && user.Did == pull.OwnerDid { 148 resubmitResult = s.resubmitCheck(ctx, f, pull) 149 } 150 151 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 152 LoggedInUser: user, 153 RepoInfo: f.RepoInfo(ctx, s, user), 154 DidHandleMap: didHandleMap, 155 Pull: pull, 156 MergeCheck: mergeCheckResponse, 157 ResubmitCheck: resubmitResult, 158 }) 159} 160 161func (s *State) mergeCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse { 162 if pull.State == db.PullMerged { 163 return types.MergeCheckResponse{} 164 } 165 166 secret, err := db.GetRegistrationKey(s.db, f.Knot) 167 if err != nil { 168 log.Printf("failed to get registration key: %v", err) 169 return types.MergeCheckResponse{ 170 Error: "failed to check merge status: this knot is unregistered", 171 } 172 } 173 174 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 175 if err != nil { 176 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 177 return types.MergeCheckResponse{ 178 Error: "failed to check merge status", 179 } 180 } 181 182 resp, err := ksClient.MergeCheck([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch) 183 if err != nil { 184 log.Println("failed to check for mergeability:", err) 185 return types.MergeCheckResponse{ 186 Error: "failed to check merge status", 187 } 188 } 189 switch resp.StatusCode { 190 case 404: 191 return types.MergeCheckResponse{ 192 Error: "failed to check merge status: this knot does not support PRs", 193 } 194 case 400: 195 return types.MergeCheckResponse{ 196 Error: "failed to check merge status: does this knot support PRs?", 197 } 198 } 199 200 respBody, err := io.ReadAll(resp.Body) 201 if err != nil { 202 log.Println("failed to read merge check response body") 203 return types.MergeCheckResponse{ 204 Error: "failed to check merge status: knot is not speaking the right language", 205 } 206 } 207 defer resp.Body.Close() 208 209 var mergeCheckResponse types.MergeCheckResponse 210 err = json.Unmarshal(respBody, &mergeCheckResponse) 211 if err != nil { 212 log.Println("failed to unmarshal merge check response", err) 213 return types.MergeCheckResponse{ 214 Error: "failed to check merge status: knot is not speaking the right language", 215 } 216 } 217 218 return mergeCheckResponse 219} 220 221func (s *State) resubmitCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult { 222 ctx, span := s.t.TraceStart(ctx, "resubmitCheck") 223 defer span.End() 224 225 span.SetAttributes(attribute.Int("pull.id", pull.PullId)) 226 227 if pull.State == db.PullMerged || pull.PullSource == nil { 228 span.SetAttributes(attribute.String("result", "Unknown")) 229 return pages.Unknown 230 } 231 232 var knot, ownerDid, repoName string 233 234 if pull.PullSource.RepoAt != nil { 235 // fork-based pulls 236 span.SetAttributes(attribute.Bool("isForkBased", true)) 237 sourceRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String()) 238 if err != nil { 239 log.Println("failed to get source repo", err) 240 span.RecordError(err) 241 span.SetAttributes(attribute.String("error", "failed_to_get_source_repo")) 242 span.SetAttributes(attribute.String("result", "Unknown")) 243 return pages.Unknown 244 } 245 246 knot = sourceRepo.Knot 247 ownerDid = sourceRepo.Did 248 repoName = sourceRepo.Name 249 } else { 250 // pulls within the same repo 251 span.SetAttributes(attribute.Bool("isBranchBased", true)) 252 knot = f.Knot 253 ownerDid = f.OwnerDid() 254 repoName = f.RepoName 255 } 256 257 span.SetAttributes( 258 attribute.String("knot", knot), 259 attribute.String("ownerDid", ownerDid), 260 attribute.String("repoName", repoName), 261 attribute.String("sourceBranch", pull.PullSource.Branch), 262 ) 263 264 us, err := NewUnsignedClient(knot, s.config.Dev) 265 if err != nil { 266 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 267 span.RecordError(err) 268 span.SetAttributes(attribute.String("error", "failed_to_setup_client")) 269 span.SetAttributes(attribute.String("result", "Unknown")) 270 return pages.Unknown 271 } 272 273 resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 274 if err != nil { 275 log.Println("failed to reach knotserver", err) 276 span.RecordError(err) 277 span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver")) 278 span.SetAttributes(attribute.String("result", "Unknown")) 279 return pages.Unknown 280 } 281 282 body, err := io.ReadAll(resp.Body) 283 if err != nil { 284 log.Printf("error reading response body: %v", err) 285 span.RecordError(err) 286 span.SetAttributes(attribute.String("error", "failed_to_read_response")) 287 span.SetAttributes(attribute.String("result", "Unknown")) 288 return pages.Unknown 289 } 290 defer resp.Body.Close() 291 292 var result types.RepoBranchResponse 293 if err := json.Unmarshal(body, &result); err != nil { 294 log.Println("failed to parse response:", err) 295 span.RecordError(err) 296 span.SetAttributes(attribute.String("error", "failed_to_parse_response")) 297 span.SetAttributes(attribute.String("result", "Unknown")) 298 return pages.Unknown 299 } 300 301 latestSubmission := pull.Submissions[pull.LastRoundNumber()] 302 303 span.SetAttributes( 304 attribute.String("latestSubmission.SourceRev", latestSubmission.SourceRev), 305 attribute.String("branch.Hash", result.Branch.Hash), 306 ) 307 308 if latestSubmission.SourceRev != result.Branch.Hash { 309 fmt.Println(latestSubmission.SourceRev, result.Branch.Hash) 310 span.SetAttributes(attribute.String("result", "ShouldResubmit")) 311 return pages.ShouldResubmit 312 } 313 314 span.SetAttributes(attribute.String("result", "ShouldNotResubmit")) 315 return pages.ShouldNotResubmit 316} 317 318func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 319 ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatch") 320 defer span.End() 321 322 user := s.auth.GetUser(r.WithContext(ctx)) 323 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 324 if err != nil { 325 log.Println("failed to get repo and knot", err) 326 span.RecordError(err) 327 return 328 } 329 330 pull, ok := ctx.Value("pull").(*db.Pull) 331 if !ok { 332 err := errors.New("failed to get pull from context") 333 log.Println(err) 334 span.RecordError(err) 335 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 336 return 337 } 338 339 roundId := chi.URLParam(r, "round") 340 roundIdInt, err := strconv.Atoi(roundId) 341 if err != nil || roundIdInt >= len(pull.Submissions) { 342 http.Error(w, "bad round id", http.StatusBadRequest) 343 log.Println("failed to parse round id", err) 344 span.RecordError(err) 345 span.SetAttributes(attribute.String("error", "bad_round_id")) 346 return 347 } 348 349 span.SetAttributes( 350 attribute.Int("pull.id", pull.PullId), 351 attribute.Int("round", roundIdInt), 352 attribute.String("pull.owner", pull.OwnerDid), 353 ) 354 355 identsToResolve := []string{pull.OwnerDid} 356 resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 357 didHandleMap := make(map[string]string) 358 for _, identity := range resolvedIds { 359 if !identity.Handle.IsInvalidHandle() { 360 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 361 } else { 362 didHandleMap[identity.DID.String()] = identity.DID.String() 363 } 364 } 365 span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds))) 366 367 diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch) 368 369 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 370 LoggedInUser: user, 371 DidHandleMap: didHandleMap, 372 RepoInfo: f.RepoInfo(ctx, s, user), 373 Pull: pull, 374 Round: roundIdInt, 375 Submission: pull.Submissions[roundIdInt], 376 Diff: &diff, 377 }) 378} 379 380func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 381 ctx, span := s.t.TraceStart(r.Context(), "RepoPullInterdiff") 382 defer span.End() 383 384 user := s.auth.GetUser(r) 385 386 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 387 if err != nil { 388 log.Println("failed to get repo and knot", err) 389 return 390 } 391 392 pull, ok := ctx.Value("pull").(*db.Pull) 393 if !ok { 394 log.Println("failed to get pull") 395 s.pages.Notice(w, "pull-error", "Failed to get pull.") 396 return 397 } 398 399 _, roundSpan := s.t.TraceStart(ctx, "parseRound") 400 roundId := chi.URLParam(r, "round") 401 roundIdInt, err := strconv.Atoi(roundId) 402 if err != nil || roundIdInt >= len(pull.Submissions) { 403 http.Error(w, "bad round id", http.StatusBadRequest) 404 log.Println("failed to parse round id", err) 405 roundSpan.End() 406 return 407 } 408 409 if roundIdInt == 0 { 410 http.Error(w, "bad round id", http.StatusBadRequest) 411 log.Println("cannot interdiff initial submission") 412 roundSpan.End() 413 return 414 } 415 roundSpan.End() 416 417 _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 418 identsToResolve := []string{pull.OwnerDid} 419 resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 420 didHandleMap := make(map[string]string) 421 for _, identity := range resolvedIds { 422 if !identity.Handle.IsInvalidHandle() { 423 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 424 } else { 425 didHandleMap[identity.DID.String()] = identity.DID.String() 426 } 427 } 428 identSpan.End() 429 430 _, diffSpan := s.t.TraceStart(ctx, "calculateInterdiff") 431 currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch) 432 if err != nil { 433 log.Println("failed to interdiff; current patch malformed") 434 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 435 diffSpan.End() 436 return 437 } 438 439 previousPatch, err := pull.Submissions[roundIdInt-1].AsDiff(pull.TargetBranch) 440 if err != nil { 441 log.Println("failed to interdiff; previous patch malformed") 442 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 443 diffSpan.End() 444 return 445 } 446 447 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 448 diffSpan.End() 449 450 _, renderSpan := s.t.TraceStart(ctx, "renderInterdiffPage") 451 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 452 LoggedInUser: s.auth.GetUser(r.WithContext(ctx)), 453 RepoInfo: f.RepoInfo(ctx, s, user), 454 Pull: pull, 455 Round: roundIdInt, 456 DidHandleMap: didHandleMap, 457 Interdiff: interdiff, 458 }) 459 renderSpan.End() 460 return 461} 462 463func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 464 ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatchRaw") 465 defer span.End() 466 467 pull, ok := ctx.Value("pull").(*db.Pull) 468 if !ok { 469 log.Println("failed to get pull") 470 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 471 return 472 } 473 474 _, roundSpan := s.t.TraceStart(ctx, "parseRound") 475 roundId := chi.URLParam(r, "round") 476 roundIdInt, err := strconv.Atoi(roundId) 477 if err != nil || roundIdInt >= len(pull.Submissions) { 478 http.Error(w, "bad round id", http.StatusBadRequest) 479 log.Println("failed to parse round id", err) 480 roundSpan.End() 481 return 482 } 483 roundSpan.End() 484 485 _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 486 identsToResolve := []string{pull.OwnerDid} 487 resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 488 didHandleMap := make(map[string]string) 489 for _, identity := range resolvedIds { 490 if !identity.Handle.IsInvalidHandle() { 491 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 492 } else { 493 didHandleMap[identity.DID.String()] = identity.DID.String() 494 } 495 } 496 identSpan.End() 497 498 _, writeSpan := s.t.TraceStart(ctx, "writePatch") 499 w.Header().Set("Content-Type", "text/plain") 500 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 501 writeSpan.End() 502} 503 504func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 505 ctx, span := s.t.TraceStart(r.Context(), "RepoPulls") 506 defer span.End() 507 508 user := s.auth.GetUser(r) 509 params := r.URL.Query() 510 511 _, stateSpan := s.t.TraceStart(ctx, "determinePullState") 512 state := db.PullOpen 513 switch params.Get("state") { 514 case "closed": 515 state = db.PullClosed 516 case "merged": 517 state = db.PullMerged 518 } 519 stateSpan.End() 520 521 _, repoSpan := s.t.TraceStart(ctx, "resolveRepo") 522 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 523 if err != nil { 524 log.Println("failed to get repo and knot", err) 525 repoSpan.End() 526 return 527 } 528 repoSpan.End() 529 530 _, pullsSpan := s.t.TraceStart(ctx, "getPulls") 531 pulls, err := db.GetPulls(ctx, s.db, f.RepoAt, state) 532 if err != nil { 533 log.Println("failed to get pulls", err) 534 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 535 pullsSpan.End() 536 return 537 } 538 pullsSpan.End() 539 540 _, sourceRepoSpan := s.t.TraceStart(ctx, "resolvePullSources") 541 for _, p := range pulls { 542 var pullSourceRepo *db.Repo 543 if p.PullSource != nil { 544 if p.PullSource.RepoAt != nil { 545 pullSourceRepo, err = db.GetRepoByAtUri(ctx, s.db, p.PullSource.RepoAt.String()) 546 if err != nil { 547 log.Printf("failed to get repo by at uri: %v", err) 548 continue 549 } else { 550 p.PullSource.Repo = pullSourceRepo 551 } 552 } 553 } 554 } 555 sourceRepoSpan.End() 556 557 _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 558 identsToResolve := make([]string, len(pulls)) 559 for i, pull := range pulls { 560 identsToResolve[i] = pull.OwnerDid 561 } 562 resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 563 didHandleMap := make(map[string]string) 564 for _, identity := range resolvedIds { 565 if !identity.Handle.IsInvalidHandle() { 566 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 567 } else { 568 didHandleMap[identity.DID.String()] = identity.DID.String() 569 } 570 } 571 identSpan.End() 572 573 _, renderSpan := s.t.TraceStart(ctx, "renderPullsPage") 574 s.pages.RepoPulls(w, pages.RepoPullsParams{ 575 LoggedInUser: s.auth.GetUser(r.WithContext(ctx)), 576 RepoInfo: f.RepoInfo(ctx, s, user), 577 Pulls: pulls, 578 DidHandleMap: didHandleMap, 579 FilteringBy: state, 580 }) 581 renderSpan.End() 582 return 583} 584 585func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 586 ctx, span := s.t.TraceStart(r.Context(), "PullComment") 587 defer span.End() 588 589 user := s.auth.GetUser(r.WithContext(ctx)) 590 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 591 if err != nil { 592 log.Println("failed to get repo and knot", err) 593 return 594 } 595 596 pull, ok := ctx.Value("pull").(*db.Pull) 597 if !ok { 598 log.Println("failed to get pull") 599 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 600 return 601 } 602 603 _, roundSpan := s.t.TraceStart(ctx, "parseRoundNumber") 604 roundNumberStr := chi.URLParam(r, "round") 605 roundNumber, err := strconv.Atoi(roundNumberStr) 606 if err != nil || roundNumber >= len(pull.Submissions) { 607 http.Error(w, "bad round id", http.StatusBadRequest) 608 log.Println("failed to parse round id", err) 609 roundSpan.End() 610 return 611 } 612 roundSpan.End() 613 614 switch r.Method { 615 case http.MethodGet: 616 _, renderSpan := s.t.TraceStart(ctx, "renderCommentFragment") 617 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 618 LoggedInUser: user, 619 RepoInfo: f.RepoInfo(ctx, s, user), 620 Pull: pull, 621 RoundNumber: roundNumber, 622 }) 623 renderSpan.End() 624 return 625 case http.MethodPost: 626 postCtx, postSpan := s.t.TraceStart(ctx, "CreateComment") 627 defer postSpan.End() 628 629 _, validateSpan := s.t.TraceStart(postCtx, "validateComment") 630 body := r.FormValue("body") 631 if body == "" { 632 s.pages.Notice(w, "pull", "Comment body is required") 633 validateSpan.End() 634 return 635 } 636 validateSpan.End() 637 638 // Start a transaction 639 _, txSpan := s.t.TraceStart(postCtx, "startTransaction") 640 tx, err := s.db.BeginTx(postCtx, nil) 641 if err != nil { 642 log.Println("failed to start transaction", err) 643 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 644 txSpan.End() 645 return 646 } 647 defer tx.Rollback() 648 txSpan.End() 649 650 createdAt := time.Now().Format(time.RFC3339) 651 ownerDid := user.Did 652 653 _, pullAtSpan := s.t.TraceStart(postCtx, "getPullAt") 654 pullAt, err := db.GetPullAt(postCtx, s.db, f.RepoAt, pull.PullId) 655 if err != nil { 656 log.Println("failed to get pull at", err) 657 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 658 pullAtSpan.End() 659 return 660 } 661 pullAtSpan.End() 662 663 _, atProtoSpan := s.t.TraceStart(postCtx, "createAtProtoRecord") 664 atUri := f.RepoAt.String() 665 client, _ := s.auth.AuthorizedClient(r.WithContext(postCtx)) 666 atResp, err := comatproto.RepoPutRecord(postCtx, client, &comatproto.RepoPutRecord_Input{ 667 Collection: tangled.RepoPullCommentNSID, 668 Repo: user.Did, 669 Rkey: appview.TID(), 670 Record: &lexutil.LexiconTypeDecoder{ 671 Val: &tangled.RepoPullComment{ 672 Repo: &atUri, 673 Pull: string(pullAt), 674 Owner: &ownerDid, 675 Body: body, 676 CreatedAt: createdAt, 677 }, 678 }, 679 }) 680 if err != nil { 681 log.Println("failed to create pull comment", err) 682 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 683 atProtoSpan.End() 684 return 685 } 686 atProtoSpan.End() 687 688 // Create the pull comment in the database with the commentAt field 689 _, dbSpan := s.t.TraceStart(postCtx, "createDbComment") 690 commentId, err := db.NewPullComment(postCtx, tx, &db.PullComment{ 691 OwnerDid: user.Did, 692 RepoAt: f.RepoAt.String(), 693 PullId: pull.PullId, 694 Body: body, 695 CommentAt: atResp.Uri, 696 SubmissionId: pull.Submissions[roundNumber].ID, 697 }) 698 if err != nil { 699 log.Println("failed to create pull comment", err) 700 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 701 dbSpan.End() 702 return 703 } 704 dbSpan.End() 705 706 if err = tx.Commit(); err != nil { 707 log.Println("failed to commit transaction", err) 708 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 709 return 710 } 711 712 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 713 return 714 } 715} 716 717func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 718 ctx, span := s.t.TraceStart(r.Context(), "NewPull") 719 defer span.End() 720 721 user := s.auth.GetUser(r.WithContext(ctx)) 722 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 723 if err != nil { 724 log.Println("failed to get repo and knot", err) 725 span.RecordError(err) 726 return 727 } 728 729 switch r.Method { 730 case http.MethodGet: 731 span.SetAttributes(attribute.String("method", "GET")) 732 733 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 734 if err != nil { 735 log.Printf("failed to create unsigned client for %s", f.Knot) 736 span.RecordError(err) 737 s.pages.Error503(w) 738 return 739 } 740 741 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 742 if err != nil { 743 log.Println("failed to reach knotserver", err) 744 span.RecordError(err) 745 return 746 } 747 748 body, err := io.ReadAll(resp.Body) 749 if err != nil { 750 log.Printf("Error reading response body: %v", err) 751 span.RecordError(err) 752 return 753 } 754 755 var result types.RepoBranchesResponse 756 err = json.Unmarshal(body, &result) 757 if err != nil { 758 log.Println("failed to parse response:", err) 759 span.RecordError(err) 760 return 761 } 762 763 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 764 LoggedInUser: user, 765 RepoInfo: f.RepoInfo(ctx, s, user), 766 Branches: result.Branches, 767 }) 768 case http.MethodPost: 769 span.SetAttributes(attribute.String("method", "POST")) 770 771 title := r.FormValue("title") 772 body := r.FormValue("body") 773 targetBranch := r.FormValue("targetBranch") 774 fromFork := r.FormValue("fork") 775 sourceBranch := r.FormValue("sourceBranch") 776 patch := r.FormValue("patch") 777 778 span.SetAttributes( 779 attribute.String("targetBranch", targetBranch), 780 attribute.String("sourceBranch", sourceBranch), 781 attribute.Bool("hasFork", fromFork != ""), 782 attribute.Bool("hasPatch", patch != ""), 783 ) 784 785 if targetBranch == "" { 786 s.pages.Notice(w, "pull", "Target branch is required.") 787 span.SetAttributes(attribute.String("error", "missing_target_branch")) 788 return 789 } 790 791 // Determine PR type based on input parameters 792 isPushAllowed := f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() 793 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 794 isForkBased := fromFork != "" && sourceBranch != "" 795 isPatchBased := patch != "" && !isBranchBased && !isForkBased 796 797 span.SetAttributes( 798 attribute.Bool("isPushAllowed", isPushAllowed), 799 attribute.Bool("isBranchBased", isBranchBased), 800 attribute.Bool("isForkBased", isForkBased), 801 attribute.Bool("isPatchBased", isPatchBased), 802 ) 803 804 if isPatchBased && !patchutil.IsFormatPatch(patch) { 805 if title == "" { 806 s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 807 span.SetAttributes(attribute.String("error", "missing_title_for_git_diff")) 808 return 809 } 810 } 811 812 // Validate we have at least one valid PR creation method 813 if !isBranchBased && !isPatchBased && !isForkBased { 814 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 815 span.SetAttributes(attribute.String("error", "no_valid_pr_method")) 816 return 817 } 818 819 // Can't mix branch-based and patch-based approaches 820 if isBranchBased && patch != "" { 821 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 822 span.SetAttributes(attribute.String("error", "mixed_pr_methods")) 823 return 824 } 825 826 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 827 if err != nil { 828 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 829 span.RecordError(err) 830 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 831 return 832 } 833 834 caps, err := us.Capabilities() 835 if err != nil { 836 log.Println("error fetching knot caps", f.Knot, err) 837 span.RecordError(err) 838 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 839 return 840 } 841 842 span.SetAttributes( 843 attribute.Bool("caps.pullRequests.formatPatch", caps.PullRequests.FormatPatch), 844 attribute.Bool("caps.pullRequests.branchSubmissions", caps.PullRequests.BranchSubmissions), 845 attribute.Bool("caps.pullRequests.forkSubmissions", caps.PullRequests.ForkSubmissions), 846 attribute.Bool("caps.pullRequests.patchSubmissions", caps.PullRequests.PatchSubmissions), 847 ) 848 849 if !caps.PullRequests.FormatPatch { 850 s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 851 span.SetAttributes(attribute.String("error", "formatpatch_not_supported")) 852 return 853 } 854 855 // Handle the PR creation based on the type 856 if isBranchBased { 857 if !caps.PullRequests.BranchSubmissions { 858 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 859 span.SetAttributes(attribute.String("error", "branch_submissions_not_supported")) 860 return 861 } 862 s.handleBranchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, sourceBranch) 863 } else if isForkBased { 864 if !caps.PullRequests.ForkSubmissions { 865 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 866 span.SetAttributes(attribute.String("error", "fork_submissions_not_supported")) 867 return 868 } 869 s.handleForkBasedPull(w, r.WithContext(ctx), f, user, fromFork, title, body, targetBranch, sourceBranch) 870 } else if isPatchBased { 871 if !caps.PullRequests.PatchSubmissions { 872 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 873 span.SetAttributes(attribute.String("error", "patch_submissions_not_supported")) 874 return 875 } 876 s.handlePatchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch) 877 } 878 return 879 } 880} 881 882func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 883 ctx, span := s.t.TraceStart(r.Context(), "handleBranchBasedPull") 884 defer span.End() 885 886 span.SetAttributes( 887 attribute.String("targetBranch", targetBranch), 888 attribute.String("sourceBranch", sourceBranch), 889 ) 890 891 pullSource := &db.PullSource{ 892 Branch: sourceBranch, 893 } 894 recordPullSource := &tangled.RepoPull_Source{ 895 Branch: sourceBranch, 896 } 897 898 // Generate a patch using /compare 899 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 900 if err != nil { 901 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 902 span.RecordError(err) 903 span.SetAttributes(attribute.String("error", "client_creation_failed")) 904 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 905 return 906 } 907 908 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 909 if err != nil { 910 log.Println("failed to compare", err) 911 span.RecordError(err) 912 span.SetAttributes(attribute.String("error", "comparison_failed")) 913 s.pages.Notice(w, "pull", err.Error()) 914 return 915 } 916 917 sourceRev := comparison.Rev2 918 patch := comparison.Patch 919 920 span.SetAttributes(attribute.String("sourceRev", sourceRev)) 921 922 if !patchutil.IsPatchValid(patch) { 923 span.SetAttributes(attribute.String("error", "invalid_patch_format")) 924 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 925 return 926 } 927 928 s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 929} 930 931func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 932 ctx, span := s.t.TraceStart(r.Context(), "handlePatchBasedPull") 933 defer span.End() 934 935 span.SetAttributes(attribute.String("targetBranch", targetBranch)) 936 937 if !patchutil.IsPatchValid(patch) { 938 span.SetAttributes(attribute.String("error", "invalid_patch_format")) 939 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 940 return 941 } 942 943 s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, "", nil, nil) 944} 945 946func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 947 ctx, span := s.t.TraceStart(r.Context(), "handleForkBasedPull") 948 defer span.End() 949 950 span.SetAttributes( 951 attribute.String("forkRepo", forkRepo), 952 attribute.String("targetBranch", targetBranch), 953 attribute.String("sourceBranch", sourceBranch), 954 ) 955 956 fork, err := db.GetForkByDid(ctx, s.db, user.Did, forkRepo) 957 if errors.Is(err, sql.ErrNoRows) { 958 span.SetAttributes(attribute.String("error", "fork_not_found")) 959 s.pages.Notice(w, "pull", "No such fork.") 960 return 961 } else if err != nil { 962 log.Println("failed to fetch fork:", err) 963 span.RecordError(err) 964 span.SetAttributes(attribute.String("error", "fork_fetch_failed")) 965 s.pages.Notice(w, "pull", "Failed to fetch fork.") 966 return 967 } 968 969 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 970 if err != nil { 971 log.Println("failed to fetch registration key:", err) 972 span.RecordError(err) 973 span.SetAttributes(attribute.String("error", "registration_key_fetch_failed")) 974 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 975 return 976 } 977 978 sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 979 if err != nil { 980 log.Println("failed to create signed client:", err) 981 span.RecordError(err) 982 span.SetAttributes(attribute.String("error", "signed_client_creation_failed")) 983 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 984 return 985 } 986 987 us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 988 if err != nil { 989 log.Println("failed to create unsigned client:", err) 990 span.RecordError(err) 991 span.SetAttributes(attribute.String("error", "unsigned_client_creation_failed")) 992 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 993 return 994 } 995 996 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 997 if err != nil { 998 log.Println("failed to create hidden ref:", err, resp.StatusCode) 999 span.RecordError(err) 1000 span.SetAttributes(attribute.String("error", "hidden_ref_creation_failed")) 1001 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1002 return 1003 } 1004 1005 switch resp.StatusCode { 1006 case 404: 1007 span.SetAttributes(attribute.String("error", "not_found_status")) 1008 case 400: 1009 span.SetAttributes(attribute.String("error", "bad_request_status")) 1010 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 1011 return 1012 } 1013 1014 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 1015 span.SetAttributes(attribute.String("hiddenRef", hiddenRef)) 1016 1017 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 1018 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 1019 // hiddenRef: hidden/feature-1/main (on repo-fork) 1020 // targetBranch: main (on repo-1) 1021 // sourceBranch: feature-1 (on repo-fork) 1022 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 1023 if err != nil { 1024 log.Println("failed to compare across branches", err) 1025 span.RecordError(err) 1026 span.SetAttributes(attribute.String("error", "branch_comparison_failed")) 1027 s.pages.Notice(w, "pull", err.Error()) 1028 return 1029 } 1030 1031 sourceRev := comparison.Rev2 1032 patch := comparison.Patch 1033 span.SetAttributes(attribute.String("sourceRev", sourceRev)) 1034 1035 if !patchutil.IsPatchValid(patch) { 1036 span.SetAttributes(attribute.String("error", "invalid_patch_format")) 1037 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1038 return 1039 } 1040 1041 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 1042 if err != nil { 1043 log.Println("failed to parse fork AT URI", err) 1044 span.RecordError(err) 1045 span.SetAttributes(attribute.String("error", "fork_aturi_parse_failed")) 1046 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1047 return 1048 } 1049 1050 s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 1051 Branch: sourceBranch, 1052 RepoAt: &forkAtUri, 1053 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) 1054} 1055 1056func (s *State) createPullRequest( 1057 w http.ResponseWriter, 1058 r *http.Request, 1059 f *FullyResolvedRepo, 1060 user *auth.User, 1061 title, body, targetBranch string, 1062 patch string, 1063 sourceRev string, 1064 pullSource *db.PullSource, 1065 recordPullSource *tangled.RepoPull_Source, 1066) { 1067 ctx, span := s.t.TraceStart(r.Context(), "createPullRequest") 1068 defer span.End() 1069 1070 span.SetAttributes( 1071 attribute.String("targetBranch", targetBranch), 1072 attribute.String("sourceRev", sourceRev), 1073 attribute.Bool("hasPullSource", pullSource != nil), 1074 ) 1075 1076 tx, err := s.db.BeginTx(ctx, nil) 1077 if err != nil { 1078 log.Println("failed to start tx") 1079 span.RecordError(err) 1080 span.SetAttributes(attribute.String("error", "transaction_start_failed")) 1081 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1082 return 1083 } 1084 defer tx.Rollback() 1085 1086 // We've already checked earlier if it's diff-based and title is empty, 1087 // so if it's still empty now, it's intentionally skipped owing to format-patch. 1088 if title == "" { 1089 formatPatches, err := patchutil.ExtractPatches(patch) 1090 if err != nil { 1091 span.RecordError(err) 1092 span.SetAttributes(attribute.String("error", "extract_patches_failed")) 1093 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1094 return 1095 } 1096 if len(formatPatches) == 0 { 1097 span.SetAttributes(attribute.String("error", "no_patches_found")) 1098 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 1099 return 1100 } 1101 1102 title = formatPatches[0].Title 1103 body = formatPatches[0].Body 1104 span.SetAttributes( 1105 attribute.Bool("title_extracted", true), 1106 attribute.Bool("body_extracted", formatPatches[0].Body != ""), 1107 ) 1108 } 1109 1110 rkey := appview.TID() 1111 initialSubmission := db.PullSubmission{ 1112 Patch: patch, 1113 SourceRev: sourceRev, 1114 } 1115 err = db.NewPull(ctx, tx, &db.Pull{ 1116 Title: title, 1117 Body: body, 1118 TargetBranch: targetBranch, 1119 OwnerDid: user.Did, 1120 RepoAt: f.RepoAt, 1121 Rkey: rkey, 1122 Submissions: []*db.PullSubmission{ 1123 &initialSubmission, 1124 }, 1125 PullSource: pullSource, 1126 }) 1127 if err != nil { 1128 log.Println("failed to create pull request", err) 1129 span.RecordError(err) 1130 span.SetAttributes(attribute.String("error", "db_create_pull_failed")) 1131 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1132 return 1133 } 1134 1135 client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1136 pullId, err := db.NextPullId(s.db, f.RepoAt) 1137 if err != nil { 1138 log.Println("failed to get pull id", err) 1139 span.RecordError(err) 1140 span.SetAttributes(attribute.String("error", "get_pull_id_failed")) 1141 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1142 return 1143 } 1144 span.SetAttributes(attribute.Int("pullId", pullId)) 1145 1146 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1147 Collection: tangled.RepoPullNSID, 1148 Repo: user.Did, 1149 Rkey: rkey, 1150 Record: &lexutil.LexiconTypeDecoder{ 1151 Val: &tangled.RepoPull{ 1152 Title: title, 1153 PullId: int64(pullId), 1154 TargetRepo: string(f.RepoAt), 1155 TargetBranch: targetBranch, 1156 Patch: patch, 1157 Source: recordPullSource, 1158 }, 1159 }, 1160 }) 1161 1162 if err != nil { 1163 log.Println("failed to create pull request", err) 1164 span.RecordError(err) 1165 span.SetAttributes(attribute.String("error", "atproto_create_record_failed")) 1166 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1167 return 1168 } 1169 1170 if err = tx.Commit(); err != nil { 1171 log.Println("failed to commit transaction", err) 1172 span.RecordError(err) 1173 span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 1174 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1175 return 1176 } 1177 1178 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 1179} 1180 1181func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1182 ctx, span := s.t.TraceStart(r.Context(), "ValidatePatch") 1183 defer span.End() 1184 1185 _, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1186 if err != nil { 1187 log.Println("failed to get repo and knot", err) 1188 span.RecordError(err) 1189 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1190 return 1191 } 1192 1193 patch := r.FormValue("patch") 1194 span.SetAttributes(attribute.Bool("hasPatch", patch != "")) 1195 1196 if patch == "" { 1197 span.SetAttributes(attribute.String("error", "empty_patch")) 1198 s.pages.Notice(w, "patch-error", "Patch is required.") 1199 return 1200 } 1201 1202 if !patchutil.IsPatchValid(patch) { 1203 span.SetAttributes(attribute.String("error", "invalid_patch_format")) 1204 s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 1205 return 1206 } 1207 1208 isFormatPatch := patchutil.IsFormatPatch(patch) 1209 span.SetAttributes(attribute.Bool("isFormatPatch", isFormatPatch)) 1210 1211 if isFormatPatch { 1212 s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.") 1213 } else { 1214 s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") 1215 } 1216} 1217 1218func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1219 ctx, span := s.t.TraceStart(r.Context(), "PatchUploadFragment") 1220 defer span.End() 1221 1222 user := s.auth.GetUser(r.WithContext(ctx)) 1223 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1224 if err != nil { 1225 log.Println("failed to get repo and knot", err) 1226 span.RecordError(err) 1227 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1228 return 1229 } 1230 1231 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1232 RepoInfo: f.RepoInfo(ctx, s, user), 1233 }) 1234} 1235 1236func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1237 ctx, span := s.t.TraceStart(r.Context(), "CompareBranchesFragment") 1238 defer span.End() 1239 1240 user := s.auth.GetUser(r.WithContext(ctx)) 1241 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1242 if err != nil { 1243 log.Println("failed to get repo and knot", err) 1244 span.RecordError(err) 1245 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1246 return 1247 } 1248 1249 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 1250 if err != nil { 1251 log.Printf("failed to create unsigned client for %s", f.Knot) 1252 span.RecordError(err) 1253 span.SetAttributes(attribute.String("error", "client_creation_failed")) 1254 s.pages.Error503(w) 1255 return 1256 } 1257 1258 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 1259 if err != nil { 1260 log.Println("failed to reach knotserver", err) 1261 span.RecordError(err) 1262 span.SetAttributes(attribute.String("error", "knotserver_connection_failed")) 1263 return 1264 } 1265 1266 body, err := io.ReadAll(resp.Body) 1267 if err != nil { 1268 log.Printf("Error reading response body: %v", err) 1269 span.RecordError(err) 1270 span.SetAttributes(attribute.String("error", "response_read_failed")) 1271 return 1272 } 1273 defer resp.Body.Close() 1274 1275 var result types.RepoBranchesResponse 1276 err = json.Unmarshal(body, &result) 1277 if err != nil { 1278 log.Println("failed to parse response:", err) 1279 span.RecordError(err) 1280 span.SetAttributes(attribute.String("error", "response_parse_failed")) 1281 return 1282 } 1283 span.SetAttributes(attribute.Int("branches.count", len(result.Branches))) 1284 1285 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1286 RepoInfo: f.RepoInfo(ctx, s, user), 1287 Branches: result.Branches, 1288 }) 1289} 1290 1291func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1292 ctx, span := s.t.TraceStart(r.Context(), "CompareForksFragment") 1293 defer span.End() 1294 1295 user := s.auth.GetUser(r.WithContext(ctx)) 1296 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1297 if err != nil { 1298 log.Println("failed to get repo and knot", err) 1299 span.RecordError(err) 1300 return 1301 } 1302 1303 forks, err := db.GetForksByDid(ctx, s.db, user.Did) 1304 if err != nil { 1305 log.Println("failed to get forks", err) 1306 span.RecordError(err) 1307 return 1308 } 1309 1310 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1311 RepoInfo: f.RepoInfo(ctx, s, user), 1312 Forks: forks, 1313 }) 1314} 1315 1316func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1317 ctx, span := s.t.TraceStart(r.Context(), "CompareForksBranchesFragment") 1318 defer span.End() 1319 1320 user := s.auth.GetUser(r.WithContext(ctx)) 1321 1322 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1323 if err != nil { 1324 log.Println("failed to get repo and knot", err) 1325 span.RecordError(err) 1326 return 1327 } 1328 1329 forkVal := r.URL.Query().Get("fork") 1330 span.SetAttributes(attribute.String("fork", forkVal)) 1331 1332 // fork repo 1333 repo, err := db.GetRepo(ctx, s.db, user.Did, forkVal) 1334 if err != nil { 1335 log.Println("failed to get repo", user.Did, forkVal) 1336 span.RecordError(err) 1337 return 1338 } 1339 1340 sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1341 if err != nil { 1342 log.Printf("failed to create unsigned client for %s", repo.Knot) 1343 span.RecordError(err) 1344 s.pages.Error503(w) 1345 return 1346 } 1347 1348 sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1349 if err != nil { 1350 log.Println("failed to reach knotserver for source branches", err) 1351 span.RecordError(err) 1352 return 1353 } 1354 1355 sourceBody, err := io.ReadAll(sourceResp.Body) 1356 if err != nil { 1357 log.Println("failed to read source response body", err) 1358 span.RecordError(err) 1359 return 1360 } 1361 defer sourceResp.Body.Close() 1362 1363 var sourceResult types.RepoBranchesResponse 1364 err = json.Unmarshal(sourceBody, &sourceResult) 1365 if err != nil { 1366 log.Println("failed to parse source branches response:", err) 1367 span.RecordError(err) 1368 return 1369 } 1370 1371 targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1372 if err != nil { 1373 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1374 span.RecordError(err) 1375 s.pages.Error503(w) 1376 return 1377 } 1378 1379 targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1380 if err != nil { 1381 log.Println("failed to reach knotserver for target branches", err) 1382 span.RecordError(err) 1383 return 1384 } 1385 1386 targetBody, err := io.ReadAll(targetResp.Body) 1387 if err != nil { 1388 log.Println("failed to read target response body", err) 1389 span.RecordError(err) 1390 return 1391 } 1392 defer targetResp.Body.Close() 1393 1394 var targetResult types.RepoBranchesResponse 1395 err = json.Unmarshal(targetBody, &targetResult) 1396 if err != nil { 1397 log.Println("failed to parse target branches response:", err) 1398 span.RecordError(err) 1399 return 1400 } 1401 1402 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1403 RepoInfo: f.RepoInfo(ctx, s, user), 1404 SourceBranches: sourceResult.Branches, 1405 TargetBranches: targetResult.Branches, 1406 }) 1407} 1408 1409func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1410 ctx, span := s.t.TraceStart(r.Context(), "ResubmitPull") 1411 defer span.End() 1412 1413 user := s.auth.GetUser(r.WithContext(ctx)) 1414 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1415 if err != nil { 1416 log.Println("failed to get repo and knot", err) 1417 span.RecordError(err) 1418 return 1419 } 1420 1421 pull, ok := ctx.Value("pull").(*db.Pull) 1422 if !ok { 1423 log.Println("failed to get pull") 1424 span.RecordError(errors.New("failed to get pull from context")) 1425 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1426 return 1427 } 1428 1429 span.SetAttributes( 1430 attribute.Int("pull.id", pull.PullId), 1431 attribute.String("pull.owner", pull.OwnerDid), 1432 attribute.String("method", r.Method), 1433 ) 1434 1435 switch r.Method { 1436 case http.MethodGet: 1437 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1438 RepoInfo: f.RepoInfo(ctx, s, user), 1439 Pull: pull, 1440 }) 1441 return 1442 case http.MethodPost: 1443 if pull.IsPatchBased() { 1444 span.SetAttributes(attribute.String("pull.type", "patch_based")) 1445 s.resubmitPatch(w, r.WithContext(ctx)) 1446 return 1447 } else if pull.IsBranchBased() { 1448 span.SetAttributes(attribute.String("pull.type", "branch_based")) 1449 s.resubmitBranch(w, r.WithContext(ctx)) 1450 return 1451 } else if pull.IsForkBased() { 1452 span.SetAttributes(attribute.String("pull.type", "fork_based")) 1453 s.resubmitFork(w, r.WithContext(ctx)) 1454 return 1455 } 1456 span.SetAttributes(attribute.String("pull.type", "unknown")) 1457 } 1458} 1459 1460func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1461 ctx, span := s.t.TraceStart(r.Context(), "resubmitPatch") 1462 defer span.End() 1463 1464 user := s.auth.GetUser(r.WithContext(ctx)) 1465 1466 pull, ok := ctx.Value("pull").(*db.Pull) 1467 if !ok { 1468 log.Println("failed to get pull") 1469 span.RecordError(errors.New("failed to get pull from context")) 1470 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1471 return 1472 } 1473 1474 span.SetAttributes( 1475 attribute.Int("pull.id", pull.PullId), 1476 attribute.String("pull.owner", pull.OwnerDid), 1477 ) 1478 1479 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1480 if err != nil { 1481 log.Println("failed to get repo and knot", err) 1482 span.RecordError(err) 1483 return 1484 } 1485 1486 if user.Did != pull.OwnerDid { 1487 log.Println("unauthorized user") 1488 span.SetAttributes(attribute.String("error", "unauthorized_user")) 1489 w.WriteHeader(http.StatusUnauthorized) 1490 return 1491 } 1492 1493 patch := r.FormValue("patch") 1494 span.SetAttributes(attribute.Bool("has_patch", patch != "")) 1495 1496 if err = validateResubmittedPatch(pull, patch); err != nil { 1497 span.SetAttributes(attribute.String("error", "invalid_patch")) 1498 s.pages.Notice(w, "resubmit-error", err.Error()) 1499 return 1500 } 1501 1502 tx, err := s.db.BeginTx(ctx, nil) 1503 if err != nil { 1504 log.Println("failed to start tx") 1505 span.RecordError(err) 1506 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1507 return 1508 } 1509 defer tx.Rollback() 1510 1511 err = db.ResubmitPull(tx, pull, patch, "") 1512 if err != nil { 1513 log.Println("failed to resubmit pull request", err) 1514 span.RecordError(err) 1515 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1516 return 1517 } 1518 client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1519 1520 ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1521 if err != nil { 1522 // failed to get record 1523 span.RecordError(err) 1524 span.SetAttributes(attribute.String("error", "record_not_found")) 1525 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1526 return 1527 } 1528 1529 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1530 Collection: tangled.RepoPullNSID, 1531 Repo: user.Did, 1532 Rkey: pull.Rkey, 1533 SwapRecord: ex.Cid, 1534 Record: &lexutil.LexiconTypeDecoder{ 1535 Val: &tangled.RepoPull{ 1536 Title: pull.Title, 1537 PullId: int64(pull.PullId), 1538 TargetRepo: string(f.RepoAt), 1539 TargetBranch: pull.TargetBranch, 1540 Patch: patch, // new patch 1541 }, 1542 }, 1543 }) 1544 if err != nil { 1545 log.Println("failed to update record", err) 1546 span.RecordError(err) 1547 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1548 return 1549 } 1550 1551 if err = tx.Commit(); err != nil { 1552 log.Println("failed to commit transaction", err) 1553 span.RecordError(err) 1554 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1555 return 1556 } 1557 1558 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1559 return 1560} 1561 1562func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1563 ctx, span := s.t.TraceStart(r.Context(), "resubmitBranch") 1564 defer span.End() 1565 1566 user := s.auth.GetUser(r.WithContext(ctx)) 1567 1568 pull, ok := ctx.Value("pull").(*db.Pull) 1569 if !ok { 1570 log.Println("failed to get pull") 1571 span.RecordError(errors.New("failed to get pull from context")) 1572 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1573 return 1574 } 1575 1576 span.SetAttributes( 1577 attribute.Int("pull.id", pull.PullId), 1578 attribute.String("pull.owner", pull.OwnerDid), 1579 attribute.String("pull.source_branch", pull.PullSource.Branch), 1580 attribute.String("pull.target_branch", pull.TargetBranch), 1581 ) 1582 1583 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1584 if err != nil { 1585 log.Println("failed to get repo and knot", err) 1586 span.RecordError(err) 1587 return 1588 } 1589 1590 if user.Did != pull.OwnerDid { 1591 log.Println("unauthorized user") 1592 span.SetAttributes(attribute.String("error", "unauthorized_user")) 1593 w.WriteHeader(http.StatusUnauthorized) 1594 return 1595 } 1596 1597 if !f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() { 1598 log.Println("unauthorized user") 1599 span.SetAttributes(attribute.String("error", "push_not_allowed")) 1600 w.WriteHeader(http.StatusUnauthorized) 1601 return 1602 } 1603 1604 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1605 if err != nil { 1606 log.Printf("failed to create client for %s: %s", f.Knot, err) 1607 span.RecordError(err) 1608 span.SetAttributes(attribute.String("error", "client_creation_failed")) 1609 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1610 return 1611 } 1612 1613 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1614 if err != nil { 1615 log.Printf("compare request failed: %s", err) 1616 span.RecordError(err) 1617 span.SetAttributes(attribute.String("error", "compare_failed")) 1618 s.pages.Notice(w, "resubmit-error", err.Error()) 1619 return 1620 } 1621 1622 sourceRev := comparison.Rev2 1623 patch := comparison.Patch 1624 span.SetAttributes(attribute.String("source_rev", sourceRev)) 1625 1626 if err = validateResubmittedPatch(pull, patch); err != nil { 1627 span.SetAttributes(attribute.String("error", "invalid_patch")) 1628 s.pages.Notice(w, "resubmit-error", err.Error()) 1629 return 1630 } 1631 1632 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1633 span.SetAttributes(attribute.String("error", "no_changes")) 1634 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1635 return 1636 } 1637 1638 tx, err := s.db.BeginTx(ctx, nil) 1639 if err != nil { 1640 log.Println("failed to start tx") 1641 span.RecordError(err) 1642 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1643 return 1644 } 1645 defer tx.Rollback() 1646 1647 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1648 if err != nil { 1649 log.Println("failed to create pull request", err) 1650 span.RecordError(err) 1651 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1652 return 1653 } 1654 client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1655 1656 ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1657 if err != nil { 1658 // failed to get record 1659 span.RecordError(err) 1660 span.SetAttributes(attribute.String("error", "record_not_found")) 1661 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1662 return 1663 } 1664 1665 recordPullSource := &tangled.RepoPull_Source{ 1666 Branch: pull.PullSource.Branch, 1667 } 1668 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1669 Collection: tangled.RepoPullNSID, 1670 Repo: user.Did, 1671 Rkey: pull.Rkey, 1672 SwapRecord: ex.Cid, 1673 Record: &lexutil.LexiconTypeDecoder{ 1674 Val: &tangled.RepoPull{ 1675 Title: pull.Title, 1676 PullId: int64(pull.PullId), 1677 TargetRepo: string(f.RepoAt), 1678 TargetBranch: pull.TargetBranch, 1679 Patch: patch, // new patch 1680 Source: recordPullSource, 1681 }, 1682 }, 1683 }) 1684 if err != nil { 1685 log.Println("failed to update record", err) 1686 span.RecordError(err) 1687 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1688 return 1689 } 1690 1691 if err = tx.Commit(); err != nil { 1692 log.Println("failed to commit transaction", err) 1693 span.RecordError(err) 1694 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1695 return 1696 } 1697 1698 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1699 return 1700} 1701 1702func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1703 ctx, span := s.t.TraceStart(r.Context(), "resubmitFork") 1704 defer span.End() 1705 1706 user := s.auth.GetUser(r.WithContext(ctx)) 1707 1708 pull, ok := ctx.Value("pull").(*db.Pull) 1709 if !ok { 1710 log.Println("failed to get pull") 1711 span.RecordError(errors.New("failed to get pull from context")) 1712 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1713 return 1714 } 1715 1716 span.SetAttributes( 1717 attribute.Int("pull.id", pull.PullId), 1718 attribute.String("pull.owner", pull.OwnerDid), 1719 attribute.String("pull.source_branch", pull.PullSource.Branch), 1720 attribute.String("pull.target_branch", pull.TargetBranch), 1721 ) 1722 1723 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1724 if err != nil { 1725 log.Println("failed to get repo and knot", err) 1726 span.RecordError(err) 1727 return 1728 } 1729 1730 if user.Did != pull.OwnerDid { 1731 log.Println("unauthorized user") 1732 span.SetAttributes(attribute.String("error", "unauthorized_user")) 1733 w.WriteHeader(http.StatusUnauthorized) 1734 return 1735 } 1736 1737 forkRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String()) 1738 if err != nil { 1739 log.Println("failed to get source repo", err) 1740 span.RecordError(err) 1741 span.SetAttributes(attribute.String("error", "source_repo_not_found")) 1742 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1743 return 1744 } 1745 1746 span.SetAttributes( 1747 attribute.String("fork.knot", forkRepo.Knot), 1748 attribute.String("fork.did", forkRepo.Did), 1749 attribute.String("fork.name", forkRepo.Name), 1750 ) 1751 1752 // extract patch by performing compare 1753 ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1754 if err != nil { 1755 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1756 span.RecordError(err) 1757 span.SetAttributes(attribute.String("error", "client_creation_failed")) 1758 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1759 return 1760 } 1761 1762 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1763 if err != nil { 1764 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1765 span.RecordError(err) 1766 span.SetAttributes(attribute.String("error", "reg_key_not_found")) 1767 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1768 return 1769 } 1770 1771 // update the hidden tracking branch to latest 1772 signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1773 if err != nil { 1774 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1775 span.RecordError(err) 1776 span.SetAttributes(attribute.String("error", "signed_client_creation_failed")) 1777 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1778 return 1779 } 1780 1781 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1782 if err != nil || resp.StatusCode != http.StatusNoContent { 1783 log.Printf("failed to update tracking branch: %s", err) 1784 span.RecordError(err) 1785 span.SetAttributes(attribute.String("error", "hidden_ref_update_failed")) 1786 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1787 return 1788 } 1789 1790 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1791 span.SetAttributes(attribute.String("hidden_ref", hiddenRef)) 1792 1793 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1794 if err != nil { 1795 log.Printf("failed to compare branches: %s", err) 1796 span.RecordError(err) 1797 span.SetAttributes(attribute.String("error", "compare_failed")) 1798 s.pages.Notice(w, "resubmit-error", err.Error()) 1799 return 1800 } 1801 1802 sourceRev := comparison.Rev2 1803 patch := comparison.Patch 1804 span.SetAttributes(attribute.String("source_rev", sourceRev)) 1805 1806 if err = validateResubmittedPatch(pull, patch); err != nil { 1807 span.SetAttributes(attribute.String("error", "invalid_patch")) 1808 s.pages.Notice(w, "resubmit-error", err.Error()) 1809 return 1810 } 1811 1812 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1813 span.SetAttributes(attribute.String("error", "no_changes")) 1814 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1815 return 1816 } 1817 1818 tx, err := s.db.BeginTx(ctx, nil) 1819 if err != nil { 1820 log.Println("failed to start tx") 1821 span.RecordError(err) 1822 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1823 return 1824 } 1825 defer tx.Rollback() 1826 1827 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1828 if err != nil { 1829 log.Println("failed to create pull request", err) 1830 span.RecordError(err) 1831 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1832 return 1833 } 1834 client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1835 1836 ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1837 if err != nil { 1838 // failed to get record 1839 span.RecordError(err) 1840 span.SetAttributes(attribute.String("error", "record_not_found")) 1841 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1842 return 1843 } 1844 1845 repoAt := pull.PullSource.RepoAt.String() 1846 recordPullSource := &tangled.RepoPull_Source{ 1847 Branch: pull.PullSource.Branch, 1848 Repo: &repoAt, 1849 } 1850 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1851 Collection: tangled.RepoPullNSID, 1852 Repo: user.Did, 1853 Rkey: pull.Rkey, 1854 SwapRecord: ex.Cid, 1855 Record: &lexutil.LexiconTypeDecoder{ 1856 Val: &tangled.RepoPull{ 1857 Title: pull.Title, 1858 PullId: int64(pull.PullId), 1859 TargetRepo: string(f.RepoAt), 1860 TargetBranch: pull.TargetBranch, 1861 Patch: patch, // new patch 1862 Source: recordPullSource, 1863 }, 1864 }, 1865 }) 1866 if err != nil { 1867 log.Println("failed to update record", err) 1868 span.RecordError(err) 1869 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1870 return 1871 } 1872 1873 if err = tx.Commit(); err != nil { 1874 log.Println("failed to commit transaction", err) 1875 span.RecordError(err) 1876 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1877 return 1878 } 1879 1880 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1881 return 1882} 1883 1884// validate a resubmission against a pull request 1885func validateResubmittedPatch(pull *db.Pull, patch string) error { 1886 if patch == "" { 1887 return fmt.Errorf("Patch is empty.") 1888 } 1889 1890 if patch == pull.LatestPatch() { 1891 return fmt.Errorf("Patch is identical to previous submission.") 1892 } 1893 1894 if !patchutil.IsPatchValid(patch) { 1895 return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1896 } 1897 1898 return nil 1899} 1900 1901func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1902 ctx, span := s.t.TraceStart(r.Context(), "MergePull") 1903 defer span.End() 1904 1905 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1906 if err != nil { 1907 log.Println("failed to resolve repo:", err) 1908 span.RecordError(err) 1909 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1910 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1911 return 1912 } 1913 1914 pull, ok := ctx.Value("pull").(*db.Pull) 1915 if !ok { 1916 log.Println("failed to get pull") 1917 span.SetAttributes(attribute.String("error", "pull_not_in_context")) 1918 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1919 return 1920 } 1921 1922 span.SetAttributes( 1923 attribute.Int("pull.id", pull.PullId), 1924 attribute.String("pull.owner", pull.OwnerDid), 1925 attribute.String("target_branch", pull.TargetBranch), 1926 ) 1927 1928 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1929 if err != nil { 1930 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1931 span.RecordError(err) 1932 span.SetAttributes(attribute.String("error", "reg_key_not_found")) 1933 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1934 return 1935 } 1936 1937 ident, err := s.resolver.ResolveIdent(ctx, pull.OwnerDid) 1938 if err != nil { 1939 log.Printf("resolving identity: %s", err) 1940 span.RecordError(err) 1941 span.SetAttributes(attribute.String("error", "resolve_identity_failed")) 1942 w.WriteHeader(http.StatusNotFound) 1943 return 1944 } 1945 1946 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1947 if err != nil { 1948 log.Printf("failed to get primary email: %s", err) 1949 span.RecordError(err) 1950 span.SetAttributes(attribute.String("error", "get_email_failed")) 1951 } 1952 1953 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1954 if err != nil { 1955 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1956 span.RecordError(err) 1957 span.SetAttributes(attribute.String("error", "client_creation_failed")) 1958 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1959 return 1960 } 1961 1962 // Merge the pull request 1963 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1964 if err != nil { 1965 log.Printf("failed to merge pull request: %s", err) 1966 span.RecordError(err) 1967 span.SetAttributes(attribute.String("error", "merge_failed")) 1968 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1969 return 1970 } 1971 1972 span.SetAttributes(attribute.Int("response.status", resp.StatusCode)) 1973 1974 if resp.StatusCode == http.StatusOK { 1975 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1976 if err != nil { 1977 log.Printf("failed to update pull request status in database: %s", err) 1978 span.RecordError(err) 1979 span.SetAttributes(attribute.String("error", "db_update_failed")) 1980 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1981 return 1982 } 1983 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1984 } else { 1985 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1986 span.SetAttributes(attribute.String("error", "non_ok_response")) 1987 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1988 } 1989} 1990 1991func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1992 ctx, span := s.t.TraceStart(r.Context(), "ClosePull") 1993 defer span.End() 1994 1995 user := s.auth.GetUser(r.WithContext(ctx)) 1996 1997 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1998 if err != nil { 1999 log.Println("malformed middleware") 2000 span.RecordError(err) 2001 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 2002 return 2003 } 2004 2005 pull, ok := ctx.Value("pull").(*db.Pull) 2006 if !ok { 2007 log.Println("failed to get pull") 2008 span.SetAttributes(attribute.String("error", "pull_not_in_context")) 2009 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2010 return 2011 } 2012 2013 span.SetAttributes( 2014 attribute.Int("pull.id", pull.PullId), 2015 attribute.String("pull.owner", pull.OwnerDid), 2016 attribute.String("user.did", user.Did), 2017 ) 2018 2019 // auth filter: only owner or collaborators can close 2020 roles := RolesInRepo(s, user, f) 2021 isCollaborator := roles.IsCollaborator() 2022 isPullAuthor := user.Did == pull.OwnerDid 2023 isCloseAllowed := isCollaborator || isPullAuthor 2024 2025 span.SetAttributes( 2026 attribute.Bool("is_collaborator", isCollaborator), 2027 attribute.Bool("is_pull_author", isPullAuthor), 2028 attribute.Bool("is_close_allowed", isCloseAllowed), 2029 ) 2030 2031 if !isCloseAllowed { 2032 log.Println("failed to close pull") 2033 span.SetAttributes(attribute.String("error", "unauthorized")) 2034 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2035 return 2036 } 2037 2038 // Start a transaction 2039 tx, err := s.db.BeginTx(ctx, nil) 2040 if err != nil { 2041 log.Println("failed to start transaction", err) 2042 span.RecordError(err) 2043 span.SetAttributes(attribute.String("error", "transaction_start_failed")) 2044 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2045 return 2046 } 2047 2048 // Close the pull in the database 2049 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 2050 if err != nil { 2051 log.Println("failed to close pull", err) 2052 span.RecordError(err) 2053 span.SetAttributes(attribute.String("error", "db_close_failed")) 2054 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2055 return 2056 } 2057 2058 // Commit the transaction 2059 if err = tx.Commit(); err != nil { 2060 log.Println("failed to commit transaction", err) 2061 span.RecordError(err) 2062 span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 2063 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2064 return 2065 } 2066 2067 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2068 return 2069} 2070 2071func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 2072 ctx, span := s.t.TraceStart(r.Context(), "ReopenPull") 2073 defer span.End() 2074 2075 user := s.auth.GetUser(r.WithContext(ctx)) 2076 2077 f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2078 if err != nil { 2079 log.Println("failed to resolve repo", err) 2080 span.RecordError(err) 2081 span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 2082 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2083 return 2084 } 2085 2086 pull, ok := ctx.Value("pull").(*db.Pull) 2087 if !ok { 2088 log.Println("failed to get pull") 2089 span.SetAttributes(attribute.String("error", "pull_not_in_context")) 2090 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2091 return 2092 } 2093 2094 span.SetAttributes( 2095 attribute.Int("pull.id", pull.PullId), 2096 attribute.String("pull.owner", pull.OwnerDid), 2097 attribute.String("user.did", user.Did), 2098 ) 2099 2100 // auth filter: only owner or collaborators can reopen 2101 roles := RolesInRepo(s, user, f) 2102 isCollaborator := roles.IsCollaborator() 2103 isPullAuthor := user.Did == pull.OwnerDid 2104 isReopenAllowed := isCollaborator || isPullAuthor 2105 2106 span.SetAttributes( 2107 attribute.Bool("is_collaborator", isCollaborator), 2108 attribute.Bool("is_pull_author", isPullAuthor), 2109 attribute.Bool("is_reopen_allowed", isReopenAllowed), 2110 ) 2111 2112 if !isReopenAllowed { 2113 log.Println("failed to reopen pull") 2114 span.SetAttributes(attribute.String("error", "unauthorized")) 2115 s.pages.Notice(w, "pull-close", "You are unauthorized to reopen this pull.") 2116 return 2117 } 2118 2119 // Start a transaction 2120 tx, err := s.db.BeginTx(ctx, nil) 2121 if err != nil { 2122 log.Println("failed to start transaction", err) 2123 span.RecordError(err) 2124 span.SetAttributes(attribute.String("error", "transaction_start_failed")) 2125 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2126 return 2127 } 2128 2129 // Reopen the pull in the database 2130 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 2131 if err != nil { 2132 log.Println("failed to reopen pull", err) 2133 span.RecordError(err) 2134 span.SetAttributes(attribute.String("error", "db_reopen_failed")) 2135 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2136 return 2137 } 2138 2139 // Commit the transaction 2140 if err = tx.Commit(); err != nil { 2141 log.Println("failed to commit transaction", err) 2142 span.RecordError(err) 2143 span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 2144 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2145 return 2146 } 2147 2148 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2149 return 2150}