Monorepo for Tangled tangled.org
856
fork

Configure Feed

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

appview: handlers, state, and templates for more record types w/ repoDID #279

open opened by oyster.cafe targeting master from lt/repo-rename-by-rkey

Lewis: May this revision serve well! lewis@tangled.org

appview,knotserver: validate git repo ownership according to knot

Lewis: May this revision serve well! lewis@tangled.org

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mjm6w2jhaa22
-3031
Interdiff #3 #4
appview/db/notifications.go

This file has not been changed.

appview/issues/issues.go

This file has not been changed.

appview/labels/labels.go

This file has not been changed.

appview/middleware/middleware.go

This file has not been changed.

appview/models/repo.go

This file has not been changed.

appview/pages/repoinfo/repoinfo.go

This file has not been changed.

appview/pages/templates/goodfirstissues/index.html

This file has not been changed.

appview/pages/templates/knots/dashboard.html

This file has not been changed.

appview/pages/templates/layouts/repobase.html

This file has not been changed.

appview/pages/templates/notifications/fragments/item.html

This file has not been changed.

appview/pages/templates/repo/empty.html

This file has not been changed.

appview/pages/templates/repo/fragments/cloneDropdown.html

This file has not been changed.

appview/pages/templates/repo/index.html

This file has not been changed.

appview/pages/templates/repo/pulls/fragments/pullActions.html

This file has not been changed.

appview/pages/templates/repo/pulls/fragments/pullHeader.html

This file has not been changed.

appview/pages/templates/repo/pulls/pull.html

This file has not been changed.

appview/pages/templates/repo/settings/hooks.html

This file has not been changed.

appview/pages/templates/repo/settings/sites.html

This file has not been changed.

appview/pages/templates/spindles/dashboard.html

This file has not been changed.

appview/pages/templates/timeline/fragments/preview.html

This file has not been changed.

appview/pages/templates/timeline/fragments/timeline.html

This file has not been changed.

appview/pages/templates/user/fragments/repoCard.html

This file has not been changed.

appview/pages/templates/user/overview.html

This file has not been changed.

appview/pipelines/pipelines.go

This file has not been changed.

