Monorepo for Tangled tangled.org
10

Configure Feed

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

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 repoName := repoString[1] 102 fork, err := db.GetForkByDid(s.db, forkOwnerDid, repoName) 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, "repo_name", repoName) 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 forkAtUri := fork.RepoAt() 186 var forkDid *syntax.DID 187 if fork.RepoDid != "" { 188 forkDid = new(syntax.DID) 189 *forkDid = syntax.DID(fork.RepoDid) 190 } 191 192 pullSource := &models.PullSource{ 193 Branch: sourceBranch, 194 RepoAt: &forkAtUri, 195 RepoDid: forkDid, 196 } 197 198 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 199} 200 201func (s *Pulls) createPullRequest( 202 w http.ResponseWriter, 203 r *http.Request, 204 repo *models.Repo, 205 userDid syntax.DID, 206 title, body, targetBranch string, 207 patch string, 208 combined string, 209 sourceRev string, 210 pullSource *models.PullSource, 211 isStacked bool, 212 stackTitles, stackBodies map[string]string, 213) { 214 l := s.logger.With("handler", "createPullRequest", "user", userDid, "target_branch", targetBranch, "is_stacked", isStacked) 215 216 if isStacked { 217 // creates a series of PRs, each linking to the previous, identified by jj's change-id 218 s.createStackedPullRequest( 219 w, 220 r, 221 repo, 222 userDid, 223 targetBranch, 224 patch, 225 sourceRev, 226 pullSource, 227 stackTitles, 228 stackBodies, 229 ) 230 return 231 } 232 233 client, err := s.oauth.AuthorizedClient(r) 234 if err != nil { 235 l.Error("failed to get authorized client", "err", err) 236 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 237 return 238 } 239 240 tx, err := s.db.BeginTx(r.Context(), nil) 241 if err != nil { 242 l.Error("failed to start tx", "err", err) 243 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 244 return 245 } 246 defer tx.Rollback() 247 248 // We've already checked earlier if it's diff-based and title is empty, 249 // so if it's still empty now, it's intentionally skipped owing to format-patch. 250 if title == "" || body == "" { 251 formatPatches, err := patchutil.ExtractPatches(patch) 252 if err != nil { 253 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 254 return 255 } 256 if len(formatPatches) == 0 { 257 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 258 return 259 } 260 261 if title == "" { 262 title = formatPatches[0].Title 263 } 264 if body == "" { 265 body = formatPatches[0].Body 266 } 267 } 268 269 mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 270 271 rkey := tid.TID() 272 273 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 274 if err != nil { 275 l.Error("failed to upload patch", "err", err) 276 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 277 return 278 } 279 280 now := time.Now() 281 282 pull := &models.Pull{ 283 Title: title, 284 Body: body, 285 TargetBranch: targetBranch, 286 OwnerDid: userDid.String(), 287 RepoAt: repo.RepoAt(), 288 Rkey: rkey, 289 Mentions: mentions, 290 References: references, 291 Submissions: []*models.PullSubmission{ 292 { 293 Patch: patch, 294 Combined: combined, 295 SourceRev: sourceRev, 296 Blob: *blob.Blob, 297 Created: now, 298 }, 299 }, 300 PullSource: pullSource, 301 State: models.PullOpen, 302 Created: now, 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: &lexutil.LexiconTypeDecoder{ 311 Val: &record, 312 }, 313 }) 314 if err != nil { 315 l.Error("failed to create pull request", "err", err) 316 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 317 return 318 } 319 320 err = db.PutPull(tx, pull) 321 if err != nil { 322 l.Error("failed to create pull request in database", "err", err) 323 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 324 return 325 } 326 pullId, err := db.NextPullId(tx, repo.RepoAt()) 327 if err != nil { 328 s.logger.Error("failed to get pull id", "err", err) 329 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 330 return 331 } 332 333 if err = tx.Commit(); err != nil { 334 l.Error("failed to commit transaction for pull request", "err", err) 335 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 336 return 337 } 338 339 s.notifier.NewPull(r.Context(), pull) 340 341 s.applyCreationLabels(r.Context(), client, userDid, []*models.Pull{pull}, r.Form, repo) 342 343 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 344 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId)) 345} 346 347func (s *Pulls) createStackedPullRequest( 348 w http.ResponseWriter, 349 r *http.Request, 350 repo *models.Repo, 351 userDid syntax.DID, 352 targetBranch string, 353 patch string, 354 sourceRev string, 355 pullSource *models.PullSource, 356 stackTitles, stackBodies map[string]string, 357) { 358 l := s.logger.With("handler", "createStackedPullRequest", "user", userDid, "target_branch", targetBranch, "source_rev", sourceRev) 359 360 // run some necessary checks for stacked-prs first 361 362 formatPatches, err := patchutil.ExtractPatches(patch) 363 if err != nil { 364 l.Error("failed to extract patches", "err", err) 365 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 366 return 367 } 368 369 // must have atleast 1 patch to begin with 370 if len(formatPatches) == 0 { 371 l.Error("empty patches") 372 s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 373 return 374 } 375 376 client, err := s.oauth.AuthorizedClient(r) 377 if err != nil { 378 l.Error("failed to get authorized client", "err", err) 379 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 380 return 381 } 382 383 // first upload all blobs 384 blobs := make([]*lexutil.LexBlob, len(formatPatches)) 385 for i, p := range formatPatches { 386 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 387 if err != nil { 388 l.Error("failed to upload patch blob", "err", err, "patch_index", i) 389 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 390 return 391 } 392 l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 393 blobs[i] = blob.Blob 394 } 395 396 // build a stack out of this patch 397 stack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pullSource, formatPatches, blobs, stackTitles, stackBodies) 398 if err != nil { 399 l.Error("failed to create stack", "err", err) 400 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 401 return 402 } 403 404 // apply all record creations at once 405 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 406 for _, p := range stack { 407 record := p.AsRecord() 408 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 409 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 410 Collection: tangled.RepoPullNSID, 411 Rkey: &p.Rkey, 412 Value: &lexutil.LexiconTypeDecoder{ 413 Val: &record, 414 }, 415 }, 416 }) 417 } 418 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 419 Repo: userDid.String(), 420 Writes: writes, 421 }) 422 if err != nil { 423 l.Error("failed to create stacked pull request", "err", err) 424 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 425 return 426 } 427 428 // create all pulls at once 429 tx, err := s.db.BeginTx(r.Context(), nil) 430 if err != nil { 431 l.Error("failed to start tx", "err", err) 432 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 433 return 434 } 435 defer tx.Rollback() 436 437 for _, p := range stack { 438 err = db.PutPull(tx, p) 439 if err != nil { 440 l.Error("failed to create pull request in database", "err", err, "pull_rkey", p.Rkey) 441 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 442 return 443 } 444 445 } 446 447 if err = tx.Commit(); err != nil { 448 l.Error("failed to commit transaction for pull requests", "err", err) 449 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 450 return 451 } 452 453 // notify about each pull 454 // 455 // this is performed after tx.Commit, because it could result in a locked DB otherwise 456 for _, p := range stack { 457 s.notifier.NewPull(r.Context(), p) 458 } 459 460 s.applyCreationLabels(r.Context(), client, userDid, stack, r.Form, repo) 461 462 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 463 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo)) 464} 465 466func (s *Pulls) newStack( 467 ctx context.Context, 468 repo *models.Repo, 469 userDid syntax.DID, 470 targetBranch string, 471 pullSource *models.PullSource, 472 formatPatches []types.FormatPatch, 473 blobs []*lexutil.LexBlob, 474 stackTitles, stackBodies map[string]string, 475) (models.Stack, error) { 476 var stack models.Stack 477 var parentAtUri *syntax.ATURI 478 for i, fp := range formatPatches { 479 // all patches must have a jj change-id 480 cid, err := fp.ChangeId() 481 if err != nil { 482 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 483 } 484 485 title := fp.Title 486 body := fp.Body 487 if override, ok := stackTitles[cid]; ok && strings.TrimSpace(override) != "" { 488 title = override 489 } 490 if override, ok := stackBodies[cid]; ok { 491 body = override 492 } 493 rkey := tid.TID() 494 495 mentions, references := s.mentionsResolver.Resolve(ctx, body) 496 497 now := time.Now() 498 499 pull := models.Pull{ 500 Title: title, 501 Body: body, 502 TargetBranch: targetBranch, 503 OwnerDid: userDid.String(), 504 RepoAt: repo.RepoAt(), 505 Rkey: rkey, 506 Mentions: mentions, 507 References: references, 508 Submissions: []*models.PullSubmission{ 509 { 510 Patch: fp.Raw, 511 SourceRev: fp.SHA, 512 Combined: fp.Raw, 513 Blob: *blobs[i], 514 Created: now, 515 }, 516 }, 517 PullSource: pullSource, 518 Created: now, 519 State: models.PullOpen, 520 521 DependentOn: parentAtUri, 522 Repo: repo, 523 } 524 525 stack = append(stack, &pull) 526 527 parent := pull.AtUri() 528 parentAtUri = &parent 529 } 530 531 return stack, nil 532}