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