-3031
appview/pulls/pulls.go
··· 3 3 import ( 4 4 "bytes" 5 5 "compress/gzip" 6 - "context" 7 - "database/sql" 8 - "encoding/json" 9 - "errors" 10 6 "fmt" 11 7 "io" 12 - "iter" 13 8 "log/slog" 14 - "net/http" 15 - "net/url" 16 - "slices" 17 - "sort" 18 - "strconv" 19 - "strings" 20 - "time" 21 9 22 - "tangled.org/core/api/tangled" 23 10 "tangled.org/core/appview/config" 24 11 "tangled.org/core/appview/db" 25 12 pulls_indexer "tangled.org/core/appview/indexer/pulls" ··· 28 15 "tangled.org/core/appview/notify" 29 16 "tangled.org/core/appview/oauth" 30 17 "tangled.org/core/appview/pages" 31 - "tangled.org/core/appview/pages/markup" 32 - "tangled.org/core/appview/pages/repoinfo" 33 - "tangled.org/core/appview/pagination" 34 18 "tangled.org/core/appview/reporesolver" 35 - "tangled.org/core/appview/searchquery" 36 19 "tangled.org/core/appview/validator" 37 - "tangled.org/core/appview/xrpcclient" 38 20 "tangled.org/core/idresolver" 39 21 "tangled.org/core/ogre" 40 - "tangled.org/core/orm" 41 - "tangled.org/core/patchutil" 42 22 "tangled.org/core/rbac" 43 - "tangled.org/core/tid" 44 - "tangled.org/core/types" 45 - "tangled.org/core/xrpc" 46 23 47 - comatproto "github.com/bluesky-social/indigo/api/atproto" 48 - "github.com/bluesky-social/indigo/atproto/atclient" 49 - "github.com/bluesky-social/indigo/atproto/syntax" 50 - lexutil "github.com/bluesky-social/indigo/lex/util" 51 24 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 52 - "github.com/go-chi/chi/v5" 53 25 ) 54 26 55 27 const ApplicationGzip = "application/gzip" ··· 109 81 return &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, host)} 110 82 } 111 83 112 - // htmx fragment 113 - func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 114 - l := s.logger.With("handler", "PullActions") 115 - 116 - switch r.Method { 117 - case http.MethodGet: 118 - user := s.oauth.GetMultiAccountUser(r) 119 - if user != nil { 120 - l = l.With("user", user.Did) 121 - } 122 - 123 - f, err := s.repoResolver.Resolve(r) 124 - if err != nil { 125 - l.Error("failed to get repo and knot", "err", err) 126 - return 127 - } 128 - 129 - pull, ok := r.Context().Value("pull").(*models.Pull) 130 - if !ok { 131 - l.Error("failed to get pull") 132 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 133 - return 134 - } 135 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 136 - 137 - // can be nil if this pull is not stacked 138 - stack, _ := r.Context().Value("stack").(models.Stack) 139 - 140 - roundNumberStr := chi.URLParam(r, "round") 141 - roundNumber, err := strconv.Atoi(roundNumberStr) 142 - if err != nil { 143 - roundNumber = pull.LastRoundNumber() 144 - } 145 - if roundNumber >= len(pull.Submissions) { 146 - http.Error(w, "bad round id", http.StatusBadRequest) 147 - l.Error("failed to parse round id", "err", err, "round_number", roundNumber) 148 - return 149 - } 150 - 151 - mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 152 - branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 153 - resubmitResult := pages.Unknown 154 - if user.Did == pull.OwnerDid { 155 - resubmitResult = s.resubmitCheck(r, f, pull, stack) 156 - } 157 - 158 - s.pages.PullActionsFragment(w, pages.PullActionsParams{ 159 - LoggedInUser: user, 160 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 161 - Pull: pull, 162 - RoundNumber: roundNumber, 163 - MergeCheck: mergeCheckResponse, 164 - ResubmitCheck: resubmitResult, 165 - BranchDeleteStatus: branchDeleteStatus, 166 - Stack: stack, 167 - }) 168 - return 169 - } 170 - } 171 - 172 - func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff bool) { 173 - l := s.logger.With("handler", "repoPullHelper", "interdiff", interdiff) 174 - 175 - user := s.oauth.GetMultiAccountUser(r) 176 - if user != nil { 177 - l = l.With("user", user.Did) 178 - } 179 - 180 - f, err := s.repoResolver.Resolve(r) 181 - if err != nil { 182 - l.Error("failed to get repo and knot", "err", err) 183 - return 184 - } 185 - 186 - pull, ok := r.Context().Value("pull").(*models.Pull) 187 - if !ok { 188 - l.Error("failed to get pull") 189 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 190 - return 191 - } 192 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 193 - 194 - backlinks, err := db.GetBacklinks(s.db, pull.AtUri()) 195 - if err != nil { 196 - l.Error("failed to get pull backlinks", "err", err) 197 - s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.") 198 - return 199 - } 200 - 201 - roundId := chi.URLParam(r, "round") 202 - roundIdInt := pull.LastRoundNumber() 203 - if r, err := strconv.Atoi(roundId); err == nil { 204 - roundIdInt = r 205 - } 206 - if roundIdInt >= len(pull.Submissions) { 207 - http.Error(w, "bad round id", http.StatusBadRequest) 208 - l.Error("failed to parse round id", "err", err, "round_number", roundIdInt) 209 - return 210 - } 211 - 212 - var diffOpts types.DiffOpts 213 - if d := r.URL.Query().Get("diff"); d == "split" { 214 - diffOpts.Split = true 215 - } 216 - 217 - // can be nil if this pull is not stacked 218 - stack, _ := r.Context().Value("stack").(models.Stack) 219 - 220 - mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 221 - branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 222 - resubmitResult := pages.Unknown 223 - if user != nil && user.Did == pull.OwnerDid { 224 - resubmitResult = s.resubmitCheck(r, f, pull, stack) 225 - } 226 - 227 - m := make(map[string]models.Pipeline) 228 - 229 - var shas []string 230 - for _, s := range pull.Submissions { 231 - shas = append(shas, s.SourceRev) 232 - } 233 - for _, p := range stack { 234 - shas = append(shas, p.LatestSha()) 235 - } 236 - 237 - ps, err := db.GetPipelineStatuses( 238 - s.db, 239 - len(shas), 240 - orm.FilterEq("p.repo_owner", f.Did), 241 - orm.FilterEq("p.repo_name", f.Rkey), 242 - orm.FilterEq("p.knot", f.Knot), 243 - orm.FilterIn("p.sha", shas), 244 - ) 245 - if err != nil { 246 - l.Error("failed to fetch pipeline statuses", "err", err) 247 - // non-fatal 248 - } 249 - 250 - for _, p := range ps { 251 - m[p.Sha] = p 252 - } 253 - 254 - reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 255 - if err != nil { 256 - l.Error("failed to get pull reactions", "err", err) 257 - } 258 - 259 - userReactions := map[models.ReactionKind]bool{} 260 - if user != nil { 261 - userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri()) 262 - } 263 - 264 - labelDefs, err := db.GetLabelDefinitions( 265 - s.db, 266 - orm.FilterIn("at_uri", f.Labels), 267 - orm.FilterContains("scope", tangled.RepoPullNSID), 268 - ) 269 - if err != nil { 270 - l.Error("failed to fetch labels", "err", err) 271 - s.pages.Error503(w) 272 - return 273 - } 274 - 275 - defs := make(map[string]*models.LabelDefinition) 276 - for _, l := range labelDefs { 277 - defs[l.AtUri().String()] = &l 278 - } 279 - 280 - vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) 281 - if user != nil { 282 - participants := pull.Participants() 283 - vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), participants) 284 - if err != nil { 285 - l.Error("failed to fetch vouch relationships", "err", err) 286 - } 287 - } 288 - 289 - patch := pull.Submissions[roundIdInt].CombinedPatch() 290 - var diff types.DiffRenderer 291 - diff = patchutil.AsNiceDiff(patch, pull.TargetBranch) 292 - 293 - if interdiff { 294 - currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 295 - if err != nil { 296 - l.Error("failed to interdiff; current patch malformed", "err", err, "round_number", roundIdInt) 297 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 298 - return 299 - } 300 - 301 - previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 302 - if err != nil { 303 - l.Error("failed to interdiff; previous patch malformed", "err", err, "round_number", roundIdInt) 304 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 305 - return 306 - } 307 - 308 - diff = patchutil.Interdiff(previousPatch, currentPatch) 309 - } 310 - 311 - err = s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 312 - LoggedInUser: user, 313 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 314 - Pull: pull, 315 - Stack: stack, 316 - Backlinks: backlinks, 317 - BranchDeleteStatus: branchDeleteStatus, 318 - MergeCheck: mergeCheckResponse, 319 - ResubmitCheck: resubmitResult, 320 - Pipelines: m, 321 - Diff: diff, 322 - DiffOpts: diffOpts, 323 - ActiveRound: roundIdInt, 324 - IsInterdiff: interdiff, 325 - 326 - Reactions: reactionMap, 327 - UserReacted: userReactions, 328 - 329 - LabelDefs: defs, 330 - VouchRelationships: vouchRelationships, 331 - }) 332 - if err != nil { 333 - l.Error("failed to render page", "err", err) 334 - } 335 - } 336 - 337 - func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 338 - l := s.logger.With("handler", "RepoSinglePull") 339 - 340 - pull, ok := r.Context().Value("pull").(*models.Pull) 341 - if !ok { 342 - l.Error("failed to get pull") 343 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 344 - return 345 - } 346 - 347 - http.Redirect(w, r, r.URL.String()+fmt.Sprintf("/round/%d", pull.LastRoundNumber()), http.StatusFound) 348 - } 349 - 350 - func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse { 351 - if pull.State == models.PullMerged { 352 - return types.MergeCheckResponse{} 353 - } 354 - 355 - xrpcc := s.knotClient(f.Knot) 356 - 357 - // combine patches of substack 358 - subStack := stack.Below(pull) 359 - // collect the portion of the stack that is mergeable 360 - mergeable := subStack.Mergeable() 361 - // combine each patch 362 - patch := mergeable.CombinedPatch() 363 - 364 - resp, err := tangled.RepoMergeCheck( 365 - r.Context(), 366 - xrpcc, 367 - &tangled.RepoMergeCheck_Input{ 368 - Did: f.Did, 369 - Name: f.Name, 370 - Branch: pull.TargetBranch, 371 - Patch: patch, 372 - }, 373 - ) 374 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 375 - s.logger.Error("failed to check for mergeability", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 376 - return types.MergeCheckResponse{ 377 - Error: fmt.Sprintf("failed to check merge status: %s", xrpcerr.Error()), 378 - } 379 - } 380 - 381 - return mergeCheckResponseFrom(resp) 382 - } 383 - 384 - func mergeCheckResponseFrom(resp *tangled.RepoMergeCheck_Output) types.MergeCheckResponse { 385 - conflicts := make([]types.ConflictInfo, len(resp.Conflicts)) 386 - for i, c := range resp.Conflicts { 387 - conflicts[i] = types.ConflictInfo{Filename: c.Filename, Reason: c.Reason} 388 - } 389 - out := types.MergeCheckResponse{ 390 - IsConflicted: resp.Is_conflicted, 391 - Conflicts: conflicts, 392 - } 393 - if resp.Message != nil { 394 - out.Message = *resp.Message 395 - } 396 - if resp.Error != nil { 397 - out.Error = *resp.Error 398 - } 399 - return out 400 - } 401 - 402 - func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *models.BranchDeleteStatus { 403 - if pull.State != models.PullMerged { 404 - return nil 405 - } 406 - 407 - user := s.oauth.GetMultiAccountUser(r) 408 - if user == nil { 409 - return nil 410 - } 411 - 412 - var branch string 413 - // check if the branch exists 414 - // NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates 415 - if pull.IsBranchBased() { 416 - branch = pull.PullSource.Branch 417 - } else if pull.IsForkBased() { 418 - branch = pull.PullSource.Branch 419 - repo = pull.PullSource.Repo 420 - } else { 421 - return nil 422 - } 423 - 424 - // deleted fork 425 - if repo == nil { 426 - return nil 427 - } 428 - 429 - // user can only delete branch if they are a collaborator in the repo that the branch belongs to 430 - perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.RepoIdentifier()) 431 - if !slices.Contains(perms, "repo:push") { 432 - return nil 433 - } 434 - 435 - xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 436 - resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoAt().String()) 437 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 438 - s.logger.Error("failed to get branch", "xrpcerr", xrpcerr, "err", err) 439 - return nil 440 - } 441 - 442 - return &models.BranchDeleteStatus{ 443 - Repo: repo, 444 - Branch: resp.Name, 445 - } 446 - } 447 - 448 - func (s *Pulls) resubmitCheck(r *http.Request, repo *models.Repo, pull *models.Pull, stack models.Stack) pages.ResubmitResult { 449 - if pull.State == models.PullMerged || pull.State == models.PullAbandoned || pull.PullSource == nil { 450 - return pages.Unknown 451 - } 452 - 453 - var sourceRepoDid string 454 - if pull.PullSource.RepoDid != nil { 455 - sourceRepoDid = string(*pull.PullSource.RepoDid) 456 - } else { 457 - sourceRepoDid = repo.RepoDid 458 - } 459 - 460 - xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 461 - branchResp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepoDid) 462 - if err != nil { 463 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 464 - s.logger.Error("failed to call XRPC repo.branches", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "branch", pull.PullSource.Branch) 465 - return pages.Unknown 466 - } 467 - s.logger.Error("failed to reach knotserver", "err", err, "pull_id", pull.PullId) 468 - return pages.Unknown 469 - } 470 - 471 - targetBranch := branchResp 472 - 473 - top := stack[0] 474 - latestSourceRev := top.LatestSha() 475 - 476 - if latestSourceRev != targetBranch.Hash { 477 - return pages.ShouldResubmit 478 - } 479 - 480 - return pages.ShouldNotResubmit 481 - } 482 - 483 - func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 484 - s.repoPullHelper(w, r, false) 485 - } 486 - 487 - func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 488 - s.repoPullHelper(w, r, true) 489 - } 490 - 491 - func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 492 - l := s.logger.With("handler", "RepoPullPatchRaw") 493 - 494 - pull, ok := r.Context().Value("pull").(*models.Pull) 495 - if !ok { 496 - l.Error("failed to get pull") 497 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 498 - return 499 - } 500 - l = l.With("pull_id", pull.PullId) 501 - 502 - roundId := chi.URLParam(r, "round") 503 - roundIdInt, err := strconv.Atoi(roundId) 504 - if err != nil || roundIdInt >= len(pull.Submissions) { 505 - http.Error(w, "bad round id", http.StatusBadRequest) 506 - l.Error("failed to parse round id", "err", err, "round_id_str", roundId) 507 - return 508 - } 509 - 510 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 511 - w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 512 - } 513 - 514 - func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { 515 - l := s.logger.With("handler", "RepoPulls") 516 - 517 - user := s.oauth.GetMultiAccountUser(r) 518 - if user != nil { 519 - l = l.With("user", user.Did) 520 - } 521 - 522 - params := r.URL.Query() 523 - page := pagination.FromContext(r.Context()) 524 - 525 - f, err := s.repoResolver.Resolve(r) 526 - if err != nil { 527 - l.Error("failed to get repo and knot", "err", err) 528 - return 529 - } 530 - l = l.With("repo_at", f.RepoAt().String()) 531 - 532 - query := searchquery.Parse(params.Get("q")) 533 - 534 - var state *models.PullState 535 - if urlState := params.Get("state"); urlState != "" { 536 - switch urlState { 537 - case "open": 538 - state = ptrPullState(models.PullOpen) 539 - case "closed": 540 - state = ptrPullState(models.PullClosed) 541 - case "merged": 542 - state = ptrPullState(models.PullMerged) 543 - } 544 - query.Set("state", urlState) 545 - } else if queryState := query.Get("state"); queryState != nil { 546 - switch *queryState { 547 - case "open": 548 - state = ptrPullState(models.PullOpen) 549 - case "closed": 550 - state = ptrPullState(models.PullClosed) 551 - case "merged": 552 - state = ptrPullState(models.PullMerged) 553 - } 554 - } else if _, hasQ := params["q"]; !hasQ { 555 - state = ptrPullState(models.PullOpen) 556 - query.Set("state", "open") 557 - } 558 - 559 - resolve := func(ctx context.Context, ident string) (string, error) { 560 - id, err := s.idResolver.ResolveIdent(ctx, ident) 561 - if err != nil { 562 - return "", err 563 - } 564 - return id.DID.String(), nil 565 - } 566 - 567 - authorDid, negatedAuthorDids := searchquery.ResolveAuthor(r.Context(), query, resolve, l) 568 - 569 - labels := query.GetAll("label") 570 - negatedLabels := query.GetAllNegated("label") 571 - labelValues := query.GetDynamicTags() 572 - negatedLabelValues := query.GetNegatedDynamicTags() 573 - 574 - // resolve DID-format label values: if a dynamic tag's label 575 - // definition has format "did", resolve the handle to a DID 576 - if len(labelValues) > 0 || len(negatedLabelValues) > 0 { 577 - labelDefs, err := db.GetLabelDefinitions( 578 - s.db, 579 - orm.FilterIn("at_uri", f.Labels), 580 - orm.FilterContains("scope", tangled.RepoPullNSID), 581 - ) 582 - if err == nil { 583 - didLabels := make(map[string]bool) 584 - for _, def := range labelDefs { 585 - if def.ValueType.Format == models.ValueTypeFormatDid { 586 - didLabels[def.Name] = true 587 - } 588 - } 589 - labelValues = searchquery.ResolveDIDLabelValues(r.Context(), labelValues, didLabels, resolve, l) 590 - negatedLabelValues = searchquery.ResolveDIDLabelValues(r.Context(), negatedLabelValues, didLabels, resolve, l) 591 - } else { 592 - l.Debug("failed to fetch label definitions for DID resolution", "err", err) 593 - } 594 - } 595 - 596 - tf := searchquery.ExtractTextFilters(query) 597 - 598 - searchOpts := models.PullSearchOptions{ 599 - Keywords: tf.Keywords, 600 - Phrases: tf.Phrases, 601 - RepoDid: f.RepoDid, 602 - State: state, 603 - AuthorDid: authorDid, 604 - Labels: labels, 605 - LabelValues: labelValues, 606 - NegatedKeywords: tf.NegatedKeywords, 607 - NegatedPhrases: tf.NegatedPhrases, 608 - NegatedLabels: negatedLabels, 609 - NegatedLabelValues: negatedLabelValues, 610 - NegatedAuthorDids: negatedAuthorDids, 611 - Page: page, 612 - } 613 - 614 - var totalPulls int 615 - if state == nil { 616 - totalPulls = f.RepoStats.PullCount.Open + f.RepoStats.PullCount.Merged + f.RepoStats.PullCount.Closed 617 - } else { 618 - switch *state { 619 - case models.PullOpen: 620 - totalPulls = f.RepoStats.PullCount.Open 621 - case models.PullMerged: 622 - totalPulls = f.RepoStats.PullCount.Merged 623 - case models.PullClosed: 624 - totalPulls = f.RepoStats.PullCount.Closed 625 - } 626 - } 627 - 628 - repoInfo := s.repoResolver.GetRepoInfo(r, user) 629 - 630 - var pulls []*models.Pull 631 - 632 - if searchOpts.HasSearchFilters() { 633 - res, err := s.indexer.Search(r.Context(), searchOpts) 634 - if err != nil { 635 - l.Error("failed to search for pulls", "err", err) 636 - return 637 - } 638 - totalPulls = int(res.Total) 639 - l.Debug("searched pulls with indexer", "count", len(res.Hits)) 640 - 641 - // update tab counts to reflect filtered results 642 - countOpts := searchOpts 643 - countOpts.Page = pagination.Page{Limit: 1} 644 - for _, ps := range []models.PullState{models.PullOpen, models.PullMerged, models.PullClosed} { 645 - countOpts.State = &ps 646 - countRes, err := s.indexer.Search(r.Context(), countOpts) 647 - if err != nil { 648 - continue 649 - } 650 - switch ps { 651 - case models.PullOpen: 652 - repoInfo.Stats.PullCount.Open = int(countRes.Total) 653 - case models.PullMerged: 654 - repoInfo.Stats.PullCount.Merged = int(countRes.Total) 655 - case models.PullClosed: 656 - repoInfo.Stats.PullCount.Closed = int(countRes.Total) 657 - } 658 - } 659 - 660 - if len(res.Hits) > 0 { 661 - pulls, err = db.GetPulls( 662 - s.db, 663 - orm.FilterIn("id", res.Hits), 664 - ) 665 - if err != nil { 666 - l.Error("failed to get pulls", "err", err) 667 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 668 - return 669 - } 670 - } 671 - } else { 672 - filters := []orm.Filter{ 673 - orm.FilterEq("repo_did", f.RepoDid), 674 - } 675 - if state != nil { 676 - filters = append(filters, orm.FilterEq("state", *state)) 677 - } 678 - pulls, err = db.GetPullsPaginated( 679 - s.db, 680 - page, 681 - filters..., 682 - ) 683 - if err != nil { 684 - l.Error("failed to get pulls", "err", err) 685 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 686 - return 687 - } 688 - } 689 - 690 - for _, p := range pulls { 691 - var pullSourceRepo *models.Repo 692 - if p.PullSource != nil { 693 - if p.PullSource.RepoDid != nil { 694 - pullSourceRepo, err = db.GetRepoByDid(s.db, string(*p.PullSource.RepoDid)) 695 - if err != nil { 696 - l.Error("failed to get repo by did", "err", err, "repo_did", p.PullSource.RepoDid.String()) 697 - continue 698 - } else { 699 - p.PullSource.Repo = pullSourceRepo 700 - } 701 - } 702 - } 703 - } 704 - 705 - var stacks []models.Stack 706 - var shas []string 707 - 708 - pullMap := make(map[string]*models.Pull) 709 - for _, p := range pulls { 710 - shas = append(shas, p.LatestSha()) 711 - pullMap[p.AtUri().String()] = p 712 - } 713 - 714 - // track which PRs have been added to stacks 715 - visited := make(map[string]bool) 716 - 717 - // group stacked PRs together using dependent_on relationships 718 - for _, p := range pulls { 719 - if visited[p.AtUri().String()] { 720 - continue 721 - } 722 - 723 - root := p 724 - for root.DependentOn != nil { 725 - if parent, ok := pullMap[root.DependentOn.String()]; ok { 726 - root = parent 727 - } else { 728 - break // parent not in current page 729 - } 730 - } 731 - 732 - var stack models.Stack 733 - current := root 734 - for { 735 - if visited[current.AtUri().String()] { 736 - break 737 - } 738 - stack = append(stack, current) 739 - visited[current.AtUri().String()] = true 740 - 741 - found := false 742 - for _, candidate := range pulls { 743 - if candidate.DependentOn != nil && 744 - candidate.DependentOn.String() == current.AtUri().String() { 745 - current = candidate 746 - found = true 747 - break 748 - } 749 - } 750 - if !found { 751 - break 752 - } 753 - } 754 - 755 - slices.Reverse(stack) 756 - stacks = append(stacks, stack) 757 - } 758 - 759 - ps, err := db.GetPipelineStatuses( 760 - s.db, 761 - len(shas), 762 - orm.FilterEq("p.repo_owner", f.Did), 763 - orm.FilterEq("p.repo_name", f.Rkey), 764 - orm.FilterEq("p.knot", f.Knot), 765 - orm.FilterIn("p.sha", shas), 766 - ) 767 - if err != nil { 768 - l.Warn("failed to fetch pipeline statuses", "err", err) 769 - // non-fatal 770 - } 771 - m := make(map[string]models.Pipeline) 772 - for _, p := range ps { 773 - m[p.Sha] = p 774 - } 775 - 776 - labelDefs, err := db.GetLabelDefinitions( 777 - s.db, 778 - orm.FilterIn("at_uri", f.Labels), 779 - orm.FilterContains("scope", tangled.RepoPullNSID), 780 - ) 781 - if err != nil { 782 - l.Error("failed to fetch labels", "err", err) 783 - s.pages.Error503(w) 784 - return 785 - } 786 - 787 - defs := make(map[string]*models.LabelDefinition) 788 - for _, l := range labelDefs { 789 - defs[l.AtUri().String()] = &l 790 - } 791 - 792 - filterState := "" 793 - if state != nil { 794 - filterState = state.String() 795 - } 796 - 797 - vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) 798 - if user != nil { 799 - dids := make([]syntax.DID, len(pulls)) 800 - for i, p := range pulls { 801 - dids[i] = syntax.DID(p.OwnerDid) 802 - } 803 - vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), dids) 804 - if err != nil { 805 - l.Error("failed to fetch vouch relationships", "err", err) 806 - } 807 - } 808 - 809 - err = s.pages.RepoPulls(w, pages.RepoPullsParams{ 810 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 811 - RepoInfo: repoInfo, 812 - Pulls: pulls, 813 - LabelDefs: defs, 814 - FilterState: filterState, 815 - FilterQuery: query.String(), 816 - Stacks: stacks, 817 - Pipelines: m, 818 - Page: page, 819 - PullCount: totalPulls, 820 - VouchRelationships: vouchRelationships, 821 - }) 822 - if err != nil { 823 - l.Error("failed to render page", "err", err) 824 - } 825 - } 826 - 827 - func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 828 - l := s.logger.With("handler", "PullComment") 829 - 830 - user := s.oauth.GetMultiAccountUser(r) 831 - if user != nil { 832 - l = l.With("user", user.Did) 833 - } 834 - 835 - f, err := s.repoResolver.Resolve(r) 836 - if err != nil { 837 - l.Error("failed to get repo and knot", "err", err) 838 - return 839 - } 840 - 841 - pull, ok := r.Context().Value("pull").(*models.Pull) 842 - if !ok { 843 - l.Error("failed to get pull") 844 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 845 - return 846 - } 847 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 848 - 849 - roundNumberStr := chi.URLParam(r, "round") 850 - roundNumber, err := strconv.Atoi(roundNumberStr) 851 - if err != nil || roundNumber >= len(pull.Submissions) { 852 - http.Error(w, "bad round id", http.StatusBadRequest) 853 - l.Error("failed to parse round id", "err", err, "round_number_str", roundNumberStr) 854 - return 855 - } 856 - 857 - switch r.Method { 858 - case http.MethodGet: 859 - s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 860 - LoggedInUser: user, 861 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 862 - Pull: pull, 863 - RoundNumber: roundNumber, 864 - }) 865 - return 866 - case http.MethodPost: 867 - body := r.FormValue("body") 868 - if body == "" { 869 - s.pages.Notice(w, "pull", "Comment body is required") 870 - return 871 - } 872 - 873 - mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 874 - 875 - // Start a transaction 876 - tx, err := s.db.BeginTx(r.Context(), nil) 877 - if err != nil { 878 - l.Error("failed to start transaction", "err", err) 879 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 880 - return 881 - } 882 - defer tx.Rollback() 883 - 884 - createdAt := time.Now().Format(time.RFC3339) 885 - 886 - client, err := s.oauth.AuthorizedClient(r) 887 - if err != nil { 888 - l.Error("failed to get authorized client", "err", err) 889 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 890 - return 891 - } 892 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 893 - Collection: tangled.RepoPullCommentNSID, 894 - Repo: user.Did, 895 - Rkey: tid.TID(), 896 - Record: &lexutil.LexiconTypeDecoder{ 897 - Val: &tangled.RepoPullComment{ 898 - Pull: pull.AtUri().String(), 899 - Body: body, 900 - CreatedAt: createdAt, 901 - }, 902 - }, 903 - }) 904 - if err != nil { 905 - l.Error("failed to create pull comment", "err", err) 906 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 907 - return 908 - } 909 - 910 - comment := &models.PullComment{ 911 - OwnerDid: user.Did, 912 - RepoDid: string(f.RepoDid), 913 - PullId: pull.PullId, 914 - Body: body, 915 - CommentAt: atResp.Uri, 916 - SubmissionId: pull.Submissions[roundNumber].ID, 917 - Mentions: mentions, 918 - References: references, 919 - } 920 - 921 - // Create the pull comment in the database with the commentAt field 922 - commentId, err := db.NewPullComment(tx, comment) 923 - if err != nil { 924 - l.Error("failed to create pull comment in database", "err", err) 925 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 926 - return 927 - } 928 - 929 - // Commit the transaction 930 - if err = tx.Commit(); err != nil { 931 - l.Error("failed to commit transaction", "err", err) 932 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 933 - return 934 - } 935 - 936 - s.notifier.NewPullComment(r.Context(), comment, mentions) 937 - 938 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 939 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId)) 940 - return 941 - } 942 - } 943 - 944 - func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { 945 - l := s.logger.With("handler", "NewPull") 946 - 947 - user := s.oauth.GetMultiAccountUser(r) 948 - if user != nil { 949 - l = l.With("user", user.Did) 950 - } 951 - 952 - f, err := s.repoResolver.Resolve(r) 953 - if err != nil { 954 - l.Error("failed to get repo and knot", "err", err) 955 - return 956 - } 957 - l = l.With("repo_at", f.RepoAt().String()) 958 - 959 - switch r.Method { 960 - case http.MethodGet: 961 - params, err := s.composeParams(r, f) 962 - if err != nil { 963 - l.Error("failed to build compose params", "err", err) 964 - s.pages.Error503(w) 965 - return 966 - } 967 - s.pages.RepoNewPull(w, params) 968 - 969 - case http.MethodPost: 970 - title := r.FormValue("title") 971 - body := r.FormValue("body") 972 - targetBranch := r.FormValue("targetBranch") 973 - fromFork := r.FormValue("fork") 974 - sourceBranch := r.FormValue("sourceBranch") 975 - patch := r.FormValue("patch") 976 - userDid := syntax.DID(user.Did) 977 - 978 - if targetBranch == "" { 979 - s.pages.Notice(w, "pull", "Target branch is required.") 980 - return 981 - } 982 - 983 - // Determine PR type based on input parameters 984 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(userDid.String(), f.Knot, f.RepoIdentifier())} 985 - isPushAllowed := roles.IsPushAllowed() 986 - isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 987 - isForkBased := fromFork != "" && sourceBranch != "" 988 - isPatchBased := patch != "" && !isBranchBased && !isForkBased 989 - isStacked := r.FormValue("mode") == "stack" && !isPatchBased 990 - 991 - if isPatchBased && !patchutil.IsFormatPatch(patch) { 992 - if title == "" { 993 - s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 994 - return 995 - } 996 - sanitizer := markup.NewSanitizer() 997 - if st := strings.TrimSpace(sanitizer.SanitizeDescription(title)); (st) == "" { 998 - s.pages.Notice(w, "pull", "Title is empty after HTML sanitization") 999 - return 1000 - } 1001 - } 1002 - 1003 - // Validate we have at least one valid PR creation method 1004 - if !isBranchBased && !isPatchBased && !isForkBased { 1005 - s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 1006 - return 1007 - } 1008 - 1009 - // Can't mix branch-based and patch-based approaches 1010 - if isBranchBased && patch != "" { 1011 - s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 1012 - return 1013 - } 1014 - 1015 - if isBranchBased && sourceBranch == targetBranch { 1016 - s.pages.Notice(w, "pull", "Source and target branch must be different.") 1017 - return 1018 - } 1019 - 1020 - // us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1021 - // if err != nil { 1022 - // log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 1023 - // s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 1024 - // return 1025 - // } 1026 - 1027 - // TODO: make capabilities an xrpc call 1028 - caps := struct { 1029 - PullRequests struct { 1030 - FormatPatch bool 1031 - BranchSubmissions bool 1032 - ForkSubmissions bool 1033 - PatchSubmissions bool 1034 - } 1035 - }{ 1036 - PullRequests: struct { 1037 - FormatPatch bool 1038 - BranchSubmissions bool 1039 - ForkSubmissions bool 1040 - PatchSubmissions bool 1041 - }{ 1042 - FormatPatch: true, 1043 - BranchSubmissions: true, 1044 - ForkSubmissions: true, 1045 - PatchSubmissions: true, 1046 - }, 1047 - } 1048 - 1049 - // caps, err := us.Capabilities() 1050 - // if err != nil { 1051 - // log.Println("error fetching knot caps", f.Knot, err) 1052 - // s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 1053 - // return 1054 - // } 1055 - 1056 - if !caps.PullRequests.FormatPatch { 1057 - s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 1058 - return 1059 - } 1060 - 1061 - stackTitles := parseBracketedForm(r.Form, "stackTitle") 1062 - stackBodies := parseBracketedForm(r.Form, "stackBody") 1063 - 1064 - // Handle the PR creation based on the type 1065 - if isBranchBased { 1066 - if !caps.PullRequests.BranchSubmissions { 1067 - s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 1068 - return 1069 - } 1070 - s.handleBranchBasedPull(w, r, f, userDid, title, body, targetBranch, sourceBranch, isStacked, stackTitles, stackBodies) 1071 - } else if isForkBased { 1072 - if !caps.PullRequests.ForkSubmissions { 1073 - s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 1074 - return 1075 - } 1076 - s.handleForkBasedPull(w, r, f, userDid, fromFork, title, body, targetBranch, sourceBranch, isStacked, stackTitles, stackBodies) 1077 - } else if isPatchBased { 1078 - if !caps.PullRequests.PatchSubmissions { 1079 - s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 1080 - return 1081 - } 1082 - s.handlePatchBasedPull(w, r, f, userDid, title, body, targetBranch, patch, isStacked, stackTitles, stackBodies) 1083 - } 1084 - return 1085 - } 1086 - } 1087 - 1088 - func (s *Pulls) handleBranchBasedPull( 1089 - w http.ResponseWriter, 1090 - r *http.Request, 1091 - repo *models.Repo, 1092 - userDid syntax.DID, 1093 - title, 1094 - body, 1095 - targetBranch, 1096 - sourceBranch string, 1097 - isStacked bool, 1098 - stackTitles, stackBodies map[string]string, 1099 - ) { 1100 - l := s.logger.With("handler", "handleBranchBasedPull", "user", userDid, "target_branch", targetBranch, "source_branch", sourceBranch, "is_stacked", isStacked) 1101 - 1102 - xrpcc := s.knotClient(repo.Knot) 1103 - 1104 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch) 1105 - if err != nil { 1106 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1107 - l.Error("failed to call XRPC repo.compare", "xrpcerr", xrpcerr, "err", err) 1108 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1109 - return 1110 - } 1111 - l.Error("failed to compare", "err", err) 1112 - s.pages.Notice(w, "pull", err.Error()) 1113 - return 1114 - } 1115 - 1116 - var comparison types.RepoFormatPatchResponse 1117 - if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 1118 - l.Error("failed to decode XRPC compare response", "err", err) 1119 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1120 - return 1121 - } 1122 - 1123 - if len(comparison.FormatPatch) == 0 { 1124 - s.pages.Notice(w, "pull", "No commits between target and source.") 1125 - return 1126 - } 1127 - 1128 - sourceRev := comparison.Rev2 1129 - patch := comparison.FormatPatchRaw 1130 - combined := comparison.CombinedPatchRaw 1131 - 1132 - if err := s.validator.ValidatePatch(&patch); err != nil { 1133 - s.logger.Error("failed to validate patch", "err", err) 1134 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1135 - return 1136 - } 1137 - 1138 - pullSource := &models.PullSource{ 1139 - Branch: sourceBranch, 1140 - } 1141 - 1142 - s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 1143 - } 1144 - 1145 - func (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) { 1146 - if err := s.validator.ValidatePatch(&patch); err != nil { 1147 - s.logger.Error("patch validation failed", "err", err) 1148 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1149 - return 1150 - } 1151 - 1152 - s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, "", "", nil, isStacked, stackTitles, stackBodies) 1153 - } 1154 - 1155 - func (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) { 1156 - l := s.logger.With("handler", "handleForkBasedPull", "user", userDid, "fork_repo", forkRepo, "target_branch", targetBranch, "source_branch", sourceBranch, "is_stacked", isStacked) 1157 - 1158 - repoString := strings.SplitN(forkRepo, "/", 2) 1159 - forkOwnerDid := repoString[0] 1160 - forkRkey := strings.ToLower(repoString[1]) 1161 - fork, err := db.GetForkByDid(s.db, forkOwnerDid, forkRkey) 1162 - if errors.Is(err, sql.ErrNoRows) { 1163 - s.pages.Notice(w, "pull", "No such fork.") 1164 - return 1165 - } else if err != nil { 1166 - l.Error("failed to fetch fork", "err", err, "fork_owner_did", forkOwnerDid, "fork_rkey", forkRkey) 1167 - s.pages.Notice(w, "pull", "Failed to fetch fork.") 1168 - return 1169 - } 1170 - 1171 - client, err := s.oauth.ServiceClient( 1172 - r, 1173 - oauth.WithService(fork.Knot), 1174 - oauth.WithLxm(tangled.RepoHiddenRefNSID), 1175 - oauth.WithDev(s.config.Core.Dev), 1176 - ) 1177 - 1178 - resp, err := tangled.RepoHiddenRef( 1179 - r.Context(), 1180 - client, 1181 - &tangled.RepoHiddenRef_Input{ 1182 - ForkRef: sourceBranch, 1183 - RemoteRef: targetBranch, 1184 - Repo: fork.RepoAt().String(), 1185 - }, 1186 - ) 1187 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1188 - s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 1189 - s.pages.Notice(w, "pull", xrpcerr.Error()) 1190 - return 1191 - } 1192 - 1193 - if !resp.Success { 1194 - errorMsg := "Failed to create pull request" 1195 - if resp.Error != nil { 1196 - errorMsg = fmt.Sprintf("Failed to create pull request: %s", *resp.Error) 1197 - } 1198 - s.pages.Notice(w, "pull", errorMsg) 1199 - return 1200 - } 1201 - 1202 - hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 1203 - // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 1204 - // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 1205 - // hiddenRef: hidden/feature-1/main (on repo-fork) 1206 - // targetBranch: main (on repo-1) 1207 - // sourceBranch: feature-1 (on repo-fork) 1208 - forkXrpcc := s.knotClient(fork.Knot) 1209 - 1210 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 1211 - if err != nil { 1212 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1213 - l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef) 1214 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1215 - return 1216 - } 1217 - l.Error("failed to compare across branches", "err", err, "hidden_ref", hiddenRef) 1218 - s.pages.Notice(w, "pull", err.Error()) 1219 - return 1220 - } 1221 - 1222 - var comparison types.RepoFormatPatchResponse 1223 - if err := json.Unmarshal(forkXrpcBytes, &comparison); err != nil { 1224 - l.Error("failed to decode XRPC compare response for fork", "err", err) 1225 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1226 - return 1227 - } 1228 - 1229 - if len(comparison.FormatPatch) == 0 { 1230 - s.pages.Notice(w, "pull", "No commits between target and source.") 1231 - return 1232 - } 1233 - 1234 - sourceRev := comparison.Rev2 1235 - patch := comparison.FormatPatchRaw 1236 - combined := comparison.CombinedPatchRaw 1237 - 1238 - if err := s.validator.ValidatePatch(&patch); err != nil { 1239 - s.logger.Error("failed to validate patch", "err", err) 1240 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1241 - return 1242 - } 1243 - 1244 - forkDid := syntax.DID(fork.RepoDid) 1245 - pullSource := &models.PullSource{ 1246 - Branch: sourceBranch, 1247 - RepoDid: &forkDid, 1248 - } 1249 - 1250 - s.createPullRequest(w, r, repo, userDid, title, body, targetBranch, patch, combined, sourceRev, pullSource, isStacked, stackTitles, stackBodies) 1251 - } 1252 - 1253 - func (s *Pulls) createPullRequest( 1254 - w http.ResponseWriter, 1255 - r *http.Request, 1256 - repo *models.Repo, 1257 - userDid syntax.DID, 1258 - title, body, targetBranch string, 1259 - patch string, 1260 - combined string, 1261 - sourceRev string, 1262 - pullSource *models.PullSource, 1263 - isStacked bool, 1264 - stackTitles, stackBodies map[string]string, 1265 - ) { 1266 - l := s.logger.With("handler", "createPullRequest", "user", userDid, "target_branch", targetBranch, "is_stacked", isStacked) 1267 - 1268 - if isStacked { 1269 - // creates a series of PRs, each linking to the previous, identified by jj's change-id 1270 - s.createStackedPullRequest( 1271 - w, 1272 - r, 1273 - repo, 1274 - userDid, 1275 - targetBranch, 1276 - patch, 1277 - sourceRev, 1278 - pullSource, 1279 - stackTitles, 1280 - stackBodies, 1281 - ) 1282 - return 1283 - } 1284 - 1285 - client, err := s.oauth.AuthorizedClient(r) 1286 - if err != nil { 1287 - l.Error("failed to get authorized client", "err", err) 1288 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1289 - return 1290 - } 1291 - 1292 - tx, err := s.db.BeginTx(r.Context(), nil) 1293 - if err != nil { 1294 - l.Error("failed to start tx", "err", err) 1295 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1296 - return 1297 - } 1298 - defer tx.Rollback() 1299 - 1300 - // We've already checked earlier if it's diff-based and title is empty, 1301 - // so if it's still empty now, it's intentionally skipped owing to format-patch. 1302 - if title == "" || body == "" { 1303 - formatPatches, err := patchutil.ExtractPatches(patch) 1304 - if err != nil { 1305 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1306 - return 1307 - } 1308 - if len(formatPatches) == 0 { 1309 - s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 1310 - return 1311 - } 1312 - 1313 - if title == "" { 1314 - title = formatPatches[0].Title 1315 - } 1316 - if body == "" { 1317 - body = formatPatches[0].Body 1318 - } 1319 - } 1320 - 1321 - mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 1322 - 1323 - rkey := tid.TID() 1324 - 1325 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 1326 - if err != nil { 1327 - l.Error("failed to upload patch", "err", err) 1328 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1329 - return 1330 - } 1331 - 1332 - now := time.Now() 1333 - 1334 - pull := &models.Pull{ 1335 - Title: title, 1336 - Body: body, 1337 - TargetBranch: targetBranch, 1338 - OwnerDid: userDid.String(), 1339 - RepoDid: syntax.DID(repo.RepoDid), 1340 - Rkey: rkey, 1341 - Mentions: mentions, 1342 - References: references, 1343 - Submissions: []*models.PullSubmission{ 1344 - { 1345 - Patch: patch, 1346 - Combined: combined, 1347 - SourceRev: sourceRev, 1348 - Blob: *blob.Blob, 1349 - Created: now, 1350 - }, 1351 - }, 1352 - PullSource: pullSource, 1353 - State: models.PullOpen, 1354 - Created: now, 1355 - } 1356 - 1357 - record := pull.AsRecord() 1358 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1359 - Collection: tangled.RepoPullNSID, 1360 - Repo: userDid.String(), 1361 - Rkey: rkey, 1362 - Record: &lexutil.LexiconTypeDecoder{ 1363 - Val: &record, 1364 - }, 1365 - }) 1366 - if err != nil { 1367 - l.Error("failed to create pull request", "err", err) 1368 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1369 - return 1370 - } 1371 - 1372 - err = db.PutPull(tx, pull) 1373 - if err != nil { 1374 - l.Error("failed to create pull request in database", "err", err) 1375 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1376 - return 1377 - } 1378 - pullId, err := db.NextPullId(tx, repo.RepoDid) 1379 - if err != nil { 1380 - s.logger.Error("failed to get pull id", "err", err) 1381 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1382 - return 1383 - } 1384 - 1385 - if err = tx.Commit(); err != nil { 1386 - l.Error("failed to commit transaction for pull request", "err", err) 1387 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1388 - return 1389 - } 1390 - 1391 - s.notifier.NewPull(r.Context(), pull) 1392 - 1393 - s.applyCreationLabels(r.Context(), client, userDid, []*models.Pull{pull}, r.Form, repo) 1394 - 1395 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 1396 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId)) 1397 - } 1398 - 1399 - func (s *Pulls) createStackedPullRequest( 1400 - w http.ResponseWriter, 1401 - r *http.Request, 1402 - repo *models.Repo, 1403 - userDid syntax.DID, 1404 - targetBranch string, 1405 - patch string, 1406 - sourceRev string, 1407 - pullSource *models.PullSource, 1408 - stackTitles, stackBodies map[string]string, 1409 - ) { 1410 - l := s.logger.With("handler", "createStackedPullRequest", "user", userDid, "target_branch", targetBranch, "source_rev", sourceRev) 1411 - 1412 - // run some necessary checks for stacked-prs first 1413 - 1414 - formatPatches, err := patchutil.ExtractPatches(patch) 1415 - if err != nil { 1416 - l.Error("failed to extract patches", "err", err) 1417 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1418 - return 1419 - } 1420 - 1421 - // must have atleast 1 patch to begin with 1422 - if len(formatPatches) == 0 { 1423 - l.Error("empty patches") 1424 - s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 1425 - return 1426 - } 1427 - 1428 - client, err := s.oauth.AuthorizedClient(r) 1429 - if err != nil { 1430 - l.Error("failed to get authorized client", "err", err) 1431 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1432 - return 1433 - } 1434 - 1435 - // first upload all blobs 1436 - blobs := make([]*lexutil.LexBlob, len(formatPatches)) 1437 - for i, p := range formatPatches { 1438 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 1439 - if err != nil { 1440 - l.Error("failed to upload patch blob", "err", err, "patch_index", i) 1441 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1442 - return 1443 - } 1444 - l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 1445 - blobs[i] = blob.Blob 1446 - } 1447 - 1448 - // build a stack out of this patch 1449 - stack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pullSource, formatPatches, blobs, stackTitles, stackBodies) 1450 - if err != nil { 1451 - l.Error("failed to create stack", "err", err) 1452 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 1453 - return 1454 - } 1455 - 1456 - // apply all record creations at once 1457 - var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1458 - for _, p := range stack { 1459 - record := p.AsRecord() 1460 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1461 - RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1462 - Collection: tangled.RepoPullNSID, 1463 - Rkey: &p.Rkey, 1464 - Value: &lexutil.LexiconTypeDecoder{ 1465 - Val: &record, 1466 - }, 1467 - }, 1468 - }) 1469 - } 1470 - _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1471 - Repo: userDid.String(), 1472 - Writes: writes, 1473 - }) 1474 - if err != nil { 1475 - l.Error("failed to create stacked pull request", "err", err) 1476 - s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1477 - return 1478 - } 1479 - 1480 - // create all pulls at once 1481 - tx, err := s.db.BeginTx(r.Context(), nil) 1482 - if err != nil { 1483 - l.Error("failed to start tx", "err", err) 1484 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1485 - return 1486 - } 1487 - defer tx.Rollback() 1488 - 1489 - for _, p := range stack { 1490 - err = db.PutPull(tx, p) 1491 - if err != nil { 1492 - l.Error("failed to create pull request in database", "err", err, "pull_rkey", p.Rkey) 1493 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1494 - return 1495 - } 1496 - 1497 - } 1498 - 1499 - if err = tx.Commit(); err != nil { 1500 - l.Error("failed to commit transaction for pull requests", "err", err) 1501 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1502 - return 1503 - } 1504 - 1505 - // notify about each pull 1506 - // 1507 - // this is performed after tx.Commit, because it could result in a locked DB otherwise 1508 - for _, p := range stack { 1509 - s.notifier.NewPull(r.Context(), p) 1510 - } 1511 - 1512 - s.applyCreationLabels(r.Context(), client, userDid, stack, r.Form, repo) 1513 - 1514 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 1515 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo)) 1516 - } 1517 - 1518 - func (s *Pulls) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 1519 - body := r.FormValue("body") 1520 - s.pages.MarkdownPreviewFragment(w, body) 1521 - } 1522 - 1523 - func (s *Pulls) RefreshCompose(w http.ResponseWriter, r *http.Request) { 1524 - l := s.logger.With("handler", "RefreshCompose") 1525 - 1526 - f, err := s.repoResolver.Resolve(r) 1527 - if err != nil { 1528 - l.Error("failed to resolve repo", "err", err) 1529 - s.pages.Error503(w) 1530 - return 1531 - } 1532 - 1533 - params, err := s.composeParams(r, f) 1534 - if err != nil { 1535 - l.Error("failed to build compose params", "err", err) 1536 - s.pages.Error503(w) 1537 - return 1538 - } 1539 - w.Header().Set("HX-Replace-Url", composeCanonicalURL(params)) 1540 - s.pages.PullComposeHostFragment(w, params) 1541 - } 1542 - 1543 - func composeCanonicalURL(params pages.RepoNewPullParams) string { 1544 - base := fmt.Sprintf("/%s/pulls/new", params.RepoInfo.FullName()) 1545 - q := url.Values{} 1546 - if params.IsStacked { 1547 - q.Set("mode", "stack") 1548 - } 1549 - if params.Source != "" && params.Source != pages.SourceBranch { 1550 - q.Set("source", string(params.Source)) 1551 - } 1552 - if params.SourceBranch != "" { 1553 - q.Set("sourceBranch", params.SourceBranch) 1554 - } 1555 - if params.TargetBranch != "" { 1556 - q.Set("targetBranch", params.TargetBranch) 1557 - } 1558 - if params.Source == pages.SourceFork && params.Fork != "" { 1559 - q.Set("fork", params.Fork) 1560 - } 1561 - if len(q) == 0 { 1562 - return base 1563 - } 1564 - return base + "?" + q.Encode() 1565 - } 1566 - 1567 - func (s *Pulls) composeParams(r *http.Request, repo *models.Repo) (pages.RepoNewPullParams, error) { 1568 - l := s.logger.With("handler", "composeParams") 1569 - user := s.oauth.GetMultiAccountUser(r) 1570 - 1571 - branches, err := s.listBranches(r.Context(), repo) 1572 - if err != nil { 1573 - return pages.RepoNewPullParams{}, err 1574 - } 1575 - 1576 - var forks []models.Repo 1577 - if user != nil { 1578 - forks, err = db.GetForksByDid(s.db, user.Did) 1579 - if err != nil { 1580 - l.Warn("failed to list user forks", "err", err, "user", user.Did) 1581 - } 1582 - } 1583 - 1584 - repoInfo := s.repoResolver.GetRepoInfo(r, user) 1585 - source, ok := pages.ParseSource(r.FormValue("source")) 1586 - if !ok { 1587 - source = pages.SourceBranch 1588 - if !repoInfo.Roles.IsPushAllowed() { 1589 - source = pages.SourceFork 1590 - } 1591 - } 1592 - 1593 - sourceBranch := r.FormValue("sourceBranch") 1594 - targetBranch := r.FormValue("targetBranch") 1595 - fork := r.FormValue("fork") 1596 - patch := r.FormValue("patch") 1597 - 1598 - if source == pages.SourceFork && fork == "" && len(forks) == 1 { 1599 - fork = fmt.Sprintf("%s/%s", forks[0].Did, forks[0].Name) 1600 - } 1601 - 1602 - var forkBranches []types.Branch 1603 - var forkBranchesErr error 1604 - if source == pages.SourceFork && fork != "" { 1605 - forkBranches, forkBranchesErr = s.listForkBranches(r.Context(), fork) 1606 - if forkBranchesErr != nil { 1607 - l.Warn("failed to list fork branches", "err", forkBranchesErr, "fork", fork) 1608 - } 1609 - } 1610 - 1611 - sourceBranchList := sourceBranchChoices(branches) 1612 - targetBranch = defaultTargetBranch(branches, targetBranch) 1613 - sourceBranch = defaultSourceBranch(source, sourceBranch, sourceBranchList, forkBranches) 1614 - 1615 - comparison, diff, prefetchErr := s.prefetchComparison(r, repo, source, fork, targetBranch, sourceBranch, patch) 1616 - var prefillErr string 1617 - if joined := errors.Join(prefetchErr, forkBranchesErr); joined != nil { 1618 - prefillErr = joined.Error() 1619 - } 1620 - 1621 - mergeCheck := s.composeMergeCheck(r.Context(), repo, targetBranch, comparison) 1622 - 1623 - refreshUrl := fmt.Sprintf("/%s/pulls/new/refresh", repoInfo.FullName()) 1624 - var diffOpts types.DiffOpts 1625 - if r.FormValue("diff") == "split" { 1626 - diffOpts.Split = true 1627 - } 1628 - diffOpts.RefreshUrl = refreshUrl 1629 - diffOpts.Target = "#diff-area" 1630 - 1631 - labelDefs, err := s.pullLabelDefs(repo) 1632 - if err != nil { 1633 - l.Warn("failed to load label definitions", "err", err) 1634 - } 1635 - labelState := labelStateFromForm(r.Form, labelDefs) 1636 - perCidLabelForms := parseStackLabelForms(r.Form) 1637 - stackLabelStates := make(map[string]models.LabelState, len(perCidLabelForms)) 1638 - for cid, perForm := range perCidLabelForms { 1639 - stackLabelStates[cid] = labelStateFromForm(perForm, labelDefs) 1640 - } 1641 - 1642 - stackTitles := parseBracketedForm(r.Form, "stackTitle") 1643 - stackBodies := parseBracketedForm(r.Form, "stackBody") 1644 - stackSplits := parseBracketedForm(r.Form, "stackSplit") 1645 - 1646 - title := r.FormValue("title") 1647 - body := r.FormValue("body") 1648 - if comparison != nil && len(comparison.FormatPatch) > 0 { 1649 - first := comparison.FormatPatch[0] 1650 - if title == "" && first.PatchHeader != nil { 1651 - title = first.Title 1652 - } 1653 - if body == "" && first.PatchHeader != nil { 1654 - body = first.Body 1655 - } 1656 - } 1657 - 1658 - isStacked := r.FormValue("mode") == "stack" && source != pages.SourcePatch 1659 - var stackedDiffs []pages.StackedDiff 1660 - if isStacked { 1661 - stackedDiffs = stackPerCommitDiffs(comparison, targetBranch, refreshUrl, stackSplits) 1662 - } 1663 - 1664 - return pages.RepoNewPullParams{ 1665 - LoggedInUser: user, 1666 - RepoInfo: repoInfo, 1667 - Branches: branches, 1668 - SourceBranches: sourceBranchList, 1669 - ForkBranches: forkBranches, 1670 - Forks: forks, 1671 - Source: source, 1672 - SourceBranch: sourceBranch, 1673 - TargetBranch: targetBranch, 1674 - Fork: fork, 1675 - Patch: patch, 1676 - Title: title, 1677 - Body: body, 1678 - IsStacked: isStacked, 1679 - Comparison: comparison, 1680 - Diff: diff, 1681 - DiffOpts: diffOpts, 1682 - StackedDiffs: stackedDiffs, 1683 - MergeCheck: mergeCheck, 1684 - StackTitles: stackTitles, 1685 - StackBodies: stackBodies, 1686 - PrefillError: prefillErr, 1687 - LabelDefs: labelDefs, 1688 - LabelState: labelState, 1689 - StackLabelStates: stackLabelStates, 1690 - }, nil 1691 - } 1692 - 1693 - func (s *Pulls) pullLabelDefs(repo *models.Repo) (map[string]*models.LabelDefinition, error) { 1694 - defs, err := db.GetLabelDefinitions( 1695 - s.db, 1696 - orm.FilterIn("at_uri", repo.Labels), 1697 - orm.FilterContains("scope", tangled.RepoPullNSID), 1698 - ) 1699 - if err != nil { 1700 - return nil, err 1701 - } 1702 - 1703 - out := make(map[string]*models.LabelDefinition, len(defs)) 1704 - for i := range defs { 1705 - d := defs[i] 1706 - if !slices.Contains(d.Scope, tangled.RepoPullNSID) { 1707 - continue 1708 - } 1709 - out[d.AtUri().String()] = &d 1710 - } 1711 - return out, nil 1712 - } 1713 - 1714 - func formLabelEntries(form url.Values, defs map[string]*models.LabelDefinition) iter.Seq2[string, string] { 1715 - return func(yield func(string, string) bool) { 1716 - for key := range defs { 1717 - for _, v := range form[key] { 1718 - if v == "" { 1719 - continue 1720 - } 1721 - if !yield(key, v) { 1722 - return 1723 - } 1724 - } 1725 - } 1726 - } 1727 - } 1728 - 1729 - func labelStateFromForm(form url.Values, defs map[string]*models.LabelDefinition) models.LabelState { 1730 - state := models.NewLabelState() 1731 - actx := &models.LabelApplicationCtx{Defs: defs} 1732 - for key, val := range formLabelEntries(form, defs) { 1733 - _ = actx.ApplyLabelOp(state, models.LabelOp{ 1734 - Operation: models.LabelOperationAdd, 1735 - OperandKey: key, 1736 - OperandValue: val, 1737 - }) 1738 - } 1739 - return state 1740 - } 1741 - 1742 - func buildCreationLabelOps( 1743 - userDid syntax.DID, 1744 - subject syntax.ATURI, 1745 - rkey string, 1746 - form url.Values, 1747 - defs map[string]*models.LabelDefinition, 1748 - performedAt time.Time, 1749 - ) []models.LabelOp { 1750 - var ops []models.LabelOp 1751 - for key, val := range formLabelEntries(form, defs) { 1752 - ops = append(ops, models.LabelOp{ 1753 - Did: userDid.String(), 1754 - Rkey: rkey, 1755 - Subject: subject, 1756 - Operation: models.LabelOperationAdd, 1757 - OperandKey: key, 1758 - OperandValue: val, 1759 - PerformedAt: performedAt, 1760 - }) 1761 - } 1762 - return ops 1763 - } 1764 - 1765 - func (s *Pulls) applyCreationLabels( 1766 - ctx context.Context, 1767 - client *atclient.APIClient, 1768 - userDid syntax.DID, 1769 - pulls []*models.Pull, 1770 - form url.Values, 1771 - repo *models.Repo, 1772 - ) { 1773 - l := s.logger.With("handler", "applyCreationLabels", "user", userDid) 1774 - 1775 - defs, err := s.pullLabelDefs(repo) 1776 - if err != nil { 1777 - l.Warn("failed to fetch label defs", "err", err) 1778 - return 1779 - } 1780 - if len(defs) == 0 { 1781 - return 1782 - } 1783 - 1784 - perCidForms := parseStackLabelForms(form) 1785 - 1786 - applyAll := form.Get("applyLabelsToAll") == "on" 1787 - var firstStackForm url.Values 1788 - if applyAll && len(pulls) > 0 && len(pulls[0].Submissions) > 0 { 1789 - if firstCid := pulls[0].Submissions[0].ChangeId(); firstCid != "" { 1790 - if f, ok := perCidForms[firstCid]; ok { 1791 - firstStackForm = f 1792 - } 1793 - } 1794 - } 1795 - 1796 - performedAt := time.Now() 1797 - for _, pull := range pulls { 1798 - labelForm := form 1799 - if firstStackForm != nil { 1800 - labelForm = firstStackForm 1801 - } else if len(perCidForms) > 0 && len(pull.Submissions) > 0 { 1802 - if cid := pull.Submissions[0].ChangeId(); cid != "" { 1803 - if perForm, ok := perCidForms[cid]; ok { 1804 - labelForm = perForm 1805 - } 1806 - } 1807 - } 1808 - rkey := tid.TID() 1809 - raw := buildCreationLabelOps(userDid, pull.AtUri(), rkey, labelForm, defs, performedAt) 1810 - 1811 - valid := make([]models.LabelOp, 0, len(raw)) 1812 - for _, op := range raw { 1813 - def := defs[op.OperandKey] 1814 - if err := s.validator.ValidateLabelOp(def, repo, &op); err != nil { 1815 - l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey) 1816 - continue 1817 - } 1818 - valid = append(valid, op) 1819 - } 1820 - if len(valid) == 0 { 1821 - continue 1822 - } 1823 - 1824 - record := models.LabelOpsAsRecord(valid) 1825 - if _, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1826 - Collection: tangled.LabelOpNSID, 1827 - Repo: userDid.String(), 1828 - Rkey: rkey, 1829 - Record: &lexutil.LexiconTypeDecoder{Val: &record}, 1830 - }); err != nil { 1831 - l.Warn("failed to write label ops to PDS", "err", err, "subject", pull.AtUri()) 1832 - continue 1833 - } 1834 - 1835 - if err := s.indexLabelOps(ctx, valid); err != nil { 1836 - l.Warn("failed to index label ops", "err", err, "subject", pull.AtUri()) 1837 - if _, err := comatproto.RepoDeleteRecord(context.Background(), client, &comatproto.RepoDeleteRecord_Input{ 1838 - Collection: tangled.LabelOpNSID, 1839 - Repo: userDid.String(), 1840 - Rkey: rkey, 1841 - }); err != nil { 1842 - l.Warn("failed to rollback label ops record from PDS", "err", err, "subject", pull.AtUri()) 1843 - } 1844 - continue 1845 - } 1846 - 1847 - s.notifier.NewPullLabelOp(ctx, pull) 1848 - } 1849 - } 1850 - 1851 - func (s *Pulls) indexLabelOps(ctx context.Context, ops []models.LabelOp) error { 1852 - tx, err := s.db.BeginTx(ctx, nil) 1853 - if err != nil { 1854 - return err 1855 - } 1856 - defer tx.Rollback() 1857 - for _, op := range ops { 1858 - if _, err := db.AddLabelOp(tx, &op); err != nil { 1859 - return err 1860 - } 1861 - } 1862 - return tx.Commit() 1863 - } 1864 - 1865 - func (s *Pulls) listBranches(ctx context.Context, repo *models.Repo) ([]types.Branch, error) { 1866 - xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 1867 - xrpcBytes, err := tangled.GitTempListBranches(ctx, xrpcc, "", 0, repo.RepoAt().String()) 1868 - if err != nil { 1869 - return nil, err 1870 - } 1871 - var result types.RepoBranchesResponse 1872 - if err := json.Unmarshal(xrpcBytes, &result); err != nil { 1873 - return nil, err 1874 - } 1875 - return result.Branches, nil 1876 - } 1877 - 1878 - func (s *Pulls) listForkBranches(ctx context.Context, forkIdent string) ([]types.Branch, error) { 1879 - parts := strings.SplitN(forkIdent, "/", 2) 1880 - if len(parts) != 2 { 1881 - return nil, fmt.Errorf("invalid fork identifier: %s", forkIdent) 1882 - } 1883 - forkRepo, err := db.GetRepo(s.db, orm.FilterEq("did", parts[0]), orm.FilterEq("name", parts[1])) 1884 - if err != nil { 1885 - return nil, err 1886 - } 1887 - branches, err := s.listBranches(ctx, forkRepo) 1888 - if err != nil { 1889 - return nil, err 1890 - } 1891 - return sortBranchesByRecency(branches), nil 1892 - } 1893 - 1894 - func sourceBranchChoices(branches []types.Branch) []types.Branch { 1895 - withoutDefault := slices.DeleteFunc(slices.Clone(branches), func(b types.Branch) bool { 1896 - return b.IsDefault 1897 - }) 1898 - return sortBranchesByRecency(withoutDefault) 1899 - } 1900 - 1901 - func defaultTargetBranch(branches []types.Branch, current string) string { 1902 - if slices.ContainsFunc(branches, func(b types.Branch) bool { return b.Reference.Name == current }) { 1903 - return current 1904 - } 1905 - if idx := slices.IndexFunc(branches, func(b types.Branch) bool { return b.IsDefault }); idx >= 0 { 1906 - return branches[idx].Reference.Name 1907 - } 1908 - return "" 1909 - } 1910 - 1911 - func defaultSourceBranch(source pages.Source, current string, branchChoices, forkBranches []types.Branch) string { 1912 - var candidates []types.Branch 1913 - switch source { 1914 - case pages.SourceFork: 1915 - candidates = forkBranches 1916 - case pages.SourceBranch: 1917 - candidates = branchChoices 1918 - default: 1919 - return current 1920 - } 1921 - if slices.ContainsFunc(candidates, func(b types.Branch) bool { return b.Reference.Name == current }) { 1922 - return current 1923 - } 1924 - if len(candidates) == 0 { 1925 - return "" 1926 - } 1927 - return candidates[0].Reference.Name 1928 - } 1929 - 1930 - func sortBranchesByRecency(branches []types.Branch) []types.Branch { 1931 - out := slices.Clone(branches) 1932 - sort.SliceStable(out, func(i, j int) bool { 1933 - if out[i].Commit == nil || out[j].Commit == nil { 1934 - return out[i].Commit != nil 1935 - } 1936 - return out[i].Commit.Committer.When.After(out[j].Commit.Committer.When) 1937 - }) 1938 - return out 1939 - } 1940 - 1941 - func (s *Pulls) prefetchComparison(r *http.Request, repo *models.Repo, source pages.Source, fork, targetBranch, sourceBranch, patch string) (*types.RepoFormatPatchResponse, *types.NiceDiff, error) { 1942 - var ( 1943 - comparison *types.RepoFormatPatchResponse 1944 - err error 1945 - ) 1946 - switch source { 1947 - case pages.SourcePatch: 1948 - if strings.TrimSpace(patch) == "" { 1949 - return nil, nil, nil 1950 - } 1951 - if verr := s.validator.ValidatePatch(&patch); verr != nil { 1952 - return nil, nil, fmt.Errorf("invalid patch: paste a valid git diff or format-patch") 1953 - } 1954 - comparison = parsePastedPatch(patch) 1955 - case pages.SourceBranch: 1956 - if targetBranch == "" || sourceBranch == "" { 1957 - return nil, nil, nil 1958 - } 1959 - comparison, err = s.fetchBranchComparison(r.Context(), repo, targetBranch, sourceBranch) 1960 - case pages.SourceFork: 1961 - if fork == "" || targetBranch == "" || sourceBranch == "" { 1962 - return nil, nil, nil 1963 - } 1964 - comparison, err = s.fetchForkComparison(r, fork, targetBranch, sourceBranch) 1965 - default: 1966 - return nil, nil, nil 1967 - } 1968 - if err != nil { 1969 - s.logger.With("handler", "prefetchComparison").Warn("failed to pre-fetch comparison", "err", err, "source", source) 1970 - return nil, nil, err 1971 - } 1972 - 1973 - return comparison, deriveDiff(comparison, targetBranch), nil 1974 - } 1975 - 1976 - func (s *Pulls) composeMergeCheck(ctx context.Context, repo *models.Repo, targetBranch string, comparison *types.RepoFormatPatchResponse) *types.MergeCheckResponse { 1977 - if comparison == nil || targetBranch == "" { 1978 - return nil 1979 - } 1980 - patch := comparison.CombinedPatchRaw 1981 - if patch == "" { 1982 - patch = comparison.FormatPatchRaw 1983 - } 1984 - if patch == "" { 1985 - return nil 1986 - } 1987 - 1988 - xrpcc := s.knotClient(repo.Knot) 1989 - 1990 - resp, err := tangled.RepoMergeCheck(ctx, xrpcc, &tangled.RepoMergeCheck_Input{ 1991 - Did: repo.Did, 1992 - Name: repo.Name, 1993 - Branch: targetBranch, 1994 - Patch: patch, 1995 - }) 1996 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1997 - s.logger.With("handler", "composeMergeCheck").Warn("failed to check mergeability", "xrpcerr", xrpcerr, "err", err, "target_branch", targetBranch) 1998 - return &types.MergeCheckResponse{Error: xrpcerr.Error()} 1999 - } 2000 - 2001 - out := mergeCheckResponseFrom(resp) 2002 - return &out 2003 - } 2004 - 2005 - func bracketComponents(key, prefix string) ([]string, bool) { 2006 - if !strings.HasPrefix(key, prefix) { 2007 - return nil, false 2008 - } 2009 - rest := key[len(prefix):] 2010 - var parts []string 2011 - for len(rest) > 0 { 2012 - if !strings.HasPrefix(rest, "[") { 2013 - return nil, false 2014 - } 2015 - end := strings.Index(rest, "]") 2016 - if end <= 0 { 2017 - return nil, false 2018 - } 2019 - parts = append(parts, rest[1:end]) 2020 - rest = rest[end+1:] 2021 - } 2022 - if len(parts) == 0 { 2023 - return nil, false 2024 - } 2025 - return parts, true 2026 - } 2027 - 2028 - func parseBracketedForm(form url.Values, prefix string) map[string]string { 2029 - out := make(map[string]string) 2030 - for key, vals := range form { 2031 - parts, ok := bracketComponents(key, prefix) 2032 - if !ok || len(parts) != 1 || parts[0] == "" || len(vals) == 0 { 2033 - continue 2034 - } 2035 - out[parts[0]] = vals[0] 2036 - } 2037 - return out 2038 - } 2039 - 2040 - func parseStackLabelForms(form url.Values) map[string]url.Values { 2041 - out := make(map[string]url.Values) 2042 - for key, vals := range form { 2043 - parts, ok := bracketComponents(key, "stackLabel") 2044 - if !ok || len(parts) != 2 || parts[0] == "" || parts[1] == "" { 2045 - continue 2046 - } 2047 - cid, atUri := parts[0], parts[1] 2048 - if _, ok := out[cid]; !ok { 2049 - out[cid] = make(url.Values) 2050 - } 2051 - out[cid][atUri] = append(out[cid][atUri], vals...) 2052 - } 2053 - return out 2054 - } 2055 - 2056 - func parsePastedPatch(patch string) *types.RepoFormatPatchResponse { 2057 - if patch == "" { 2058 - return nil 2059 - } 2060 - response := &types.RepoFormatPatchResponse{FormatPatchRaw: patch} 2061 - if patchutil.IsFormatPatch(patch) { 2062 - if patches, err := patchutil.ExtractPatches(patch); err == nil { 2063 - response.FormatPatch = patches 2064 - } 2065 - } 2066 - return response 2067 - } 2068 - 2069 - func (s *Pulls) fetchBranchComparison(ctx context.Context, repo *models.Repo, targetBranch, sourceBranch string) (*types.RepoFormatPatchResponse, error) { 2070 - xrpcc := s.knotClient(repo.Knot) 2071 - 2072 - xrpcBytes, err := tangled.RepoCompare(ctx, xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch) 2073 - if err != nil { 2074 - return nil, err 2075 - } 2076 - 2077 - var comparison types.RepoFormatPatchResponse 2078 - if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 2079 - return nil, err 2080 - } 2081 - return &comparison, nil 2082 - } 2083 - 2084 - func (s *Pulls) fetchForkComparison(r *http.Request, forkIdent, targetBranch, sourceBranch string) (*types.RepoFormatPatchResponse, error) { 2085 - parts := strings.SplitN(forkIdent, "/", 2) 2086 - if len(parts) != 2 { 2087 - return nil, fmt.Errorf("invalid fork identifier: %s", forkIdent) 2088 - } 2089 - fork, err := db.GetForkByDid(s.db, parts[0], parts[1]) 2090 - if err != nil { 2091 - return nil, err 2092 - } 2093 - 2094 - client, err := s.oauth.ServiceClient( 2095 - r, 2096 - oauth.WithService(fork.Knot), 2097 - oauth.WithLxm(tangled.RepoHiddenRefNSID), 2098 - oauth.WithDev(s.config.Core.Dev), 2099 - ) 2100 - if err != nil { 2101 - return nil, err 2102 - } 2103 - 2104 - resp, err := tangled.RepoHiddenRef( 2105 - r.Context(), 2106 - client, 2107 - &tangled.RepoHiddenRef_Input{ 2108 - ForkRef: sourceBranch, 2109 - RemoteRef: targetBranch, 2110 - Repo: fork.RepoAt().String(), 2111 - }, 2112 - ) 2113 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2114 - return nil, xrpcerr 2115 - } 2116 - if !resp.Success { 2117 - if resp.Error != nil { 2118 - return nil, fmt.Errorf("hidden ref failed: %s", *resp.Error) 2119 - } 2120 - return nil, fmt.Errorf("hidden ref failed") 2121 - } 2122 - 2123 - hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 2124 - forkXrpcc := s.knotClient(fork.Knot) 2125 - 2126 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 2127 - if err != nil { 2128 - return nil, err 2129 - } 2130 - 2131 - var comparison types.RepoFormatPatchResponse 2132 - if err := json.Unmarshal(forkXrpcBytes, &comparison); err != nil { 2133 - return nil, err 2134 - } 2135 - return &comparison, nil 2136 - } 2137 - 2138 - func stackPerCommitDiffs( 2139 - comparison *types.RepoFormatPatchResponse, 2140 - targetBranch, refreshUrl string, 2141 - stackSplits map[string]string, 2142 - ) []pages.StackedDiff { 2143 - if comparison == nil { 2144 - return nil 2145 - } 2146 - out := make([]pages.StackedDiff, len(comparison.FormatPatch)) 2147 - for i, p := range comparison.FormatPatch { 2148 - nd := patchutil.AsNiceDiff(p.Raw, targetBranch) 2149 - out[i].Diff = &nd 2150 - cid := p.ChangeIdOrEmpty() 2151 - if cid == "" { 2152 - continue 2153 - } 2154 - out[i].Opts = types.DiffOpts{ 2155 - Split: stackSplits[cid] == "split", 2156 - RefreshUrl: refreshUrl, 2157 - Target: fmt.Sprintf("#stack-diff-%s", cid), 2158 - Field: fmt.Sprintf("stackSplit[%s]", cid), 2159 - } 2160 - } 2161 - return out 2162 - } 2163 - 2164 - func deriveDiff(comparison *types.RepoFormatPatchResponse, targetBranch string) *types.NiceDiff { 2165 - if comparison == nil { 2166 - return nil 2167 - } 2168 - raw := comparison.CombinedPatchRaw 2169 - if raw == "" { 2170 - raw = comparison.FormatPatchRaw 2171 - } 2172 - d := patchutil.AsNiceDiff(raw, targetBranch) 2173 - return &d 2174 - } 2175 - 2176 - func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 2177 - l := s.logger.With("handler", "ResubmitPull") 2178 - 2179 - user := s.oauth.GetMultiAccountUser(r) 2180 - if user != nil { 2181 - l = l.With("user", user.Did) 2182 - } 2183 - 2184 - pull, ok := r.Context().Value("pull").(*models.Pull) 2185 - if !ok { 2186 - l.Error("failed to get pull") 2187 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2188 - return 2189 - } 2190 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 2191 - 2192 - switch r.Method { 2193 - case http.MethodGet: 2194 - s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 2195 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 2196 - Pull: pull, 2197 - }) 2198 - return 2199 - case http.MethodPost: 2200 - if pull.IsPatchBased() { 2201 - s.resubmitPatch(w, r) 2202 - return 2203 - } else if pull.IsBranchBased() { 2204 - s.resubmitBranch(w, r) 2205 - return 2206 - } else if pull.IsForkBased() { 2207 - s.resubmitFork(w, r) 2208 - return 2209 - } 2210 - } 2211 - } 2212 - 2213 - func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 2214 - l := s.logger.With("handler", "resubmitPatch") 2215 - 2216 - user := s.oauth.GetMultiAccountUser(r) 2217 - if user != nil { 2218 - l = l.With("user", user.Did) 2219 - } 2220 - 2221 - pull, ok := r.Context().Value("pull").(*models.Pull) 2222 - if !ok { 2223 - l.Error("failed to get pull") 2224 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2225 - return 2226 - } 2227 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 2228 - 2229 - if user == nil || user.Did != pull.OwnerDid { 2230 - l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 2231 - w.WriteHeader(http.StatusUnauthorized) 2232 - return 2233 - } 2234 - 2235 - f, err := s.repoResolver.Resolve(r) 2236 - if err != nil { 2237 - l.Error("failed to get repo and knot", "err", err) 2238 - return 2239 - } 2240 - 2241 - patch := r.FormValue("patch") 2242 - 2243 - s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, "", "") 2244 - } 2245 - 2246 - func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 2247 - l := s.logger.With("handler", "resubmitBranch") 2248 - 2249 - user := s.oauth.GetMultiAccountUser(r) 2250 - if user != nil { 2251 - l = l.With("user", user.Did) 2252 - } 2253 - 2254 - pull, ok := r.Context().Value("pull").(*models.Pull) 2255 - if !ok { 2256 - l.Error("failed to get pull") 2257 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 2258 - return 2259 - } 2260 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 2261 - 2262 - if user == nil || user.Did != pull.OwnerDid { 2263 - l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 2264 - w.WriteHeader(http.StatusUnauthorized) 2265 - return 2266 - } 2267 - 2268 - f, err := s.repoResolver.Resolve(r) 2269 - if err != nil { 2270 - l.Error("failed to get repo and knot", "err", err) 2271 - return 2272 - } 2273 - 2274 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.RepoIdentifier())} 2275 - if !roles.IsPushAllowed() { 2276 - l.Warn("unauthorized user - no push permission") 2277 - w.WriteHeader(http.StatusUnauthorized) 2278 - return 2279 - } 2280 - 2281 - xrpcc := s.knotClient(f.Knot) 2282 - 2283 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 2284 - if err != nil { 2285 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2286 - l.Error("failed to call XRPC repo.compare", "xrpcerr", xrpcerr, "err", err, "source_branch", pull.PullSource.Branch) 2287 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2288 - return 2289 - } 2290 - l.Error("compare request failed", "err", err, "source_branch", pull.PullSource.Branch) 2291 - s.pages.Notice(w, "resubmit-error", err.Error()) 2292 - return 2293 - } 2294 - 2295 - var comparison types.RepoFormatPatchResponse 2296 - if err := json.Unmarshal(xrpcBytes, &comparison); err != nil { 2297 - l.Error("failed to decode XRPC compare response", "err", err) 2298 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2299 - return 2300 - } 2301 - 2302 - sourceRev := comparison.Rev2 2303 - patch := comparison.FormatPatchRaw 2304 - combined := comparison.CombinedPatchRaw 2305 - 2306 - s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 2307 - } 2308 - 2309 - func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 2310 - l := s.logger.With("handler", "resubmitFork") 2311 - 2312 - user := s.oauth.GetMultiAccountUser(r) 2313 - if user != nil { 2314 - l = l.With("user", user.Did) 2315 - } 2316 - 2317 - pull, ok := r.Context().Value("pull").(*models.Pull) 2318 - if !ok { 2319 - l.Error("failed to get pull") 2320 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 2321 - return 2322 - } 2323 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "target_branch", pull.TargetBranch) 2324 - 2325 - if user == nil || user.Did != pull.OwnerDid { 2326 - l.Warn("unauthorized user", "actual_user", user.Did, "expected_owner", pull.OwnerDid) 2327 - w.WriteHeader(http.StatusUnauthorized) 2328 - return 2329 - } 2330 - 2331 - f, err := s.repoResolver.Resolve(r) 2332 - if err != nil { 2333 - l.Error("failed to get repo and knot", "err", err) 2334 - return 2335 - } 2336 - 2337 - forkRepo, err := db.GetRepoByDid(s.db, string(*pull.PullSource.RepoDid)) 2338 - if err != nil { 2339 - l.Error("failed to get source repo", "err", err, "repo_did", pull.PullSource.RepoDid.String()) 2340 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2341 - return 2342 - } 2343 - 2344 - // update the hidden tracking branch to latest 2345 - client, err := s.oauth.ServiceClient( 2346 - r, 2347 - oauth.WithService(forkRepo.Knot), 2348 - oauth.WithLxm(tangled.RepoHiddenRefNSID), 2349 - oauth.WithDev(s.config.Core.Dev), 2350 - ) 2351 - if err != nil { 2352 - l.Error("failed to connect to knot server", "err", err, "fork_knot", forkRepo.Knot) 2353 - return 2354 - } 2355 - 2356 - resp, err := tangled.RepoHiddenRef( 2357 - r.Context(), 2358 - client, 2359 - &tangled.RepoHiddenRef_Input{ 2360 - ForkRef: pull.PullSource.Branch, 2361 - RemoteRef: pull.TargetBranch, 2362 - Repo: forkRepo.RepoAt().String(), 2363 - }, 2364 - ) 2365 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2366 - s.logger.Error("failed to set hidden ref", "xrpcerr", xrpcerr, "err", err) 2367 - s.pages.Notice(w, "resubmit-error", xrpcerr.Error()) 2368 - return 2369 - } 2370 - if !resp.Success { 2371 - l.Error("failed to update tracking ref", "err", resp.Error, "fork_ref", pull.PullSource.Branch, "remote_ref", pull.TargetBranch) 2372 - s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 2373 - return 2374 - } 2375 - 2376 - hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 2377 - // extract patch by performing compare 2378 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), s.knotClient(forkRepo.Knot), forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 2379 - if err != nil { 2380 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2381 - l.Error("failed to call XRPC repo.compare for fork", "xrpcerr", xrpcerr, "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 2382 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2383 - return 2384 - } 2385 - l.Error("failed to compare branches", "err", err, "hidden_ref", hiddenRef, "source_branch", pull.PullSource.Branch) 2386 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2387 - return 2388 - } 2389 - 2390 - var forkComparison types.RepoFormatPatchResponse 2391 - if err := json.Unmarshal(forkXrpcBytes, &forkComparison); err != nil { 2392 - l.Error("failed to decode XRPC compare response for fork", "err", err) 2393 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2394 - return 2395 - } 2396 - 2397 - // Use the fork comparison we already made 2398 - comparison := forkComparison 2399 - 2400 - sourceRev := comparison.Rev2 2401 - patch := comparison.FormatPatchRaw 2402 - combined := comparison.CombinedPatchRaw 2403 - 2404 - s.resubmitPullHelper(w, r, f, syntax.DID(user.Did), pull, patch, combined, sourceRev) 2405 - } 2406 - 2407 - func (s *Pulls) resubmitPullHelper( 2408 - w http.ResponseWriter, 2409 - r *http.Request, 2410 - repo *models.Repo, 2411 - userDid syntax.DID, 2412 - pull *models.Pull, 2413 - patch string, 2414 - combined string, 2415 - sourceRev string, 2416 - ) { 2417 - l := s.logger.With("handler", "resubmitPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 2418 - 2419 - stack := r.Context().Value("stack").(models.Stack) 2420 - if stack != nil && len(stack) != 1 { 2421 - l.Info("resubmitting stacked PR", "stack_size", len(stack)) 2422 - s.resubmitStackedPullHelper(w, r, repo, userDid, pull, patch) 2423 - return 2424 - } 2425 - 2426 - if err := s.validator.ValidatePatch(&patch); err != nil { 2427 - s.pages.Notice(w, "resubmit-error", err.Error()) 2428 - return 2429 - } 2430 - 2431 - if patch == pull.LatestPatch() { 2432 - s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 2433 - return 2434 - } 2435 - 2436 - // validate sourceRev if branch/fork based 2437 - if pull.IsBranchBased() || pull.IsForkBased() { 2438 - if sourceRev == pull.LatestSha() { 2439 - s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 2440 - return 2441 - } 2442 - } 2443 - 2444 - pullAt := pull.AtUri() 2445 - newRoundNumber := len(pull.Submissions) 2446 - newPatch := patch 2447 - newSourceRev := sourceRev 2448 - combinedPatch := combined 2449 - 2450 - client, err := s.oauth.AuthorizedClient(r) 2451 - if err != nil { 2452 - l.Error("failed to authorize client", "err", err) 2453 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2454 - return 2455 - } 2456 - 2457 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, userDid.String(), pull.Rkey) 2458 - if err != nil { 2459 - // failed to get record 2460 - l.Error("failed to get record from PDS", "err", err, "rkey", pull.Rkey) 2461 - s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 2462 - return 2463 - } 2464 - 2465 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) 2466 - if err != nil { 2467 - l.Error("failed to upload patch blob", "err", err) 2468 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2469 - return 2470 - } 2471 - record := pull.AsRecord() 2472 - record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 2473 - CreatedAt: time.Now().Format(time.RFC3339), 2474 - PatchBlob: blob.Blob, 2475 - }) 2476 - 2477 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 2478 - Collection: tangled.RepoPullNSID, 2479 - Repo: userDid.String(), 2480 - Rkey: pull.Rkey, 2481 - SwapRecord: ex.Cid, 2482 - Record: &lexutil.LexiconTypeDecoder{ 2483 - Val: &record, 2484 - }, 2485 - }) 2486 - if err != nil { 2487 - l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey) 2488 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2489 - return 2490 - } 2491 - 2492 - err = db.ResubmitPull(s.db, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 2493 - if err != nil { 2494 - l.Error("failed to resubmit pull request in database", "err", err, "round_number", newRoundNumber) 2495 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2496 - return 2497 - } 2498 - 2499 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 2500 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2501 - } 2502 - 2503 - func (s *Pulls) resubmitStackedPullHelper( 2504 - w http.ResponseWriter, 2505 - r *http.Request, 2506 - repo *models.Repo, 2507 - userDid syntax.DID, 2508 - pull *models.Pull, 2509 - patch string, 2510 - ) { 2511 - l := s.logger.With("handler", "resubmitStackedPullHelper", "user", userDid, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 2512 - 2513 - targetBranch := pull.TargetBranch 2514 - 2515 - origStack, _ := r.Context().Value("stack").(models.Stack) 2516 - 2517 - formatPatches, err := patchutil.ExtractPatches(patch) 2518 - if err != nil { 2519 - l.Error("failed to extract patches", "err", err) 2520 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Failed to parse patches.") 2521 - return 2522 - } 2523 - 2524 - // must have atleast 1 patch to begin with 2525 - if len(formatPatches) == 0 { 2526 - l.Error("no patches found in the generated format-patch") 2527 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request: No patches found in the generated patch.") 2528 - return 2529 - } 2530 - 2531 - client, err := s.oauth.AuthorizedClient(r) 2532 - if err != nil { 2533 - l.Error("failed to get authorized client", "err", err) 2534 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 2535 - return 2536 - } 2537 - 2538 - // first upload all blobs 2539 - blobs := make([]*lexutil.LexBlob, len(formatPatches)) 2540 - for i, p := range formatPatches { 2541 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.Raw), ApplicationGzip) 2542 - if err != nil { 2543 - l.Error("failed to upload patch blob", "err", err, "patch_index", i) 2544 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 2545 - return 2546 - } 2547 - l.Info("uploaded blob", "idx", i+1, "total", len(formatPatches)) 2548 - blobs[i] = blob.Blob 2549 - } 2550 - 2551 - newStack, err := s.newStack(r.Context(), repo, userDid, targetBranch, pull.PullSource, formatPatches, blobs, nil, nil) 2552 - if err != nil { 2553 - l.Error("failed to create resubmitted stack", "err", err) 2554 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2555 - return 2556 - } 2557 - 2558 - // find the diff between the stacks, first, map them by changeId 2559 - origById := make(map[string]*models.Pull) 2560 - newById := make(map[string]*models.Pull) 2561 - for _, p := range origStack { 2562 - origById[p.LatestSubmission().ChangeId()] = p 2563 - } 2564 - for _, p := range newStack { 2565 - newById[p.LatestSubmission().ChangeId()] = p 2566 - } 2567 - 2568 - // commits that got deleted: corresponding pull is closed 2569 - // commits that got added: new pull is created 2570 - // commits that got updated: corresponding pull is resubmitted & new round begins 2571 - additions := make(map[string]*models.Pull) 2572 - deletions := make(map[string]*models.Pull) 2573 - updated := make(map[string]struct{}) 2574 - 2575 - // pulls in original stack but not in new one 2576 - for _, op := range origStack { 2577 - if _, ok := newById[op.LatestSubmission().ChangeId()]; !ok { 2578 - deletions[op.LatestSubmission().ChangeId()] = op 2579 - } 2580 - } 2581 - 2582 - // pulls in new stack but not in original one 2583 - for _, np := range newStack { 2584 - if _, ok := origById[np.LatestSubmission().ChangeId()]; !ok { 2585 - additions[np.LatestSubmission().ChangeId()] = np 2586 - } 2587 - } 2588 - 2589 - // NOTE: this loop can be written in any of above blocks, 2590 - // but is written separately in the interest of simpler code 2591 - for _, np := range newStack { 2592 - if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 2593 - // pull exists in both stacks 2594 - updated[op.LatestSubmission().ChangeId()] = struct{}{} 2595 - } 2596 - } 2597 - 2598 - // NOTE: we can go through the newStack and update dependent relations and 2599 - // rkeys now that we know which ones have been updated 2600 - // update dependentOn relations for the entire stack 2601 - var parentAt *syntax.ATURI 2602 - for _, np := range newStack { 2603 - if op, ok := origById[np.LatestSubmission().ChangeId()]; ok { 2604 - // pull exists in both stacks 2605 - np.Rkey = op.Rkey 2606 - } 2607 - np.DependentOn = parentAt 2608 - x := np.AtUri() 2609 - parentAt = &x 2610 - } 2611 - 2612 - l = l.With("additions", len(additions), "deletions", len(deletions), "updates", len(updated)) 2613 - 2614 - tx, err := s.db.Begin() 2615 - if err != nil { 2616 - l.Error("failed to start transaction", "err", err) 2617 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2618 - return 2619 - } 2620 - defer tx.Rollback() 2621 - 2622 - // pds updates to make 2623 - var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 2624 - 2625 - // deleted pulls are marked as deleted in the DB 2626 - for _, p := range deletions { 2627 - // do not do delete already merged PRs 2628 - if p.State == models.PullMerged { 2629 - continue 2630 - } 2631 - 2632 - err := db.AbandonPulls(tx, orm.FilterEq("repo_did", string(p.RepoDid)), orm.FilterEq("at_uri", p.AtUri())) 2633 - if err != nil { 2634 - l.Error("failed to delete pull", "err", err, "pull_id", p.PullId) 2635 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2636 - return 2637 - } 2638 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2639 - RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 2640 - Collection: tangled.RepoPullNSID, 2641 - Rkey: p.Rkey, 2642 - }, 2643 - }) 2644 - } 2645 - 2646 - // new pulls are created 2647 - for _, p := range additions { 2648 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()), ApplicationGzip) 2649 - if err != nil { 2650 - l.Error("failed to upload patch blob for new pull", "err", err, "change_id", p.LatestSubmission().ChangeId()) 2651 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2652 - return 2653 - } 2654 - p.Submissions[0].Blob = *blob.Blob 2655 - 2656 - if err = db.PutPull(tx, p); err != nil { 2657 - l.Error("failed to create pull", "err", err, "pull_id", p.PullId, "change_id", p.LatestSubmission().ChangeId()) 2658 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2659 - return 2660 - } 2661 - 2662 - record := p.AsRecord() 2663 - record.Rounds = []*tangled.RepoPull_Round{ 2664 - { 2665 - CreatedAt: time.Now().Format(time.RFC3339), 2666 - PatchBlob: blob.Blob, 2667 - }, 2668 - } 2669 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2670 - RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2671 - Collection: tangled.RepoPullNSID, 2672 - Rkey: &p.Rkey, 2673 - Value: &lexutil.LexiconTypeDecoder{ 2674 - Val: &record, 2675 - }, 2676 - }, 2677 - }) 2678 - } 2679 - 2680 - // updated pulls are, well, updated; to start a new round 2681 - for id := range updated { 2682 - op, _ := origById[id] 2683 - np, _ := newById[id] 2684 - 2685 - // do not update already merged PRs 2686 - if op.State == models.PullMerged { 2687 - continue 2688 - } 2689 - 2690 - // resubmit the new pull 2691 - np.Rkey = op.Rkey 2692 - pullAt := op.AtUri() 2693 - newRoundNumber := len(op.Submissions) 2694 - newPatch := np.LatestPatch() 2695 - combinedPatch := np.LatestSubmission().Combined 2696 - newSourceRev := np.LatestSha() 2697 - 2698 - blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(newPatch), ApplicationGzip) 2699 - if err != nil { 2700 - l.Error("failed to upload patch blob for update", "err", err, "change_id", id, "pull_id", op.PullId) 2701 - s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2702 - return 2703 - } 2704 - 2705 - // create new round 2706 - err = db.ResubmitPull(tx, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Blob) 2707 - if err != nil { 2708 - l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 2709 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2710 - return 2711 - } 2712 - 2713 - // update dependent-on relation 2714 - if np.DependentOn != nil { 2715 - err := db.SetDependentOn(tx, *np.DependentOn, orm.FilterEq("at_uri", np.AtUri())) 2716 - if err != nil { 2717 - l.Error("failed to update pull in database", "err", err, "pull_id", op.PullId, "round_number", newRoundNumber) 2718 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2719 - return 2720 - } 2721 - } 2722 - 2723 - record := np.AsRecord() 2724 - record.Rounds = op.AsRecord().Rounds 2725 - record.Rounds = append(record.Rounds, &tangled.RepoPull_Round{ 2726 - CreatedAt: time.Now().Format(time.RFC3339), 2727 - PatchBlob: blob.Blob, 2728 - }) 2729 - writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2730 - RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2731 - Collection: tangled.RepoPullNSID, 2732 - Rkey: op.Rkey, 2733 - Value: &lexutil.LexiconTypeDecoder{ 2734 - Val: &record, 2735 - }, 2736 - }, 2737 - }) 2738 - } 2739 - 2740 - _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2741 - Repo: userDid.String(), 2742 - Writes: writes, 2743 - }) 2744 - if err != nil { 2745 - l.Error("failed to apply writes for stacked pull request", "err", err, "writes_count", len(writes)) 2746 - s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 2747 - return 2748 - } 2749 - 2750 - err = tx.Commit() 2751 - if err != nil { 2752 - l.Error("failed to commit resubmit transaction", "err", err) 2753 - s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2754 - return 2755 - } 2756 - 2757 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 2758 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2759 - } 2760 - 2761 - func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 2762 - l := s.logger.With("handler", "MergePull") 2763 - 2764 - user := s.oauth.GetMultiAccountUser(r) 2765 - if user != nil { 2766 - l = l.With("user", user.Did) 2767 - } 2768 - 2769 - f, err := s.repoResolver.Resolve(r) 2770 - if err != nil { 2771 - l.Error("failed to resolve repo", "err", err) 2772 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2773 - return 2774 - } 2775 - l = l.With("repo_at", f.RepoAt().String()) 2776 - 2777 - pull, ok := r.Context().Value("pull").(*models.Pull) 2778 - if !ok { 2779 - l.Error("failed to get pull") 2780 - s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 2781 - return 2782 - } 2783 - l = l.With("pull_id", pull.PullId, "target_branch", pull.TargetBranch) 2784 - 2785 - stack, ok := r.Context().Value("stack").(models.Stack) 2786 - if !ok { 2787 - l.Error("failed to get stack") 2788 - s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 2789 - return 2790 - } 2791 - 2792 - // combine patches of substack 2793 - subStack := stack.Below(pull) 2794 - // collect the portion of the stack that is mergeable 2795 - pullsToMerge := subStack.Mergeable() 2796 - l = l.With("pulls_to_merge", len(pullsToMerge)) 2797 - 2798 - patch := pullsToMerge.CombinedPatch() 2799 - 2800 - ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 2801 - if err != nil { 2802 - l.Error("failed to resolve identity", "err", err, "owner_did", pull.OwnerDid) 2803 - w.WriteHeader(http.StatusNotFound) 2804 - return 2805 - } 2806 - 2807 - email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 2808 - if err != nil { 2809 - l.Warn("failed to get primary email", "err", err, "owner_did", pull.OwnerDid) 2810 - } 2811 - 2812 - authorName := ident.Handle.String() 2813 - mergeInput := &tangled.RepoMerge_Input{ 2814 - Did: f.Did, 2815 - Name: f.Name, 2816 - Branch: pull.TargetBranch, 2817 - Patch: patch, 2818 - CommitMessage: &pull.Title, 2819 - AuthorName: &authorName, 2820 - } 2821 - 2822 - if pull.Body != "" { 2823 - mergeInput.CommitBody = &pull.Body 2824 - } 2825 - 2826 - if email.Address != "" { 2827 - mergeInput.AuthorEmail = &email.Address 2828 - } 2829 - 2830 - client, err := s.oauth.ServiceClient( 2831 - r, 2832 - oauth.WithService(f.Knot), 2833 - oauth.WithLxm(tangled.RepoMergeNSID), 2834 - oauth.WithDev(s.config.Core.Dev), 2835 - oauth.WithTimeout(time.Second*20), // merge is quite slow on large repos, like witchsky 2836 - ) 2837 - if err != nil { 2838 - l.Error("failed to connect to knot server", "err", err, "knot", f.Knot) 2839 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2840 - return 2841 - } 2842 - 2843 - err = tangled.RepoMerge(r.Context(), client, mergeInput) 2844 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2845 - s.logger.Error("failed to merge", "xrpcerr", xrpcerr, "err", err) 2846 - s.pages.Notice(w, "pull-merge-error", xrpcerr.Error()) 2847 - return 2848 - } 2849 - 2850 - tx, err := s.db.Begin() 2851 - if err != nil { 2852 - l.Error("failed to start transaction", "err", err) 2853 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2854 - return 2855 - } 2856 - defer tx.Rollback() 2857 - 2858 - var atUris []syntax.ATURI 2859 - for _, p := range pullsToMerge { 2860 - atUris = append(atUris, p.AtUri()) 2861 - p.State = models.PullMerged 2862 - } 2863 - err = db.MergePulls(tx, orm.FilterEq("repo_did", string(f.RepoDid)), orm.FilterIn("at_uri", atUris)) 2864 - if err != nil { 2865 - l.Error("failed to update pull request status in database", "err", err) 2866 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2867 - return 2868 - } 2869 - 2870 - err = tx.Commit() 2871 - if err != nil { 2872 - // TODO: this is unsound, we should also revert the merge from the knotserver here 2873 - l.Error("failed to commit merge transaction", "err", err) 2874 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2875 - return 2876 - } 2877 - 2878 - // notify about the pull merge 2879 - for _, p := range pullsToMerge { 2880 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2881 - } 2882 - 2883 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2884 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2885 - } 2886 - 2887 - func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2888 - l := s.logger.With("handler", "ClosePull") 2889 - 2890 - user := s.oauth.GetMultiAccountUser(r) 2891 - if user != nil { 2892 - l = l.With("user", user.Did) 2893 - } 2894 - 2895 - f, err := s.repoResolver.Resolve(r) 2896 - if err != nil { 2897 - l.Error("failed to resolve repo", "err", err) 2898 - return 2899 - } 2900 - 2901 - pull, ok := r.Context().Value("pull").(*models.Pull) 2902 - if !ok { 2903 - l.Error("failed to get pull") 2904 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2905 - return 2906 - } 2907 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 2908 - 2909 - // auth filter: only owner or collaborators can close 2910 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.RepoIdentifier())} 2911 - isOwner := roles.IsOwner() 2912 - isCollaborator := roles.IsCollaborator() 2913 - isPullAuthor := user.Did == pull.OwnerDid 2914 - isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2915 - if !isCloseAllowed { 2916 - l.Error("unauthorized to close pull", "is_owner", isOwner, "is_collaborator", isCollaborator, "is_pull_author", isPullAuthor) 2917 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2918 - return 2919 - } 2920 - 2921 - // Start a transaction 2922 - tx, err := s.db.BeginTx(r.Context(), nil) 2923 - if err != nil { 2924 - l.Error("failed to start transaction", "err", err) 2925 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 2926 - return 2927 - } 2928 - defer tx.Rollback() 2929 - 2930 - // if this PR is stacked, then we want to close all PRs above this one on the stack 2931 - stack := r.Context().Value("stack").(models.Stack) 2932 - pullsToClose := stack.Above(pull) 2933 - var atUris []syntax.ATURI 2934 - for _, p := range pullsToClose { 2935 - atUris = append(atUris, p.AtUri()) 2936 - p.State = models.PullClosed 2937 - } 2938 - err = db.ClosePulls( 2939 - tx, 2940 - orm.FilterEq("repo_did", string(f.RepoDid)), 2941 - orm.FilterIn("at_uri", atUris), 2942 - ) 2943 - if err != nil { 2944 - l.Error("failed to close pulls in database", "err", err, "pulls_to_close", len(pullsToClose)) 2945 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 2946 - } 2947 - 2948 - // Commit the transaction 2949 - if err = tx.Commit(); err != nil { 2950 - l.Error("failed to commit transaction", "err", err) 2951 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 2952 - return 2953 - } 2954 - 2955 - for _, p := range pullsToClose { 2956 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2957 - } 2958 - 2959 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2960 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2961 - } 2962 - 2963 - func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2964 - l := s.logger.With("handler", "ReopenPull") 2965 - 2966 - user := s.oauth.GetMultiAccountUser(r) 2967 - if user != nil { 2968 - l = l.With("user", user.Did) 2969 - } 2970 - 2971 - f, err := s.repoResolver.Resolve(r) 2972 - if err != nil { 2973 - l.Error("failed to resolve repo", "err", err) 2974 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2975 - return 2976 - } 2977 - 2978 - pull, ok := r.Context().Value("pull").(*models.Pull) 2979 - if !ok { 2980 - l.Error("failed to get pull") 2981 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2982 - return 2983 - } 2984 - l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "state", pull.State) 2985 - 2986 - // auth filter: only owner or collaborators can close 2987 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.RepoIdentifier())} 2988 - isOwner := roles.IsOwner() 2989 - isCollaborator := roles.IsCollaborator() 2990 - isPullAuthor := user.Did == pull.OwnerDid 2991 - isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2992 - if !isCloseAllowed { 2993 - l.Error("unauthorized to reopen pull", "is_owner", isOwner, "is_collaborator", isCollaborator, "is_pull_author", isPullAuthor) 2994 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2995 - return 2996 - } 2997 - 2998 - // Start a transaction 2999 - tx, err := s.db.BeginTx(r.Context(), nil) 3000 - if err != nil { 3001 - l.Error("failed to start transaction", "err", err) 3002 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 3003 - return 3004 - } 3005 - defer tx.Rollback() 3006 - 3007 - // if this PR is stacked, then we want to reopen all PRs above this one on the stack 3008 - stack := r.Context().Value("stack").(models.Stack) 3009 - pullsToReopen := stack.Below(pull) 3010 - var atUris []syntax.ATURI 3011 - for _, p := range pullsToReopen { 3012 - atUris = append(atUris, p.AtUri()) 3013 - p.State = models.PullOpen 3014 - } 3015 - err = db.ReopenPulls( 3016 - tx, 3017 - orm.FilterEq("repo_did", string(f.RepoDid)), 3018 - orm.FilterIn("at_uri", atUris), 3019 - ) 3020 - if err != nil { 3021 - l.Error("failed to reopen pulls in database", "err", err, "pulls_to_reopen", len(pullsToReopen)) 3022 - s.pages.Notice(w, "pull-close", "Failed to reopen pull.") 3023 - } 3024 - 3025 - // Commit the transaction 3026 - if err = tx.Commit(); err != nil { 3027 - l.Error("failed to commit transaction", "err", err) 3028 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 3029 - return 3030 - } 3031 - 3032 - for _, p := range pullsToReopen { 3033 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 3034 - } 3035 - 3036 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 3037 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 3038 - } 3039 - 3040 - func (s *Pulls) newStack( 3041 - ctx context.Context, 3042 - repo *models.Repo, 3043 - userDid syntax.DID, 3044 - targetBranch string, 3045 - pullSource *models.PullSource, 3046 - formatPatches []types.FormatPatch, 3047 - blobs []*lexutil.LexBlob, 3048 - stackTitles, stackBodies map[string]string, 3049 - ) (models.Stack, error) { 3050 - var stack models.Stack 3051 - var parentAtUri *syntax.ATURI 3052 - for i, fp := range formatPatches { 3053 - // all patches must have a jj change-id 3054 - cid, err := fp.ChangeId() 3055 - if err != nil { 3056 - return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 3057 - } 3058 - 3059 - title := fp.Title 3060 - body := fp.Body 3061 - if override, ok := stackTitles[cid]; ok && strings.TrimSpace(override) != "" { 3062 - title = override 3063 - } 3064 - if override, ok := stackBodies[cid]; ok { 3065 - body = override 3066 - } 3067 - rkey := tid.TID() 3068 - 3069 - mentions, references := s.mentionsResolver.Resolve(ctx, body) 3070 - 3071 - now := time.Now() 3072 - 3073 - pull := models.Pull{ 3074 - Title: title, 3075 - Body: body, 3076 - TargetBranch: targetBranch, 3077 - OwnerDid: userDid.String(), 3078 - RepoDid: syntax.DID(repo.RepoDid), 3079 - Rkey: rkey, 3080 - Mentions: mentions, 3081 - References: references, 3082 - Submissions: []*models.PullSubmission{ 3083 - { 3084 - Patch: fp.Raw, 3085 - SourceRev: fp.SHA, 3086 - Combined: fp.Raw, 3087 - Blob: *blobs[i], 3088 - Created: now, 3089 - }, 3090 - }, 3091 - PullSource: pullSource, 3092 - Created: now, 3093 - State: models.PullOpen, 3094 - 3095 - DependentOn: parentAtUri, 3096 - Repo: repo, 3097 - } 3098 - 3099 - stack = append(stack, &pull) 3100 - 3101 - parent := pull.AtUri() 3102 - parentAtUri = &parent 3103 - } 3104 - 3105 - return stack, nil 3106 - } 3107 - 3108 84 func gz(s string) io.Reader { 3109 85 var b bytes.Buffer 3110 86 w := gzip.NewWriter(&b) ··· 3114 90 } 3115 91 3116 92 func ptrPullState(s models.PullState) *models.PullState { return &s } 3117 - 3118 - func repoPullTarget(repo *models.Repo, branch string) *tangled.RepoPull_Target { 3119 - return &tangled.RepoPull_Target{ 3120 - Branch: branch, 3121 - Repo: repo.RepoDid, 3122 - } 3123 - }
appview/repo/artifact.go

