This repository has no description
0

Configure Feed

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

core / appview / pulls / create.go
16 kB 526 lines
1package pulls 2 3import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "strings" 11 "time" 12 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/appview/db" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/appview/oauth" 17 "tangled.org/core/appview/reporesolver" 18 "tangled.org/core/appview/xrpcclient" 19 "tangled.org/core/patchutil" 20 "tangled.org/core/tid" 21 "tangled.org/core/types" 22 "tangled.org/core/xrpc" 23 24 comatproto "github.com/bluesky-social/indigo/api/atproto" 25 "github.com/bluesky-social/indigo/atproto/syntax" 26 lexutil "github.com/bluesky-social/indigo/lex/util" 27) 28 29func (s *Pulls) handleBranchBasedPull( 30 w http.ResponseWriter, 31 r *http.Request, 32 repo *models.Repo, 33 userDid syntax.DID, 34 title, 35 body, 36 targetBranch, 37 sourceBranch string, 38 isStacked bool, 39 stackTitles, stackBodies map[string]string, 40) { 41 l := s.logger.With("handler", "handleBranchBasedPull", "user", userDid, "target_branch", targetBranch, "source_branch", sourceBranch, "is_stacked", isStacked) 42 43 xrpcc := s.knotClient(repo.Knot) 44 45 xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch) 46 if err != nil { 47 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 48 l.Error("failed to call XRPC repo.compare", "xrpcerr", xrpcerr, "err", err) 49 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 50 return 51 } 52 l.Error("failed to compare", "err", err) 53 s.pages.Notice(w, "pull", err.Error()) 54 return 55 } 56 57 var comparison types.RepoFormatPatchResponse 58 if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 59 l.Error("failed to decode XRPC compare response", "err", err) 60 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 61 return 62 } 63 64 if len(comparison.FormatPatch) == 0 { 65 s.pages.Notice(w, "pull", "No commits between target and source.") 66 return 67 } 68 69 sourceRev := comparison.Rev2 70 patch := comparison.FormatPatchRaw 71 combined := comparison.CombinedPatchRaw 72 73 if err := s.validator.ValidatePatch(&patch); err != nil { 74 s.logger.Error("failed to validate patch", "err", err) 75 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 76 return 77 } 78 79 pullSource := &models.PullSource{ 80 Branch: sourceBranch, 81 } 82 83 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 84} 85 86func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, userDid syntax.DID, title, body, targetBranch, patch string, isStacked bool, stackTitles, stackBodies map[string]string) { 87 if err := s.validator.ValidatePatch(&patch); err != nil { 88 s.logger.Error("patch validation failed", "err", err) 89 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 90 return 91 } 92 93 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, "", "", nil, isStacked, stackTitles, stackBodies) 94} 95 96func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, userDid syntax.DID, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool, stackTitles, stackBodies map[string]string) { 97 l := s.logger.With("handler", "handleForkBasedPull", "user", userDid, "fork_repo", forkRepo, "target_branch", targetBranch, "source_branch", sourceBranch, "is_stacked", isStacked) 98 99 repoString := strings.SplitN(forkRepo, "/", 2) 100 forkOwnerDid := repoString[0] 101 forkRkey := strings.ToLower(repoString[1]) 102 fork, err := db.GetForkByDid(s.db, forkOwnerDid, forkRkey) 103 if errors.Is(err, sql.ErrNoRows) { 104 s.pages.Notice(w, "pull", "No such fork.") 105 return 106 } else if err != nil { 107 l.Error("failed to fetch fork", "err", err, "fork_owner_did", forkOwnerDid, "fork_rkey", forkRkey) 108 s.pages.Notice(w, "pull", "Failed to fetch fork.") 109 return 110 } 111 112 client, err := s.oauth.ServiceClient( 113 r, 114 oauth.WithService(fork.Knot), 115 oauth.WithLxm(tangled.RepoHiddenRefNSID), 116 oauth.WithDev(s.config.Core.Dev), 117 ) 118 119 resp, err := tangled.RepoHiddenRef( 120 r.Context(), 121 client, 122 &tangled.RepoHiddenRef_Input{ 123 ForkRef: sourceBranch, 124 RemoteRef: targetBranch, 125 Repo: fork.RepoAt().String(), 126 }, 127 ) 128 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 129 s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 130 s.pages.Notice(w, "pull", xrpcerr.Error()) 131 return 132 } 133 134 if !resp.Success { 135 errorMsg := "Failed to create pull request" 136 if resp.Error != nil { 137 errorMsg = fmt.Sprintf("Failed to create pull request: %s", *resp.Error) 138 } 139 s.pages.Notice(w, "pull", errorMsg) 140 return 141 } 142 143 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 144 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 145 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 146 // hiddenRef: hidden/feature-1/main (on repo-fork) 147 // targetBranch: main (on repo-1) 148 // sourceBranch: feature-1 (on repo-fork) 149 forkXrpcc := s.knotClient(fork.Knot) 150 151 forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 152 if err != nil { 153 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 154 l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef) 155 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 156 return 157 } 158 l.Error("failed to compare across branches", "err", err, "hidden_ref", hiddenRef) 159 s.pages.Notice(w, "pull", err.Error()) 160 return 161 } 162 163 var comparison types.RepoFormatPatchResponse 164 if err := json.Unmarshal(forkXrpcBytes, &comparison); err != nil { 165 l.Error("failed to decode XRPC compare response for fork", "err", err) 166 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 167 return 168 } 169 170 if len(comparison.FormatPatch) == 0 { 171 s.pages.Notice(w, "pull", "No commits between target and source.") 172 return 173 } 174 175 sourceRev := comparison.Rev2 176 patch := comparison.FormatPatchRaw 177 combined := comparison.CombinedPatchRaw 178 179 if err := s.validator.ValidatePatch(&patch); err != nil { 180 s.logger.Error("failed to validate patch", "err", err) 181 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 182 return 183 } 184 185 forkDid := syntax.DID(fork.RepoDid) 186 pullSource := &models.PullSource{ 187 Branch: sourceBranch, 188 RepoDid: &forkDid, 189 } 190 191 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 192} 193 194func (s *Pulls) createPullRequest( 195 w http.ResponseWriter, 196 r *http.Request, 197 repo *models.Repo, 198 userDid syntax.DID, 199 title, body, targetBranch string, 200 patch string, 201 combined string, 202 sourceRev string, 203 pullSource *models.PullSource, 204 isStacked bool, 205 stackTitles, stackBodies map[string]string, 206) { 207 l := s.logger.With("handler", "createPullRequest", "user", userDid, "target_branch", targetBranch, "is_stacked", isStacked) 208 209 if isStacked { 210 // creates a series of PRs, each linking to the previous, identified by jj's change-id 211 s.createStackedPullRequest( 212 w, 213 r, 214 repo, 215 userDid, 216 targetBranch, 217 patch, 218 sourceRev, 219 pullSource, 220 stackTitles, 221 stackBodies, 222 ) 223 return 224 } 225 226 client, err := s.oauth.AuthorizedClient(r) 227 if err != nil { 228 l.Error("failed to get authorized client", "err", err) 229 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 230 return 231 } 232 233 tx, err := s.db.BeginTx(r.Context(), nil) 234 if err != nil { 235 l.Error("failed to start tx", "err", err) 236 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 237 return 238 } 239 defer tx.Rollback() 240 241 // We've already checked earlier if it's diff-based and title is empty, 242 // so if it's still empty now, it's intentionally skipped owing to format-patch. 243 if title == "" || body == "" { 244 formatPatches, err := patchutil.ExtractPatches(patch) 245 if err != nil { 246 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 247 return 248 } 249 if len(formatPatches) == 0 { 250 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 251 return 252 } 253 254 if title == "" { 255 title = formatPatches[0].Title 256 } 257 if body == "" { 258 body = formatPatches[0].Body 259 } 260 } 261 262 mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 263 264 rkey := tid.TID() 265 266 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 267 if err != nil { 268 l.Error("failed to upload patch", "err", err) 269 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 270 return 271 } 272 273 now := time.Now() 274 275 pull := &models.Pull{ 276 Title: title, 277 Body: body, 278 TargetBranch: targetBranch, 279 OwnerDid: userDid.String(), 280 RepoDid: syntax.DID(repo.RepoDid), 281 Rkey: rkey, 282 Mentions: mentions, 283 References: references, 284 Submissions: []*models.PullSubmission{ 285 { 286 Patch: patch, 287 Combined: combined, 288 SourceRev: sourceRev, 289 Blob: *blob.Blob, 290 Created: now, 291 }, 292 }, 293 PullSource: pullSource, 294 State: models.PullOpen, 295 Created: now, 296 Repo: repo, 297 } 298 299 record := pull.AsRecord() 300 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 301 Collection: tangled.RepoPullNSID, 302 Repo: userDid.String(), 303 Rkey: rkey, 304 Record: &lexutil.LexiconTypeDecoder{ 305 Val: &record, 306 }, 307 }) 308 if err != nil { 309 l.Error("failed to create pull request", "err", err) 310 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 311 return 312 } 313 314 err = db.PutPull(tx, pull) 315 if err != nil { 316 l.Error("failed to create pull request in database", "err", err) 317 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 318 return 319 } 320 pullId, err := db.NextPullId(tx, repo.RepoDid) 321 if err != nil { 322 s.logger.Error("failed to get pull id", "err", err) 323 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 324 return 325 } 326 327 if err = tx.Commit(); err != nil { 328 l.Error("failed to commit transaction for pull request", "err", err) 329 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 330 return 331 } 332 333 s.notifier.NewPull(r.Context(), pull) 334 335 s.applyCreationLabels(r.Context(), client, userDid, []*models.Pull{pull}, r.Form, repo) 336 337 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 338 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId)) 339} 340 341func (s *Pulls) createStackedPullRequest( 342 w http.ResponseWriter, 343 r *http.Request, 344 repo *models.Repo, 345 userDid syntax.DID, 346 targetBranch string, 347 patch string, 348 sourceRev string, 349 pullSource *models.PullSource, 350 stackTitles, stackBodies map[string]string, 351) { 352 l := s.logger.With("handler", "createStackedPullRequest", "user", userDid, "target_branch", targetBranch, "source_rev", sourceRev) 353 354 // run some necessary checks for stacked-prs first 355 356 formatPatches, err := patchutil.ExtractPatches(patch) 357 if err != nil { 358 l.Error("failed to extract patches", "err", err) 359 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 360 return 361 } 362 363 // must have atleast 1 patch to begin with 364 if len(formatPatches) == 0 { 365 l.Error("empty patches") 366 s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 367 return 368 } 369 370 client, err := s.oauth.AuthorizedClient(r) 371 if err != nil { 372 l.Error("failed to get authorized client", "err", err) 373 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 374 return 375 } 376 377 // first upload all blobs 378 blobs := make([]*lexutil.LexBlob, len(formatPatches)) 379 for i, p := range formatPatches { 380 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 381 if err != nil { 382 l.Error("failed to upload patch blob", "err", err, "patch_index", i) 383 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 384 return 385 } 386 l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 387 blobs[i] = blob.Blob 388 } 389 390 // build a stack out of this patch 391 stack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pullSource, formatPatches, blobs, stackTitles, stackBodies) 392 if err != nil { 393 l.Error("failed to create stack", "err", err) 394 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 395 return 396 } 397 398 // apply all record creations at once 399 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 400 for _, p := range stack { 401 record := p.AsRecord() 402 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 403 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 404 Collection: tangled.RepoPullNSID, 405 Rkey: &p.Rkey, 406 Value: &lexutil.LexiconTypeDecoder{ 407 Val: &record, 408 }, 409 }, 410 }) 411 } 412 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 413 Repo: userDid.String(), 414 Writes: writes, 415 }) 416 if err != nil { 417 l.Error("failed to create stacked pull request", "err", err) 418 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 419 return 420 } 421 422 // create all pulls at once 423 tx, err := s.db.BeginTx(r.Context(), nil) 424 if err != nil { 425 l.Error("failed to start tx", "err", err) 426 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 427 return 428 } 429 defer tx.Rollback() 430 431 for _, p := range stack { 432 err = db.PutPull(tx, p) 433 if err != nil { 434 l.Error("failed to create pull request in database", "err", err, "pull_rkey", p.Rkey) 435 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 436 return 437 } 438 439 } 440 441 if err = tx.Commit(); err != nil { 442 l.Error("failed to commit transaction for pull requests", "err", err) 443 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 444 return 445 } 446 447 // notify about each pull 448 // 449 // this is performed after tx.Commit, because it could result in a locked DB otherwise 450 for _, p := range stack { 451 s.notifier.NewPull(r.Context(), p) 452 } 453 454 s.applyCreationLabels(r.Context(), client, userDid, stack, r.Form, repo) 455 456 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 457 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo)) 458} 459 460func (s *Pulls) newStack( 461 ctx context.Context, 462 repo *models.Repo, 463 userDid syntax.DID, 464 targetBranch string, 465 pullSource *models.PullSource, 466 formatPatches []types.FormatPatch, 467 blobs []*lexutil.LexBlob, 468 stackTitles, stackBodies map[string]string, 469) (models.Stack, error) { 470 var stack models.Stack 471 var parentAtUri *syntax.ATURI 472 for i, fp := range formatPatches { 473 // all patches must have a jj change-id 474 cid, err := fp.ChangeId() 475 if err != nil { 476 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 477 } 478 479 title := fp.Title 480 body := fp.Body 481 if override, ok := stackTitles[cid]; ok && strings.TrimSpace(override) != "" { 482 title = override 483 } 484 if override, ok := stackBodies[cid]; ok { 485 body = override 486 } 487 rkey := tid.TID() 488 489 mentions, references := s.mentionsResolver.Resolve(ctx, body) 490 491 now := time.Now() 492 493 pull := models.Pull{ 494 Title: title, 495 Body: body, 496 TargetBranch: targetBranch, 497 OwnerDid: userDid.String(), 498 RepoDid: syntax.DID(repo.RepoDid), 499 Rkey: rkey, 500 Mentions: mentions, 501 References: references, 502 Submissions: []*models.PullSubmission{ 503 { 504 Patch: fp.Raw, 505 SourceRev: fp.SHA, 506 Combined: fp.Raw, 507 Blob: *blobs[i], 508 Created: now, 509 }, 510 }, 511 PullSource: pullSource, 512 Created: now, 513 State: models.PullOpen, 514 515 DependentOn: parentAtUri, 516 Repo: repo, 517 } 518 519 stack = append(stack, &pull) 520 521 parent := pull.AtUri() 522 parentAtUri = &parent 523 } 524 525 return stack, nil 526}