Monorepo for Tangled tangled.org
6

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