Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

1package pulls 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "time" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/compat113" 11 "tangled.org/core/appview/db" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/oauth" 14 "tangled.org/core/appview/pages" 15 "tangled.org/core/appview/pages/repoinfo" 16 "tangled.org/core/appview/reporesolver" 17 "tangled.org/core/appview/xrpcclient" 18 "tangled.org/core/orm" 19 "tangled.org/core/patchutil" 20 "tangled.org/core/types" 21 "tangled.org/core/xrpc" 22 23 comatproto "github.com/bluesky-social/indigo/api/atproto" 24 "github.com/bluesky-social/indigo/atproto/syntax" 25 lexutil "github.com/bluesky-social/indigo/lex/util" 26) 27 28func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 29 l := s.logger.With("handler", "ResubmitPull") 30 31 user := s.oauth.GetMultiAccountUser(r) 32 if user != nil { 33 l = l.With("user", user.Did) 34 } 35 36 pull, ok := r.Context().Value("pull").(*models.Pull) 37 if !ok { 38 l.Error("failed to get pull") 39 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 40 return 41 } 42 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 43 44 switch r.Method { 45 case http.MethodGet: 46 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 47 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 48 Pull: pull, 49 }) 50 return 51 case http.MethodPost: 52 if pull.IsPatchBased() { 53 s.resubmitPatch(w, r) 54 return 55 } else if pull.IsBranchBased() { 56 s.resubmitBranch(w, r) 57 return 58 } else if pull.IsForkBased() { 59 s.resubmitFork(w, r) 60 return 61 } 62 } 63} 64 65func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 66 l := s.logger.With("handler", "resubmitPatch") 67 68 user := s.oauth.GetMultiAccountUser(r) 69 if user != nil { 70 l = l.With("user", user.Did) 71 } 72 73 pull, ok := r.Context().Value("pull").(*models.Pull) 74 if !ok { 75 l.Error("failed to get pull") 76 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 77 return 78 } 79 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 80 81 if user == nil || user.Did != pull.OwnerDid { 82 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 83 w.WriteHeader(http.StatusUnauthorized) 84 return 85 } 86 87 f, err := s.repoResolver.Resolve(r) 88 if err != nil { 89 l.Error("failed to get repo and knot", "err", err) 90 return 91 } 92 93 patch := r.FormValue("patch") 94 95 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, "", "") 96} 97 98func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 99 l := s.logger.With("handler", "resubmitBranch") 100 101 user := s.oauth.GetMultiAccountUser(r) 102 if user != nil { 103 l = l.With("user", user.Did) 104 } 105 106 pull, ok := r.Context().Value("pull").(*models.Pull) 107 if !ok { 108 l.Error("failed to get pull") 109 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 110 return 111 } 112 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 113 114 if user == nil || user.Did != pull.OwnerDid { 115 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 116 w.WriteHeader(http.StatusUnauthorized) 117 return 118 } 119 120 f, err := s.repoResolver.Resolve(r) 121 if err != nil { 122 l.Error("failed to get repo and knot", "err", err) 123 return 124 } 125 126 roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.RepoIdentifier())} 127 if !roles.IsPushAllowed() { 128 l.Warn("unauthorized user - no push permission") 129 w.WriteHeader(http.StatusUnauthorized) 130 return 131 } 132 133 xrpcc := s.knotClient(f.Knot) 134 135 xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 136 if err != nil { 137 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 138 l.Error("failed to call XRPC repo.compare", "xrpcerr", xrpcerr, "err", err, "source_branch", pull.PullSource.Branch) 139 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 140 return 141 } 142 l.Error("compare request failed", "err", err, "source_branch", pull.PullSource.Branch) 143 s.pages.Notice(w, "resubmit-error", err.Error()) 144 return 145 } 146 147 var comparison types.RepoFormatPatchResponse 148 if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 149 l.Error("failed to decode XRPC compare response", "err", err) 150 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 151 return 152 } 153 154 sourceRev := comparison.Rev2 155 patch := comparison.FormatPatchRaw 156 combined := comparison.CombinedPatchRaw 157 158 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 159} 160 161func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 162 l := s.logger.With("handler", "resubmitFork") 163 164 user := s.oauth.GetMultiAccountUser(r) 165 if user != nil { 166 l = l.With("user", user.Did) 167 } 168 169 pull, ok := r.Context().Value("pull").(*models.Pull) 170 if !ok { 171 l.Error("failed to get pull") 172 s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 173 return 174 } 175 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 176 177 if user == nil || user.Did != pull.OwnerDid { 178 l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 179 w.WriteHeader(http.StatusUnauthorized) 180 return 181 } 182 183 f, err := s.repoResolver.Resolve(r) 184 if err != nil { 185 l.Error("failed to get repo and knot", "err", err) 186 return 187 } 188 189 forkRepo, err := db.GetRepoByDid(s.db, string(*pull.PullSource.RepoDid)) 190 if err != nil { 191 l.Error("failed to get source repo", "err", err, "repo_did", pull.PullSource.RepoDid.String()) 192 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 193 return 194 } 195 196 // update the hidden tracking branch to latest 197 client, err := s.oauth.ServiceClient( 198 r, 199 oauth.WithService(forkRepo.Knot), 200 oauth.WithLxm(tangled.RepoHiddenRefNSID), 201 oauth.WithDev(s.config.Core.Dev), 202 ) 203 if err != nil { 204 l.Error("failed to connect to knot server", "err", err, "fork_knot", forkRepo.Knot) 205 return 206 } 207 208 resp, err := tangled.RepoHiddenRef( 209 r.Context(), 210 client, 211 &tangled.RepoHiddenRef_Input{ 212 ForkRef: pull.PullSource.Branch, 213 RemoteRef: pull.TargetBranch, 214 Repo: forkRepo.RepoAt().String(), 215 }, 216 ) 217 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 218 s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 219 s.pages.Notice(w, "resubmit-error", xrpcerr.Error()) 220 return 221 } 222 if !resp.Success { 223 l.Error("failed to update tracking ref", "err", resp.Error, "fork_ref", pull.PullSource.Branch, "remote_ref", pull.TargetBranch) 224 s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 225 return 226 } 227 228 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 229 // extract patch by performing compare 230 forkXrpcBytes, err := tangled.RepoCompare(r.Context(), s.knotClient(forkRepo.Knot), forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 231 if err != nil { 232 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 233 l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 234 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 235 return 236 } 237 l.Error("failed to compare branches", "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 238 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 239 return 240 } 241 242 var forkComparison types.RepoFormatPatchResponse 243 if err := json.Unmarshal(forkXrpcBytes, &forkComparison); err != nil { 244 l.Error("failed to decode XRPC compare response for fork", "err", err) 245 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 246 return 247 } 248 249 // Use the fork comparison we already made 250 comparison := forkComparison 251 252 sourceRev := comparison.Rev2 253 patch := comparison.FormatPatchRaw 254 combined := comparison.CombinedPatchRaw 255 256 s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 257} 258 259func (s *Pulls) resubmitPullHelper( 260 w http.ResponseWriter, 261 r *http.Request, 262 repo *models.Repo, 263 userDid syntax.DID, 264 pull *models.Pull, 265 patch string, 266 combined string, 267 sourceRev string, 268) { 269 l := s.logger.With("handler", "resubmitPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 270 271 stack := r.Context().Value("stack").(models.Stack) 272 if stack != nil && len(stack) != 1 { 273 l.Info("resubmitting stacked PR", "stack_size", len(stack)) 274 s.resubmitStackedPullHelper(w, r, repo, userDid, pull, patch) 275 return 276 } 277 278 if err := s.validator.ValidatePatch(&patch); err != nil { 279 s.pages.Notice(w, "resubmit-error", err.Error()) 280 return 281 } 282 283 if patch == pull.LatestPatch() { 284 s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 285 return 286 } 287 288 // validate sourceRev if branch/fork based 289 if pull.IsBranchBased() || pull.IsForkBased() { 290 if sourceRev == pull.LatestSha() { 291 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 292 return 293 } 294 } 295 296 pullAt := pull.AtUri() 297 newRoundNumber := len(pull.Submissions) 298 newPatch := patch 299 newSourceRev := sourceRev 300 combinedPatch := combined 301 302 client, err := s.oauth.AuthorizedClient(r) 303 if err != nil { 304 l.Error("failed to authorize client", "err", err) 305 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 306 return 307 } 308 309 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, userDid.String(), pull.Rkey) 310 if err != nil { 311 // failed to get record 312 l.Error("failed to get record from PDS", "err", err, "rkey", pull.Rkey) 313 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 314 return 315 } 316 317 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 318 if err != nil { 319 l.Error("failed to upload patch blob", "err", err) 320 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 321 return 322 } 323 record := pull.AsRecord() 324 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 325 CreatedAt: time.Now().Format(time.RFC3339), 326 PatchBlob: blob.Blob, 327 }) 328 329 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 330 Collection: tangled.RepoPullNSID, 331 Repo: userDid.String(), 332 Rkey: pull.Rkey, 333 SwapRecord: ex.Cid, 334 Record: compat113.Pull(&record), 335 }) 336 if err != nil { 337 l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey) 338 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 339 return 340 } 341 342 err = db.ResubmitPull(s.db, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 343 if err != nil { 344 l.Error("failed to resubmit pull request in database", "err", err, "round_number", newRoundNumber) 345 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 346 return 347 } 348 349 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 350 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 351} 352 353func (s *Pulls) resubmitStackedPullHelper( 354 w http.ResponseWriter, 355 r *http.Request, 356 repo *models.Repo, 357 userDid syntax.DID, 358 pull *models.Pull, 359 patch string, 360) { 361 l := s.logger.With("handler", "resubmitStackedPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 362 363 targetBranch := pull.TargetBranch 364 365 origStack, _ := r.Context().Value("stack").(models.Stack) 366 367 formatPatches, err := patchutil.ExtractPatches(patch) 368 if err != nil { 369 l.Error("failed to extract patches", "err", err) 370 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Failed to parse patches.") 371 return 372 } 373 374 // must have atleast 1 patch to begin with 375 if len(formatPatches) == 0 { 376 l.Error("no patches found in the generated format-patch") 377 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request: No patches found in the generated patch.") 378 return 379 } 380 381 client, err := s.oauth.AuthorizedClient(r) 382 if err != nil { 383 l.Error("failed to get authorized client", "err", err) 384 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 385 return 386 } 387 388 // first upload all blobs 389 blobs := make([]*lexutil.LexBlob, len(formatPatches)) 390 for i, p := range formatPatches { 391 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 392 if err != nil { 393 l.Error("failed to upload patch blob", "err", err, "patch_index", i) 394 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 395 return 396 } 397 l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 398 blobs[i] = blob.Blob 399 } 400 401 newStack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pull.PullSource, formatPatches, blobs, nil, nil) 402 if err != nil { 403 l.Error("failed to create resubmitted stack", "err", err) 404 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 405 return 406 } 407 408 // find the diff between the stacks, first, map them by changeId 409 origById := make(map[string]*models.Pull) 410 newById := make(map[string]*models.Pull) 411 for _, p := range origStack { 412 origById[p.LatestSubmission().ChangeId()] = p 413 } 414 for _, p := range newStack { 415 newById[p.LatestSubmission().ChangeId()] = p 416 } 417 418 // commits that got deleted: corresponding pull is closed 419 // commits that got added: new pull is created 420 // commits that got updated: corresponding pull is resubmitted & new round begins 421 additions := make(map[string]*models.Pull) 422 deletions := make(map[string]*models.Pull) 423 updated := make(map[string]struct{}) 424 425 // pulls in original stack but not in new one 426 for _, op := range origStack { 427 if _, ok := newById[op.LatestSubmission().ChangeId()]; !ok { 428 deletions[op.LatestSubmission().ChangeId()] = op 429 } 430 } 431 432 // pulls in new stack but not in original one 433 for _, np := range newStack { 434 if _, ok := origById[np.LatestSubmission().ChangeId()]; !ok { 435 additions[np.LatestSubmission().ChangeId()] = np 436 } 437 } 438 439 // NOTE: this loop can be written in any of above blocks, 440 // but is written separately in the interest of simpler code 441 for _, np := range newStack { 442 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 443 // pull exists in both stacks 444 updated[op.LatestSubmission().ChangeId()] = struct{}{} 445 } 446 } 447 448 // NOTE: we can go through the newStack and update dependent relations and 449 // rkeys now that we know which ones have been updated 450 // update dependentOn relations for the entire stack 451 var parentAt *syntax.ATURI 452 for _, np := range newStack { 453 if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 454 // pull exists in both stacks 455 np.Rkey = op.Rkey 456 } 457 np.DependentOn = parentAt 458 x := np.AtUri() 459 parentAt = &x 460 } 461 462 l = l.With("additions", len(additions), "deletions", len(deletions), "updates", len(updated)) 463 464 tx, err := s.db.Begin() 465 if err != nil { 466 l.Error("failed to start transaction", "err", err) 467 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 468 return 469 } 470 defer tx.Rollback() 471 472 // pds updates to make 473 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 474 475 // deleted pulls are marked as deleted in the DB 476 for _, p := range deletions { 477 // do not do delete already merged PRs 478 if p.State == models.PullMerged { 479 continue 480 } 481 482 err := db.AbandonPulls(tx, orm.FilterEq("repo_did", string(p.RepoDid)), orm.FilterEq("at_uri", p.AtUri())) 483 if err != nil { 484 l.Error("failed to delete pull", "err", err, "pull_id", p.PullId) 485 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 486 return 487 } 488 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 489 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 490 Collection: tangled.RepoPullNSID, 491 Rkey: p.Rkey, 492 }, 493 }) 494 } 495 496 // new pulls are created 497 for _, p := range additions { 498 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()), ApplicationGzip) 499 if err != nil { 500 l.Error("failed to upload patch blob for new pull", "err", err, "change_id", p.LatestSubmission().ChangeId()) 501 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 502 return 503 } 504 p.Submissions[0].Blob = *blob.Blob 505 506 if err = db.PutPull(tx, p); err != nil { 507 l.Error("failed to create pull", "err", err, "pull_id", p.PullId, "change_id", p.LatestSubmission().ChangeId()) 508 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 509 return 510 } 511 512 record := p.AsRecord() 513 record.Rounds = []*tangled.RepoPull_Round{ 514 { 515 CreatedAt: time.Now().Format(time.RFC3339), 516 PatchBlob: blob.Blob, 517 }, 518 } 519 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 520 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 521 Collection: tangled.RepoPullNSID, 522 Rkey: &p.Rkey, 523 Value: compat113.Pull(&record), 524 }, 525 }) 526 } 527 528 // updated pulls are, well, updated; to start a new round 529 for id := range updated { 530 op, _ := origById[id] 531 np, _ := newById[id] 532 533 // do not update already merged PRs 534 if op.State == models.PullMerged { 535 continue 536 } 537 538 // resubmit the new pull 539 np.Rkey = op.Rkey 540 pullAt := op.AtUri() 541 newRoundNumber := len(op.Submissions) 542 newPatch := np.LatestPatch() 543 combinedPatch := np.LatestSubmission().Combined 544 newSourceRev := np.LatestSha() 545 546 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(newPatch), ApplicationGzip) 547 if err != nil { 548 l.Error("failed to upload patch blob for update", "err", err, "change_id", id, "pull_id", op.PullId) 549 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 550 return 551 } 552 553 // create new round 554 err = db.ResubmitPull(tx, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 555 if err != nil { 556 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 557 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 558 return 559 } 560 561 // update dependent-on relation 562 if np.DependentOn != nil { 563 err := db.SetDependentOn(tx, *np.DependentOn, orm.FilterEq("at_uri", np.AtUri())) 564 if err != nil { 565 l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 566 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 567 return 568 } 569 } 570 571 record := np.AsRecord() 572 record.Rounds = op.AsRecord().Rounds 573 record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 574 CreatedAt: time.Now().Format(time.RFC3339), 575 PatchBlob: blob.Blob, 576 }) 577 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 578 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 579 Collection: tangled.RepoPullNSID, 580 Rkey: op.Rkey, 581 Value: compat113.Pull(&record), 582 }, 583 }) 584 } 585 586 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 587 Repo: userDid.String(), 588 Writes: writes, 589 }) 590 if err != nil { 591 l.Error("failed to apply writes for stacked pull request", "err", err, "writes_count", len(writes)) 592 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 593 return 594 } 595 596 err = tx.Commit() 597 if err != nil { 598 l.Error("failed to commit resubmit transaction", "err", err) 599 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 600 return 601 } 602 603 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 604 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 605}