Monorepo for Tangled tangled.org
5

Configure Feed

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

at icy/yovxsu 16 kB View raw
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/appview/xrpcclient" 20 "tangled.org/core/patchutil" 21 "tangled.org/core/tid" 22 "tangled.org/core/types" 23 "tangled.org/core/xrpc" 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 74 if err := s.validator.ValidatePatch(&patch); err != nil { 75 s.logger.Error("failed to validate patch", "err", err) 76 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 77 return 78 } 79 80 pullSource := &models.PullSource{ 81 Branch: sourceBranch, 82 } 83 84 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 85} 86 87func (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) { 88 if err := s.validator.ValidatePatch(&patch); err != nil { 89 s.logger.Error("patch validation failed", "err", err) 90 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 91 return 92 } 93 94 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, "", "", nil, isStacked, stackTitles, stackBodies) 95} 96 97func (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) { 98 l := s.logger.With("handler", "handleForkBasedPull", "user", userDid, "fork_repo_did", forkRepoDid, "target_branch", targetBranch, "source_branch", sourceBranch, "is_stacked", isStacked) 99 100 if forkRepoDid == "" { 101 s.pages.Notice(w, "pull", "No such fork.") 102 return 103 } 104 fork, err := db.GetForkByRepoDid(s.db, forkRepoDid) 105 if errors.Is(err, sql.ErrNoRows) { 106 s.pages.Notice(w, "pull", "No such fork.") 107 return 108 } else if err != nil { 109 l.Error("failed to fetch fork", "err", err, "fork_repo_did", forkRepoDid) 110 s.pages.Notice(w, "pull", "Failed to fetch fork.") 111 return 112 } 113 114 client, err := s.oauth.ServiceClient( 115 r, 116 oauth.WithService(fork.Knot), 117 oauth.WithLxm(tangled.RepoHiddenRefNSID), 118 oauth.WithDev(s.config.Core.Dev), 119 ) 120 121 resp, err := tangled.RepoHiddenRef( 122 r.Context(), 123 client, 124 &tangled.RepoHiddenRef_Input{ 125 ForkRef: sourceBranch, 126 RemoteRef: targetBranch, 127 Repo: fork.RepoAt().String(), 128 }, 129 ) 130 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 131 s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 132 s.pages.Notice(w, "pull", xrpcerr.Error()) 133 return 134 } 135 136 if !resp.Success { 137 errorMsg := "Failed to create pull request" 138 if resp.Error != nil { 139 errorMsg = fmt.Sprintf("Failed to create pull request: %s", *resp.Error) 140 } 141 s.pages.Notice(w, "pull", errorMsg) 142 return 143 } 144 145 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 146 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 147 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 148 // hiddenRef: hidden/feature-1/main (on repo-fork) 149 // targetBranch: main (on repo-1) 150 // sourceBranch: feature-1 (on repo-fork) 151 forkXrpcc := s.knotClient(fork.Knot) 152 153 forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 154 if err != nil { 155 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 156 l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef) 157 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 158 return 159 } 160 l.Error("failed to compare across branches", "err", err, "hidden_ref", hiddenRef) 161 s.pages.Notice(w, "pull", err.Error()) 162 return 163 } 164 165 var comparison types.RepoFormatPatchResponse 166 if err := json.Unmarshal(forkXrpcBytes, &comparison); err != nil { 167 l.Error("failed to decode XRPC compare response for fork", "err", err) 168 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 169 return 170 } 171 172 if len(comparison.FormatPatch) == 0 { 173 s.pages.Notice(w, "pull", "No commits between target and source.") 174 return 175 } 176 177 sourceRev := comparison.Rev2 178 patch := comparison.FormatPatchRaw 179 combined := comparison.CombinedPatchRaw 180 181 if err := s.validator.ValidatePatch(&patch); err != nil { 182 s.logger.Error("failed to validate patch", "err", err) 183 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 184 return 185 } 186 187 forkDid := syntax.DID(fork.RepoDid) 188 pullSource := &models.PullSource{ 189 Branch: sourceBranch, 190 RepoDid: &forkDid, 191 } 192 193 s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 194} 195 196func (s *Pulls) createPullRequest( 197 w http.ResponseWriter, 198 r *http.Request, 199 repo *models.Repo, 200 userDid syntax.DID, 201 title, body, targetBranch string, 202 patch string, 203 combined string, 204 sourceRev string, 205 pullSource *models.PullSource, 206 isStacked bool, 207 stackTitles, stackBodies map[string]string, 208) { 209 l := s.logger.With("handler", "createPullRequest", "user", userDid, "target_branch", targetBranch, "is_stacked", isStacked) 210 211 if isStacked { 212 // creates a series of PRs, each linking to the previous, identified by jj's change-id 213 s.createStackedPullRequest( 214 w, 215 r, 216 repo, 217 userDid, 218 targetBranch, 219 patch, 220 sourceRev, 221 pullSource, 222 stackTitles, 223 stackBodies, 224 ) 225 return 226 } 227 228 client, err := s.oauth.AuthorizedClient(r) 229 if err != nil { 230 l.Error("failed to get authorized client", "err", err) 231 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 232 return 233 } 234 235 tx, err := s.db.BeginTx(r.Context(), nil) 236 if err != nil { 237 l.Error("failed to start tx", "err", err) 238 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 239 return 240 } 241 defer tx.Rollback() 242 243 // We've already checked earlier if it's diff-based and title is empty, 244 // so if it's still empty now, it's intentionally skipped owing to format-patch. 245 if title == "" || body == "" { 246 formatPatches, err := patchutil.ExtractPatches(patch) 247 if err != nil { 248 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 249 return 250 } 251 if len(formatPatches) == 0 { 252 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 253 return 254 } 255 256 if title == "" { 257 title = formatPatches[0].Title 258 } 259 if body == "" { 260 body = formatPatches[0].Body 261 } 262 } 263 264 mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 265 266 rkey := tid.TID() 267 268 blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 269 if err != nil { 270 l.Error("failed to upload patch", "err", err) 271 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 272 return 273 } 274 275 now := time.Now() 276 277 pull := &models.Pull{ 278 Title: title, 279 Body: body, 280 TargetBranch: targetBranch, 281 OwnerDid: userDid.String(), 282 RepoDid: syntax.DID(repo.RepoDid), 283 Rkey: rkey, 284 Mentions: mentions, 285 References: references, 286 Submissions: []*models.PullSubmission{ 287 { 288 Patch: patch, 289 Combined: combined, 290 SourceRev: sourceRev, 291 Blob: *blob.Blob, 292 Created: now, 293 }, 294 }, 295 PullSource: pullSource, 296 State: models.PullOpen, 297 Created: now, 298 Repo: repo, 299 } 300 301 record := pull.AsRecord() 302 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 303 Collection: tangled.RepoPullNSID, 304 Repo: userDid.String(), 305 Rkey: rkey, 306 Record: knotcompat.Pull(&record), 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 http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId), http.StatusFound) 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: knotcompat.Pull(&record), 407 }, 408 }) 409 } 410 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 411 Repo: userDid.String(), 412 Writes: writes, 413 }) 414 if err != nil { 415 l.Error("failed to create stacked pull request", "err", err) 416 s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 417 return 418 } 419 420 // create all pulls at once 421 tx, err := s.db.BeginTx(r.Context(), nil) 422 if err != nil { 423 l.Error("failed to start tx", "err", err) 424 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 425 return 426 } 427 defer tx.Rollback() 428 429 for _, p := range stack { 430 err = db.PutPull(tx, p) 431 if err != nil { 432 l.Error("failed to create pull request in database", "err", err, "pull_rkey", p.Rkey) 433 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 434 return 435 } 436 437 } 438 439 if err = tx.Commit(); err != nil { 440 l.Error("failed to commit transaction for pull requests", "err", err) 441 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 442 return 443 } 444 445 // notify about each pull 446 // 447 // this is performed after tx.Commit, because it could result in a locked DB otherwise 448 for _, p := range stack { 449 s.notifier.NewPull(r.Context(), p) 450 } 451 452 s.applyCreationLabels(r.Context(), client, userDid, stack, r.Form, repo) 453 454 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 455 http.Redirect(w, r, fmt.Sprintf("/%s/pulls", ownerSlashRepo), http.StatusFound) 456} 457 458func (s *Pulls) newStack( 459 ctx context.Context, 460 repo *models.Repo, 461 userDid syntax.DID, 462 targetBranch string, 463 pullSource *models.PullSource, 464 formatPatches []types.FormatPatch, 465 blobs []*lexutil.LexBlob, 466 stackTitles, stackBodies map[string]string, 467) (models.Stack, error) { 468 var stack models.Stack 469 var parentAtUri *syntax.ATURI 470 for i, fp := range formatPatches { 471 // all patches must have a jj change-id 472 cid, err := fp.ChangeId() 473 if err != nil { 474 return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 475 } 476 477 title := fp.Title 478 body := fp.Body 479 if override, ok := stackTitles[cid]; ok && strings.TrimSpace(override) != "" { 480 title = override 481 } 482 if override, ok := stackBodies[cid]; ok { 483 body = override 484 } 485 rkey := tid.TID() 486 487 mentions, references := s.mentionsResolver.Resolve(ctx, body) 488 489 now := time.Now() 490 491 pull := models.Pull{ 492 Title: title, 493 Body: body, 494 TargetBranch: targetBranch, 495 OwnerDid: userDid.String(), 496 RepoDid: syntax.DID(repo.RepoDid), 497 Rkey: rkey, 498 Mentions: mentions, 499 References: references, 500 Submissions: []*models.PullSubmission{ 501 { 502 Patch: fp.Raw, 503 SourceRev: fp.SHA, 504 Combined: fp.Raw, 505 Blob: *blobs[i], 506 Created: now, 507 }, 508 }, 509 PullSource: pullSource, 510 Created: now, 511 State: models.PullOpen, 512 513 DependentOn: parentAtUri, 514 Repo: repo, 515 } 516 517 stack = append(stack, &pull) 518 519 parent := pull.AtUri() 520 parentAtUri = &parent 521 } 522 523 return stack, nil 524}