This file has not been changed.

appview/repo/feed.go

This file has not been changed.

appview/repo/index.go

This file has not been changed.

appview/repo/opengraph.go

This file has not been changed.

appview/repo/repo.go

This file has not been changed.

appview/repo/repo_util.go

This file has not been changed.

appview/repo/router.go

This file has not been changed.

appview/repo/settings.go

This file has not been changed.

appview/repo/tags.go

This file has not been changed.

appview/repo/webhooks.go

This file has not been changed.

appview/reporesolver/resolver.go

This file has not been changed.

appview/settings/settings.go

This file has not been changed.

appview/sites/sites.go

This file has not been changed.

appview/state/knotstream.go

This file has not been changed.

appview/state/profile.go

This file has not been changed.

appview/state/router.go

This file has not been changed.

appview/state/state.go

This file has not been changed.

appview/strings/strings.go

This file has not been changed.

History

9 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
merge conflicts detected
expand
  • api/tangled/cbor_gen.go:866
  • api/tangled/feedstar.go:5
  • api/tangled/gitrefUpdate.go:29
  • api/tangled/repocollaborator.go:19
  • api/tangled/repoissue.go:22
  • api/tangled/repopull.go:39
  • api/tangled/tangledrepo.go:24
  • cmd/cborgen/cborgen.go:17
  • knotserver/xrpc/merge.go:118
  • lexicons/feed/star.json:10
  • lexicons/git/refUpdate.json:11
  • lexicons/issue/issue.json:9
  • lexicons/pulls/pull.json:65
  • lexicons/repo/collaborator.json:11
  • lexicons/repo/repo.json:6
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments
1 commit
expand
appview: handlers, state, and templates for more record types w/ repoDID
expand 0 comments