Monorepo for Tangled tangled.org

appview: reporesolver: factor out ResolvedRepo

Rename FullyResolvedRepo -> ResolvedRepo

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi b8fa24f6 1dccac8e

verified
Changed files
+2529 -385
appview
+2107
appview/pulls/pulls.go
··· 1 + package pulls 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "log" 10 + "net/http" 11 + "sort" 12 + "strconv" 13 + "strings" 14 + "time" 15 + 16 + "tangled.sh/tangled.sh/core/api/tangled" 17 + "tangled.sh/tangled.sh/core/appview" 18 + "tangled.sh/tangled.sh/core/appview/db" 19 + "tangled.sh/tangled.sh/core/appview/oauth" 20 + "tangled.sh/tangled.sh/core/appview/pages" 21 + "tangled.sh/tangled.sh/core/appview/reporesolver" 22 + "tangled.sh/tangled.sh/core/knotclient" 23 + "tangled.sh/tangled.sh/core/patchutil" 24 + "tangled.sh/tangled.sh/core/types" 25 + 26 + "github.com/bluekeyes/go-gitdiff/gitdiff" 27 + comatproto "github.com/bluesky-social/indigo/api/atproto" 28 + "github.com/bluesky-social/indigo/atproto/syntax" 29 + lexutil "github.com/bluesky-social/indigo/lex/util" 30 + "github.com/go-chi/chi/v5" 31 + "github.com/google/uuid" 32 + "github.com/posthog/posthog-go" 33 + ) 34 + 35 + type Pulls struct { 36 + oauth *oauth.OAuth 37 + repoResolver *reporesolver.RepoResolver 38 + pages *pages.Pages 39 + resolver *appview.Resolver 40 + db *db.DB 41 + config *appview.Config 42 + posthog posthog.Client 43 + } 44 + 45 + func New(oauth *oauth.OAuth, repoResolver *reporesolver.RepoResolver, pages *pages.Pages, resolver *appview.Resolver, db *db.DB, config *appview.Config) *Pulls { 46 + return &Pulls{oauth: oauth, repoResolver: repoResolver, pages: pages, resolver: resolver, db: db, config: config} 47 + } 48 + 49 + // htmx fragment 50 + func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 51 + switch r.Method { 52 + case http.MethodGet: 53 + user := s.oauth.GetUser(r) 54 + f, err := s.repoResolver.Resolve(r) 55 + if err != nil { 56 + log.Println("failed to get repo and knot", err) 57 + return 58 + } 59 + 60 + pull, ok := r.Context().Value("pull").(*db.Pull) 61 + if !ok { 62 + log.Println("failed to get pull") 63 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 64 + return 65 + } 66 + 67 + // can be nil if this pull is not stacked 68 + stack, _ := r.Context().Value("stack").(db.Stack) 69 + 70 + roundNumberStr := chi.URLParam(r, "round") 71 + roundNumber, err := strconv.Atoi(roundNumberStr) 72 + if err != nil { 73 + roundNumber = pull.LastRoundNumber() 74 + } 75 + if roundNumber >= len(pull.Submissions) { 76 + http.Error(w, "bad round id", http.StatusBadRequest) 77 + log.Println("failed to parse round id", err) 78 + return 79 + } 80 + 81 + mergeCheckResponse := s.mergeCheck(f, pull, stack) 82 + resubmitResult := pages.Unknown 83 + if user.Did == pull.OwnerDid { 84 + resubmitResult = s.resubmitCheck(f, pull, stack) 85 + } 86 + 87 + s.pages.PullActionsFragment(w, pages.PullActionsParams{ 88 + LoggedInUser: user, 89 + RepoInfo: f.RepoInfo(user), 90 + Pull: pull, 91 + RoundNumber: roundNumber, 92 + MergeCheck: mergeCheckResponse, 93 + ResubmitCheck: resubmitResult, 94 + Stack: stack, 95 + }) 96 + return 97 + } 98 + } 99 + 100 + func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 101 + user := s.oauth.GetUser(r) 102 + f, err := s.repoResolver.Resolve(r) 103 + if err != nil { 104 + log.Println("failed to get repo and knot", err) 105 + return 106 + } 107 + 108 + pull, ok := r.Context().Value("pull").(*db.Pull) 109 + if !ok { 110 + log.Println("failed to get pull") 111 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 112 + return 113 + } 114 + 115 + // can be nil if this pull is not stacked 116 + stack, _ := r.Context().Value("stack").(db.Stack) 117 + abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*db.Pull) 118 + 119 + totalIdents := 1 120 + for _, submission := range pull.Submissions { 121 + totalIdents += len(submission.Comments) 122 + } 123 + 124 + identsToResolve := make([]string, totalIdents) 125 + 126 + // populate idents 127 + identsToResolve[0] = pull.OwnerDid 128 + idx := 1 129 + for _, submission := range pull.Submissions { 130 + for _, comment := range submission.Comments { 131 + identsToResolve[idx] = comment.OwnerDid 132 + idx += 1 133 + } 134 + } 135 + 136 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 137 + didHandleMap := make(map[string]string) 138 + for _, identity := range resolvedIds { 139 + if !identity.Handle.IsInvalidHandle() { 140 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 141 + } else { 142 + didHandleMap[identity.DID.String()] = identity.DID.String() 143 + } 144 + } 145 + 146 + mergeCheckResponse := s.mergeCheck(f, pull, stack) 147 + resubmitResult := pages.Unknown 148 + if user != nil && user.Did == pull.OwnerDid { 149 + resubmitResult = s.resubmitCheck(f, pull, stack) 150 + } 151 + 152 + s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 153 + LoggedInUser: user, 154 + RepoInfo: f.RepoInfo(user), 155 + DidHandleMap: didHandleMap, 156 + Pull: pull, 157 + Stack: stack, 158 + AbandonedPulls: abandonedPulls, 159 + MergeCheck: mergeCheckResponse, 160 + ResubmitCheck: resubmitResult, 161 + }) 162 + } 163 + 164 + func (s *Pulls) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse { 165 + if pull.State == db.PullMerged { 166 + return types.MergeCheckResponse{} 167 + } 168 + 169 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 170 + if err != nil { 171 + log.Printf("failed to get registration key: %v", err) 172 + return types.MergeCheckResponse{ 173 + Error: "failed to check merge status: this knot is unregistered", 174 + } 175 + } 176 + 177 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 178 + if err != nil { 179 + log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 180 + return types.MergeCheckResponse{ 181 + Error: "failed to check merge status", 182 + } 183 + } 184 + 185 + patch := pull.LatestPatch() 186 + if pull.IsStacked() { 187 + // combine patches of substack 188 + subStack := stack.Below(pull) 189 + // collect the portion of the stack that is mergeable 190 + mergeable := subStack.Mergeable() 191 + // combine each patch 192 + patch = mergeable.CombinedPatch() 193 + } 194 + 195 + resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) 196 + if err != nil { 197 + log.Println("failed to check for mergeability:", err) 198 + return types.MergeCheckResponse{ 199 + Error: "failed to check merge status", 200 + } 201 + } 202 + switch resp.StatusCode { 203 + case 404: 204 + return types.MergeCheckResponse{ 205 + Error: "failed to check merge status: this knot does not support PRs", 206 + } 207 + case 400: 208 + return types.MergeCheckResponse{ 209 + Error: "failed to check merge status: does this knot support PRs?", 210 + } 211 + } 212 + 213 + respBody, err := io.ReadAll(resp.Body) 214 + if err != nil { 215 + log.Println("failed to read merge check response body") 216 + return types.MergeCheckResponse{ 217 + Error: "failed to check merge status: knot is not speaking the right language", 218 + } 219 + } 220 + defer resp.Body.Close() 221 + 222 + var mergeCheckResponse types.MergeCheckResponse 223 + err = json.Unmarshal(respBody, &mergeCheckResponse) 224 + if err != nil { 225 + log.Println("failed to unmarshal merge check response", err) 226 + return types.MergeCheckResponse{ 227 + Error: "failed to check merge status: knot is not speaking the right language", 228 + } 229 + } 230 + 231 + return mergeCheckResponse 232 + } 233 + 234 + func (s *Pulls) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult { 235 + if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil { 236 + return pages.Unknown 237 + } 238 + 239 + var knot, ownerDid, repoName string 240 + 241 + if pull.PullSource.RepoAt != nil { 242 + // fork-based pulls 243 + sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 244 + if err != nil { 245 + log.Println("failed to get source repo", err) 246 + return pages.Unknown 247 + } 248 + 249 + knot = sourceRepo.Knot 250 + ownerDid = sourceRepo.Did 251 + repoName = sourceRepo.Name 252 + } else { 253 + // pulls within the same repo 254 + knot = f.Knot 255 + ownerDid = f.OwnerDid() 256 + repoName = f.RepoName 257 + } 258 + 259 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 260 + if err != nil { 261 + log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 262 + return pages.Unknown 263 + } 264 + 265 + result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 266 + if err != nil { 267 + log.Println("failed to reach knotserver", err) 268 + return pages.Unknown 269 + } 270 + 271 + latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev 272 + 273 + if pull.IsStacked() && stack != nil { 274 + top := stack[0] 275 + latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev 276 + } 277 + 278 + log.Println(latestSourceRev, result.Branch.Hash) 279 + 280 + if latestSourceRev != result.Branch.Hash { 281 + return pages.ShouldResubmit 282 + } 283 + 284 + return pages.ShouldNotResubmit 285 + } 286 + 287 + func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 288 + user := s.oauth.GetUser(r) 289 + f, err := s.repoResolver.Resolve(r) 290 + if err != nil { 291 + log.Println("failed to get repo and knot", err) 292 + return 293 + } 294 + 295 + pull, ok := r.Context().Value("pull").(*db.Pull) 296 + if !ok { 297 + log.Println("failed to get pull") 298 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 299 + return 300 + } 301 + 302 + stack, _ := r.Context().Value("stack").(db.Stack) 303 + 304 + roundId := chi.URLParam(r, "round") 305 + roundIdInt, err := strconv.Atoi(roundId) 306 + if err != nil || roundIdInt >= len(pull.Submissions) { 307 + http.Error(w, "bad round id", http.StatusBadRequest) 308 + log.Println("failed to parse round id", err) 309 + return 310 + } 311 + 312 + identsToResolve := []string{pull.OwnerDid} 313 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 314 + didHandleMap := make(map[string]string) 315 + for _, identity := range resolvedIds { 316 + if !identity.Handle.IsInvalidHandle() { 317 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 318 + } else { 319 + didHandleMap[identity.DID.String()] = identity.DID.String() 320 + } 321 + } 322 + 323 + patch := pull.Submissions[roundIdInt].Patch 324 + diff := patchutil.AsNiceDiff(patch, pull.TargetBranch) 325 + 326 + s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 327 + LoggedInUser: user, 328 + DidHandleMap: didHandleMap, 329 + RepoInfo: f.RepoInfo(user), 330 + Pull: pull, 331 + Stack: stack, 332 + Round: roundIdInt, 333 + Submission: pull.Submissions[roundIdInt], 334 + Diff: &diff, 335 + }) 336 + 337 + } 338 + 339 + func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 340 + user := s.oauth.GetUser(r) 341 + 342 + f, err := s.repoResolver.Resolve(r) 343 + if err != nil { 344 + log.Println("failed to get repo and knot", err) 345 + return 346 + } 347 + 348 + pull, ok := r.Context().Value("pull").(*db.Pull) 349 + if !ok { 350 + log.Println("failed to get pull") 351 + s.pages.Notice(w, "pull-error", "Failed to get pull.") 352 + return 353 + } 354 + 355 + roundId := chi.URLParam(r, "round") 356 + roundIdInt, err := strconv.Atoi(roundId) 357 + if err != nil || roundIdInt >= len(pull.Submissions) { 358 + http.Error(w, "bad round id", http.StatusBadRequest) 359 + log.Println("failed to parse round id", err) 360 + return 361 + } 362 + 363 + if roundIdInt == 0 { 364 + http.Error(w, "bad round id", http.StatusBadRequest) 365 + log.Println("cannot interdiff initial submission") 366 + return 367 + } 368 + 369 + identsToResolve := []string{pull.OwnerDid} 370 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 371 + didHandleMap := make(map[string]string) 372 + for _, identity := range resolvedIds { 373 + if !identity.Handle.IsInvalidHandle() { 374 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 375 + } else { 376 + didHandleMap[identity.DID.String()] = identity.DID.String() 377 + } 378 + } 379 + 380 + currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch) 381 + if err != nil { 382 + log.Println("failed to interdiff; current patch malformed") 383 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 384 + return 385 + } 386 + 387 + previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch) 388 + if err != nil { 389 + log.Println("failed to interdiff; previous patch malformed") 390 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 391 + return 392 + } 393 + 394 + interdiff := patchutil.Interdiff(previousPatch, currentPatch) 395 + 396 + s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 397 + LoggedInUser: s.oauth.GetUser(r), 398 + RepoInfo: f.RepoInfo(user), 399 + Pull: pull, 400 + Round: roundIdInt, 401 + DidHandleMap: didHandleMap, 402 + Interdiff: interdiff, 403 + }) 404 + return 405 + } 406 + 407 + func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 408 + pull, ok := r.Context().Value("pull").(*db.Pull) 409 + if !ok { 410 + log.Println("failed to get pull") 411 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 412 + return 413 + } 414 + 415 + roundId := chi.URLParam(r, "round") 416 + roundIdInt, err := strconv.Atoi(roundId) 417 + if err != nil || roundIdInt >= len(pull.Submissions) { 418 + http.Error(w, "bad round id", http.StatusBadRequest) 419 + log.Println("failed to parse round id", err) 420 + return 421 + } 422 + 423 + identsToResolve := []string{pull.OwnerDid} 424 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 425 + didHandleMap := make(map[string]string) 426 + for _, identity := range resolvedIds { 427 + if !identity.Handle.IsInvalidHandle() { 428 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 429 + } else { 430 + didHandleMap[identity.DID.String()] = identity.DID.String() 431 + } 432 + } 433 + 434 + w.Header().Set("Content-Type", "text/plain") 435 + w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 436 + } 437 + 438 + func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { 439 + user := s.oauth.GetUser(r) 440 + params := r.URL.Query() 441 + 442 + state := db.PullOpen 443 + switch params.Get("state") { 444 + case "closed": 445 + state = db.PullClosed 446 + case "merged": 447 + state = db.PullMerged 448 + } 449 + 450 + f, err := s.repoResolver.Resolve(r) 451 + if err != nil { 452 + log.Println("failed to get repo and knot", err) 453 + return 454 + } 455 + 456 + pulls, err := db.GetPulls( 457 + s.db, 458 + db.FilterEq("repo_at", f.RepoAt), 459 + db.FilterEq("state", state), 460 + ) 461 + if err != nil { 462 + log.Println("failed to get pulls", err) 463 + s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 464 + return 465 + } 466 + 467 + for _, p := range pulls { 468 + var pullSourceRepo *db.Repo 469 + if p.PullSource != nil { 470 + if p.PullSource.RepoAt != nil { 471 + pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 472 + if err != nil { 473 + log.Printf("failed to get repo by at uri: %v", err) 474 + continue 475 + } else { 476 + p.PullSource.Repo = pullSourceRepo 477 + } 478 + } 479 + } 480 + } 481 + 482 + identsToResolve := make([]string, len(pulls)) 483 + for i, pull := range pulls { 484 + identsToResolve[i] = pull.OwnerDid 485 + } 486 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 487 + didHandleMap := make(map[string]string) 488 + for _, identity := range resolvedIds { 489 + if !identity.Handle.IsInvalidHandle() { 490 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 491 + } else { 492 + didHandleMap[identity.DID.String()] = identity.DID.String() 493 + } 494 + } 495 + 496 + s.pages.RepoPulls(w, pages.RepoPullsParams{ 497 + LoggedInUser: s.oauth.GetUser(r), 498 + RepoInfo: f.RepoInfo(user), 499 + Pulls: pulls, 500 + DidHandleMap: didHandleMap, 501 + FilteringBy: state, 502 + }) 503 + return 504 + } 505 + 506 + func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 507 + user := s.oauth.GetUser(r) 508 + f, err := s.repoResolver.Resolve(r) 509 + if err != nil { 510 + log.Println("failed to get repo and knot", err) 511 + return 512 + } 513 + 514 + pull, ok := r.Context().Value("pull").(*db.Pull) 515 + if !ok { 516 + log.Println("failed to get pull") 517 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 518 + return 519 + } 520 + 521 + roundNumberStr := chi.URLParam(r, "round") 522 + roundNumber, err := strconv.Atoi(roundNumberStr) 523 + if err != nil || roundNumber >= len(pull.Submissions) { 524 + http.Error(w, "bad round id", http.StatusBadRequest) 525 + log.Println("failed to parse round id", err) 526 + return 527 + } 528 + 529 + switch r.Method { 530 + case http.MethodGet: 531 + s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 532 + LoggedInUser: user, 533 + RepoInfo: f.RepoInfo(user), 534 + Pull: pull, 535 + RoundNumber: roundNumber, 536 + }) 537 + return 538 + case http.MethodPost: 539 + body := r.FormValue("body") 540 + if body == "" { 541 + s.pages.Notice(w, "pull", "Comment body is required") 542 + return 543 + } 544 + 545 + // Start a transaction 546 + tx, err := s.db.BeginTx(r.Context(), nil) 547 + if err != nil { 548 + log.Println("failed to start transaction", err) 549 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 550 + return 551 + } 552 + defer tx.Rollback() 553 + 554 + createdAt := time.Now().Format(time.RFC3339) 555 + ownerDid := user.Did 556 + 557 + pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 558 + if err != nil { 559 + log.Println("failed to get pull at", err) 560 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 561 + return 562 + } 563 + 564 + atUri := f.RepoAt.String() 565 + client, err := s.oauth.AuthorizedClient(r) 566 + if err != nil { 567 + log.Println("failed to get authorized client", err) 568 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 569 + return 570 + } 571 + atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 572 + Collection: tangled.RepoPullCommentNSID, 573 + Repo: user.Did, 574 + Rkey: appview.TID(), 575 + Record: &lexutil.LexiconTypeDecoder{ 576 + Val: &tangled.RepoPullComment{ 577 + Repo: &atUri, 578 + Pull: string(pullAt), 579 + Owner: &ownerDid, 580 + Body: body, 581 + CreatedAt: createdAt, 582 + }, 583 + }, 584 + }) 585 + if err != nil { 586 + log.Println("failed to create pull comment", err) 587 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 588 + return 589 + } 590 + 591 + // Create the pull comment in the database with the commentAt field 592 + commentId, err := db.NewPullComment(tx, &db.PullComment{ 593 + OwnerDid: user.Did, 594 + RepoAt: f.RepoAt.String(), 595 + PullId: pull.PullId, 596 + Body: body, 597 + CommentAt: atResp.Uri, 598 + SubmissionId: pull.Submissions[roundNumber].ID, 599 + }) 600 + if err != nil { 601 + log.Println("failed to create pull comment", err) 602 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 603 + return 604 + } 605 + 606 + // Commit the transaction 607 + if err = tx.Commit(); err != nil { 608 + log.Println("failed to commit transaction", err) 609 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 610 + return 611 + } 612 + 613 + if !s.config.Core.Dev { 614 + err = s.posthog.Enqueue(posthog.Capture{ 615 + DistinctId: user.Did, 616 + Event: "new_pull_comment", 617 + Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pull.PullId}, 618 + }) 619 + if err != nil { 620 + log.Println("failed to enqueue posthog event:", err) 621 + } 622 + } 623 + 624 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 625 + return 626 + } 627 + } 628 + 629 + func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { 630 + user := s.oauth.GetUser(r) 631 + f, err := s.repoResolver.Resolve(r) 632 + if err != nil { 633 + log.Println("failed to get repo and knot", err) 634 + return 635 + } 636 + 637 + switch r.Method { 638 + case http.MethodGet: 639 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 640 + if err != nil { 641 + log.Printf("failed to create unsigned client for %s", f.Knot) 642 + s.pages.Error503(w) 643 + return 644 + } 645 + 646 + result, err := us.Branches(f.OwnerDid(), f.RepoName) 647 + if err != nil { 648 + log.Println("failed to fetch branches", err) 649 + return 650 + } 651 + 652 + // can be one of "patch", "branch" or "fork" 653 + strategy := r.URL.Query().Get("strategy") 654 + // ignored if strategy is "patch" 655 + sourceBranch := r.URL.Query().Get("sourceBranch") 656 + targetBranch := r.URL.Query().Get("targetBranch") 657 + 658 + s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 659 + LoggedInUser: user, 660 + RepoInfo: f.RepoInfo(user), 661 + Branches: result.Branches, 662 + Strategy: strategy, 663 + SourceBranch: sourceBranch, 664 + TargetBranch: targetBranch, 665 + Title: r.URL.Query().Get("title"), 666 + Body: r.URL.Query().Get("body"), 667 + }) 668 + 669 + case http.MethodPost: 670 + title := r.FormValue("title") 671 + body := r.FormValue("body") 672 + targetBranch := r.FormValue("targetBranch") 673 + fromFork := r.FormValue("fork") 674 + sourceBranch := r.FormValue("sourceBranch") 675 + patch := r.FormValue("patch") 676 + 677 + if targetBranch == "" { 678 + s.pages.Notice(w, "pull", "Target branch is required.") 679 + return 680 + } 681 + 682 + // Determine PR type based on input parameters 683 + isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed() 684 + isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 685 + isForkBased := fromFork != "" && sourceBranch != "" 686 + isPatchBased := patch != "" && !isBranchBased && !isForkBased 687 + isStacked := r.FormValue("isStacked") == "on" 688 + 689 + if isPatchBased && !patchutil.IsFormatPatch(patch) { 690 + if title == "" { 691 + s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 692 + return 693 + } 694 + } 695 + 696 + // Validate we have at least one valid PR creation method 697 + if !isBranchBased && !isPatchBased && !isForkBased { 698 + s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 699 + return 700 + } 701 + 702 + // Can't mix branch-based and patch-based approaches 703 + if isBranchBased && patch != "" { 704 + s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 705 + return 706 + } 707 + 708 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 709 + if err != nil { 710 + log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 711 + s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 712 + return 713 + } 714 + 715 + caps, err := us.Capabilities() 716 + if err != nil { 717 + log.Println("error fetching knot caps", f.Knot, err) 718 + s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 719 + return 720 + } 721 + 722 + if !caps.PullRequests.FormatPatch { 723 + s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 724 + return 725 + } 726 + 727 + // Handle the PR creation based on the type 728 + if isBranchBased { 729 + if !caps.PullRequests.BranchSubmissions { 730 + s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 731 + return 732 + } 733 + s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked) 734 + } else if isForkBased { 735 + if !caps.PullRequests.ForkSubmissions { 736 + s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 737 + return 738 + } 739 + s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked) 740 + } else if isPatchBased { 741 + if !caps.PullRequests.PatchSubmissions { 742 + s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 743 + return 744 + } 745 + s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked) 746 + } 747 + return 748 + } 749 + } 750 + 751 + func (s *Pulls) handleBranchBasedPull( 752 + w http.ResponseWriter, 753 + r *http.Request, 754 + f *reporesolver.ResolvedRepo, 755 + user *oauth.User, 756 + title, 757 + body, 758 + targetBranch, 759 + sourceBranch string, 760 + isStacked bool, 761 + ) { 762 + pullSource := &db.PullSource{ 763 + Branch: sourceBranch, 764 + } 765 + recordPullSource := &tangled.RepoPull_Source{ 766 + Branch: sourceBranch, 767 + } 768 + 769 + // Generate a patch using /compare 770 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 771 + if err != nil { 772 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 773 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 774 + return 775 + } 776 + 777 + comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 778 + if err != nil { 779 + log.Println("failed to compare", err) 780 + s.pages.Notice(w, "pull", err.Error()) 781 + return 782 + } 783 + 784 + sourceRev := comparison.Rev2 785 + patch := comparison.Patch 786 + 787 + if !patchutil.IsPatchValid(patch) { 788 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 789 + return 790 + } 791 + 792 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked) 793 + } 794 + 795 + func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 796 + if !patchutil.IsPatchValid(patch) { 797 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 798 + return 799 + } 800 + 801 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked) 802 + } 803 + 804 + func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 805 + fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 806 + if errors.Is(err, sql.ErrNoRows) { 807 + s.pages.Notice(w, "pull", "No such fork.") 808 + return 809 + } else if err != nil { 810 + log.Println("failed to fetch fork:", err) 811 + s.pages.Notice(w, "pull", "Failed to fetch fork.") 812 + return 813 + } 814 + 815 + secret, err := db.GetRegistrationKey(s.db, fork.Knot) 816 + if err != nil { 817 + log.Println("failed to fetch registration key:", err) 818 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 819 + return 820 + } 821 + 822 + sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 823 + if err != nil { 824 + log.Println("failed to create signed client:", err) 825 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 826 + return 827 + } 828 + 829 + us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 830 + if err != nil { 831 + log.Println("failed to create unsigned client:", err) 832 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 833 + return 834 + } 835 + 836 + resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 837 + if err != nil { 838 + log.Println("failed to create hidden ref:", err, resp.StatusCode) 839 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 840 + return 841 + } 842 + 843 + switch resp.StatusCode { 844 + case 404: 845 + case 400: 846 + s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 847 + return 848 + } 849 + 850 + hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 851 + // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 852 + // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 853 + // hiddenRef: hidden/feature-1/main (on repo-fork) 854 + // targetBranch: main (on repo-1) 855 + // sourceBranch: feature-1 (on repo-fork) 856 + comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 857 + if err != nil { 858 + log.Println("failed to compare across branches", err) 859 + s.pages.Notice(w, "pull", err.Error()) 860 + return 861 + } 862 + 863 + sourceRev := comparison.Rev2 864 + patch := comparison.Patch 865 + 866 + if !patchutil.IsPatchValid(patch) { 867 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 868 + return 869 + } 870 + 871 + forkAtUri, err := syntax.ParseATURI(fork.AtUri) 872 + if err != nil { 873 + log.Println("failed to parse fork AT URI", err) 874 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 875 + return 876 + } 877 + 878 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 879 + Branch: sourceBranch, 880 + RepoAt: &forkAtUri, 881 + }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked) 882 + } 883 + 884 + func (s *Pulls) createPullRequest( 885 + w http.ResponseWriter, 886 + r *http.Request, 887 + f *reporesolver.ResolvedRepo, 888 + user *oauth.User, 889 + title, body, targetBranch string, 890 + patch string, 891 + sourceRev string, 892 + pullSource *db.PullSource, 893 + recordPullSource *tangled.RepoPull_Source, 894 + isStacked bool, 895 + ) { 896 + if isStacked { 897 + // creates a series of PRs, each linking to the previous, identified by jj's change-id 898 + s.createStackedPulLRequest( 899 + w, 900 + r, 901 + f, 902 + user, 903 + targetBranch, 904 + patch, 905 + sourceRev, 906 + pullSource, 907 + ) 908 + return 909 + } 910 + 911 + client, err := s.oauth.AuthorizedClient(r) 912 + if err != nil { 913 + log.Println("failed to get authorized client", err) 914 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 915 + return 916 + } 917 + 918 + tx, err := s.db.BeginTx(r.Context(), nil) 919 + if err != nil { 920 + log.Println("failed to start tx") 921 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 922 + return 923 + } 924 + defer tx.Rollback() 925 + 926 + // We've already checked earlier if it's diff-based and title is empty, 927 + // so if it's still empty now, it's intentionally skipped owing to format-patch. 928 + if title == "" { 929 + formatPatches, err := patchutil.ExtractPatches(patch) 930 + if err != nil { 931 + s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 932 + return 933 + } 934 + if len(formatPatches) == 0 { 935 + s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 936 + return 937 + } 938 + 939 + title = formatPatches[0].Title 940 + body = formatPatches[0].Body 941 + } 942 + 943 + rkey := appview.TID() 944 + initialSubmission := db.PullSubmission{ 945 + Patch: patch, 946 + SourceRev: sourceRev, 947 + } 948 + err = db.NewPull(tx, &db.Pull{ 949 + Title: title, 950 + Body: body, 951 + TargetBranch: targetBranch, 952 + OwnerDid: user.Did, 953 + RepoAt: f.RepoAt, 954 + Rkey: rkey, 955 + Submissions: []*db.PullSubmission{ 956 + &initialSubmission, 957 + }, 958 + PullSource: pullSource, 959 + }) 960 + if err != nil { 961 + log.Println("failed to create pull request", err) 962 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 963 + return 964 + } 965 + pullId, err := db.NextPullId(tx, f.RepoAt) 966 + if err != nil { 967 + log.Println("failed to get pull id", err) 968 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 969 + return 970 + } 971 + 972 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 973 + Collection: tangled.RepoPullNSID, 974 + Repo: user.Did, 975 + Rkey: rkey, 976 + Record: &lexutil.LexiconTypeDecoder{ 977 + Val: &tangled.RepoPull{ 978 + Title: title, 979 + PullId: int64(pullId), 980 + TargetRepo: string(f.RepoAt), 981 + TargetBranch: targetBranch, 982 + Patch: patch, 983 + Source: recordPullSource, 984 + }, 985 + }, 986 + }) 987 + if err != nil { 988 + log.Println("failed to create pull request", err) 989 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 990 + return 991 + } 992 + 993 + if err = tx.Commit(); err != nil { 994 + log.Println("failed to create pull request", err) 995 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 996 + return 997 + } 998 + 999 + if !s.config.Core.Dev { 1000 + err = s.posthog.Enqueue(posthog.Capture{ 1001 + DistinctId: user.Did, 1002 + Event: "new_pull", 1003 + Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId}, 1004 + }) 1005 + if err != nil { 1006 + log.Println("failed to enqueue posthog event:", err) 1007 + } 1008 + } 1009 + 1010 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 1011 + } 1012 + 1013 + func (s *Pulls) createStackedPulLRequest( 1014 + w http.ResponseWriter, 1015 + r *http.Request, 1016 + f *reporesolver.ResolvedRepo, 1017 + user *oauth.User, 1018 + targetBranch string, 1019 + patch string, 1020 + sourceRev string, 1021 + pullSource *db.PullSource, 1022 + ) { 1023 + // run some necessary checks for stacked-prs first 1024 + 1025 + // must be branch or fork based 1026 + if sourceRev == "" { 1027 + log.Println("stacked PR from patch-based pull") 1028 + s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.") 1029 + return 1030 + } 1031 + 1032 + formatPatches, err := patchutil.ExtractPatches(patch) 1033 + if err != nil { 1034 + log.Println("failed to extract patches", err) 1035 + s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1036 + return 1037 + } 1038 + 1039 + // must have atleast 1 patch to begin with 1040 + if len(formatPatches) == 0 { 1041 + log.Println("empty patches") 1042 + s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 1043 + return 1044 + } 1045 + 1046 + // build a stack out of this patch 1047 + stackId := uuid.New() 1048 + stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String()) 1049 + if err != nil { 1050 + log.Println("failed to create stack", err) 1051 + s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) 1052 + return 1053 + } 1054 + 1055 + client, err := s.oauth.AuthorizedClient(r) 1056 + if err != nil { 1057 + log.Println("failed to get authorized client", err) 1058 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1059 + return 1060 + } 1061 + 1062 + // apply all record creations at once 1063 + var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1064 + for _, p := range stack { 1065 + record := p.AsRecord() 1066 + write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1067 + RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1068 + Collection: tangled.RepoPullNSID, 1069 + Rkey: &p.Rkey, 1070 + Value: &lexutil.LexiconTypeDecoder{ 1071 + Val: &record, 1072 + }, 1073 + }, 1074 + } 1075 + writes = append(writes, &write) 1076 + } 1077 + _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1078 + Repo: user.Did, 1079 + Writes: writes, 1080 + }) 1081 + if err != nil { 1082 + log.Println("failed to create stacked pull request", err) 1083 + s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1084 + return 1085 + } 1086 + 1087 + // create all pulls at once 1088 + tx, err := s.db.BeginTx(r.Context(), nil) 1089 + if err != nil { 1090 + log.Println("failed to start tx") 1091 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1092 + return 1093 + } 1094 + defer tx.Rollback() 1095 + 1096 + for _, p := range stack { 1097 + err = db.NewPull(tx, p) 1098 + if err != nil { 1099 + log.Println("failed to create pull request", err) 1100 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1101 + return 1102 + } 1103 + } 1104 + 1105 + if err = tx.Commit(); err != nil { 1106 + log.Println("failed to create pull request", err) 1107 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1108 + return 1109 + } 1110 + 1111 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo())) 1112 + } 1113 + 1114 + func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1115 + _, err := s.repoResolver.Resolve(r) 1116 + if err != nil { 1117 + log.Println("failed to get repo and knot", err) 1118 + return 1119 + } 1120 + 1121 + patch := r.FormValue("patch") 1122 + if patch == "" { 1123 + s.pages.Notice(w, "patch-error", "Patch is required.") 1124 + return 1125 + } 1126 + 1127 + if patch == "" || !patchutil.IsPatchValid(patch) { 1128 + s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 1129 + return 1130 + } 1131 + 1132 + if patchutil.IsFormatPatch(patch) { 1133 + s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.") 1134 + } else { 1135 + s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") 1136 + } 1137 + } 1138 + 1139 + func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1140 + user := s.oauth.GetUser(r) 1141 + f, err := s.repoResolver.Resolve(r) 1142 + if err != nil { 1143 + log.Println("failed to get repo and knot", err) 1144 + return 1145 + } 1146 + 1147 + s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1148 + RepoInfo: f.RepoInfo(user), 1149 + }) 1150 + } 1151 + 1152 + func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1153 + user := s.oauth.GetUser(r) 1154 + f, err := s.repoResolver.Resolve(r) 1155 + if err != nil { 1156 + log.Println("failed to get repo and knot", err) 1157 + return 1158 + } 1159 + 1160 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1161 + if err != nil { 1162 + log.Printf("failed to create unsigned client for %s", f.Knot) 1163 + s.pages.Error503(w) 1164 + return 1165 + } 1166 + 1167 + result, err := us.Branches(f.OwnerDid(), f.RepoName) 1168 + if err != nil { 1169 + log.Println("failed to reach knotserver", err) 1170 + return 1171 + } 1172 + 1173 + branches := result.Branches 1174 + sort.Slice(branches, func(i int, j int) bool { 1175 + return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When) 1176 + }) 1177 + 1178 + withoutDefault := []types.Branch{} 1179 + for _, b := range branches { 1180 + if b.IsDefault { 1181 + continue 1182 + } 1183 + withoutDefault = append(withoutDefault, b) 1184 + } 1185 + 1186 + s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1187 + RepoInfo: f.RepoInfo(user), 1188 + Branches: withoutDefault, 1189 + }) 1190 + } 1191 + 1192 + func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1193 + user := s.oauth.GetUser(r) 1194 + f, err := s.repoResolver.Resolve(r) 1195 + if err != nil { 1196 + log.Println("failed to get repo and knot", err) 1197 + return 1198 + } 1199 + 1200 + forks, err := db.GetForksByDid(s.db, user.Did) 1201 + if err != nil { 1202 + log.Println("failed to get forks", err) 1203 + return 1204 + } 1205 + 1206 + s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1207 + RepoInfo: f.RepoInfo(user), 1208 + Forks: forks, 1209 + Selected: r.URL.Query().Get("fork"), 1210 + }) 1211 + } 1212 + 1213 + func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1214 + user := s.oauth.GetUser(r) 1215 + 1216 + f, err := s.repoResolver.Resolve(r) 1217 + if err != nil { 1218 + log.Println("failed to get repo and knot", err) 1219 + return 1220 + } 1221 + 1222 + forkVal := r.URL.Query().Get("fork") 1223 + 1224 + // fork repo 1225 + repo, err := db.GetRepo(s.db, user.Did, forkVal) 1226 + if err != nil { 1227 + log.Println("failed to get repo", user.Did, forkVal) 1228 + return 1229 + } 1230 + 1231 + sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1232 + if err != nil { 1233 + log.Printf("failed to create unsigned client for %s", repo.Knot) 1234 + s.pages.Error503(w) 1235 + return 1236 + } 1237 + 1238 + sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1239 + if err != nil { 1240 + log.Println("failed to reach knotserver for source branches", err) 1241 + return 1242 + } 1243 + 1244 + targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1245 + if err != nil { 1246 + log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1247 + s.pages.Error503(w) 1248 + return 1249 + } 1250 + 1251 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1252 + if err != nil { 1253 + log.Println("failed to reach knotserver for target branches", err) 1254 + return 1255 + } 1256 + 1257 + sourceBranches := sourceResult.Branches 1258 + sort.Slice(sourceBranches, func(i int, j int) bool { 1259 + return sourceBranches[i].Commit.Committer.When.After(sourceBranches[j].Commit.Committer.When) 1260 + }) 1261 + 1262 + s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1263 + RepoInfo: f.RepoInfo(user), 1264 + SourceBranches: sourceBranches, 1265 + TargetBranches: targetResult.Branches, 1266 + }) 1267 + } 1268 + 1269 + func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1270 + user := s.oauth.GetUser(r) 1271 + f, err := s.repoResolver.Resolve(r) 1272 + if err != nil { 1273 + log.Println("failed to get repo and knot", err) 1274 + return 1275 + } 1276 + 1277 + pull, ok := r.Context().Value("pull").(*db.Pull) 1278 + if !ok { 1279 + log.Println("failed to get pull") 1280 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1281 + return 1282 + } 1283 + 1284 + switch r.Method { 1285 + case http.MethodGet: 1286 + s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1287 + RepoInfo: f.RepoInfo(user), 1288 + Pull: pull, 1289 + }) 1290 + return 1291 + case http.MethodPost: 1292 + if pull.IsPatchBased() { 1293 + s.resubmitPatch(w, r) 1294 + return 1295 + } else if pull.IsBranchBased() { 1296 + s.resubmitBranch(w, r) 1297 + return 1298 + } else if pull.IsForkBased() { 1299 + s.resubmitFork(w, r) 1300 + return 1301 + } 1302 + } 1303 + } 1304 + 1305 + func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1306 + user := s.oauth.GetUser(r) 1307 + 1308 + pull, ok := r.Context().Value("pull").(*db.Pull) 1309 + if !ok { 1310 + log.Println("failed to get pull") 1311 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1312 + return 1313 + } 1314 + 1315 + f, err := s.repoResolver.Resolve(r) 1316 + if err != nil { 1317 + log.Println("failed to get repo and knot", err) 1318 + return 1319 + } 1320 + 1321 + if user.Did != pull.OwnerDid { 1322 + log.Println("unauthorized user") 1323 + w.WriteHeader(http.StatusUnauthorized) 1324 + return 1325 + } 1326 + 1327 + patch := r.FormValue("patch") 1328 + 1329 + s.resubmitPullHelper(w, r, f, user, pull, patch, "") 1330 + } 1331 + 1332 + func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1333 + user := s.oauth.GetUser(r) 1334 + 1335 + pull, ok := r.Context().Value("pull").(*db.Pull) 1336 + if !ok { 1337 + log.Println("failed to get pull") 1338 + s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1339 + return 1340 + } 1341 + 1342 + f, err := s.repoResolver.Resolve(r) 1343 + if err != nil { 1344 + log.Println("failed to get repo and knot", err) 1345 + return 1346 + } 1347 + 1348 + if user.Did != pull.OwnerDid { 1349 + log.Println("unauthorized user") 1350 + w.WriteHeader(http.StatusUnauthorized) 1351 + return 1352 + } 1353 + 1354 + if !f.RepoInfo(user).Roles.IsPushAllowed() { 1355 + log.Println("unauthorized user") 1356 + w.WriteHeader(http.StatusUnauthorized) 1357 + return 1358 + } 1359 + 1360 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1361 + if err != nil { 1362 + log.Printf("failed to create client for %s: %s", f.Knot, err) 1363 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1364 + return 1365 + } 1366 + 1367 + comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1368 + if err != nil { 1369 + log.Printf("compare request failed: %s", err) 1370 + s.pages.Notice(w, "resubmit-error", err.Error()) 1371 + return 1372 + } 1373 + 1374 + sourceRev := comparison.Rev2 1375 + patch := comparison.Patch 1376 + 1377 + s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1378 + } 1379 + 1380 + func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1381 + user := s.oauth.GetUser(r) 1382 + 1383 + pull, ok := r.Context().Value("pull").(*db.Pull) 1384 + if !ok { 1385 + log.Println("failed to get pull") 1386 + s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1387 + return 1388 + } 1389 + 1390 + f, err := s.repoResolver.Resolve(r) 1391 + if err != nil { 1392 + log.Println("failed to get repo and knot", err) 1393 + return 1394 + } 1395 + 1396 + if user.Did != pull.OwnerDid { 1397 + log.Println("unauthorized user") 1398 + w.WriteHeader(http.StatusUnauthorized) 1399 + return 1400 + } 1401 + 1402 + forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1403 + if err != nil { 1404 + log.Println("failed to get source repo", err) 1405 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1406 + return 1407 + } 1408 + 1409 + // extract patch by performing compare 1410 + ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1411 + if err != nil { 1412 + log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1413 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1414 + return 1415 + } 1416 + 1417 + secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1418 + if err != nil { 1419 + log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1420 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1421 + return 1422 + } 1423 + 1424 + // update the hidden tracking branch to latest 1425 + signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1426 + if err != nil { 1427 + log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1428 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1429 + return 1430 + } 1431 + 1432 + resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1433 + if err != nil || resp.StatusCode != http.StatusNoContent { 1434 + log.Printf("failed to update tracking branch: %s", err) 1435 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1436 + return 1437 + } 1438 + 1439 + hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1440 + comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1441 + if err != nil { 1442 + log.Printf("failed to compare branches: %s", err) 1443 + s.pages.Notice(w, "resubmit-error", err.Error()) 1444 + return 1445 + } 1446 + 1447 + sourceRev := comparison.Rev2 1448 + patch := comparison.Patch 1449 + 1450 + s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev) 1451 + } 1452 + 1453 + // validate a resubmission against a pull request 1454 + func validateResubmittedPatch(pull *db.Pull, patch string) error { 1455 + if patch == "" { 1456 + return fmt.Errorf("Patch is empty.") 1457 + } 1458 + 1459 + if patch == pull.LatestPatch() { 1460 + return fmt.Errorf("Patch is identical to previous submission.") 1461 + } 1462 + 1463 + if !patchutil.IsPatchValid(patch) { 1464 + return fmt.Errorf("Invalid patch format. Please provide a valid diff.") 1465 + } 1466 + 1467 + return nil 1468 + } 1469 + 1470 + func (s *Pulls) resubmitPullHelper( 1471 + w http.ResponseWriter, 1472 + r *http.Request, 1473 + f *reporesolver.ResolvedRepo, 1474 + user *oauth.User, 1475 + pull *db.Pull, 1476 + patch string, 1477 + sourceRev string, 1478 + ) { 1479 + if pull.IsStacked() { 1480 + log.Println("resubmitting stacked PR") 1481 + s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId) 1482 + return 1483 + } 1484 + 1485 + if err := validateResubmittedPatch(pull, patch); err != nil { 1486 + s.pages.Notice(w, "resubmit-error", err.Error()) 1487 + return 1488 + } 1489 + 1490 + // validate sourceRev if branch/fork based 1491 + if pull.IsBranchBased() || pull.IsForkBased() { 1492 + if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1493 + s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1494 + return 1495 + } 1496 + } 1497 + 1498 + tx, err := s.db.BeginTx(r.Context(), nil) 1499 + if err != nil { 1500 + log.Println("failed to start tx") 1501 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1502 + return 1503 + } 1504 + defer tx.Rollback() 1505 + 1506 + err = db.ResubmitPull(tx, pull, patch, sourceRev) 1507 + if err != nil { 1508 + log.Println("failed to create pull request", err) 1509 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1510 + return 1511 + } 1512 + client, err := s.oauth.AuthorizedClient(r) 1513 + if err != nil { 1514 + log.Println("failed to authorize client") 1515 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1516 + return 1517 + } 1518 + 1519 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1520 + if err != nil { 1521 + // failed to get record 1522 + s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1523 + return 1524 + } 1525 + 1526 + var recordPullSource *tangled.RepoPull_Source 1527 + if pull.IsBranchBased() { 1528 + recordPullSource = &tangled.RepoPull_Source{ 1529 + Branch: pull.PullSource.Branch, 1530 + } 1531 + } 1532 + if pull.IsForkBased() { 1533 + repoAt := pull.PullSource.RepoAt.String() 1534 + recordPullSource = &tangled.RepoPull_Source{ 1535 + Branch: pull.PullSource.Branch, 1536 + Repo: &repoAt, 1537 + } 1538 + } 1539 + 1540 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1541 + Collection: tangled.RepoPullNSID, 1542 + Repo: user.Did, 1543 + Rkey: pull.Rkey, 1544 + SwapRecord: ex.Cid, 1545 + Record: &lexutil.LexiconTypeDecoder{ 1546 + Val: &tangled.RepoPull{ 1547 + Title: pull.Title, 1548 + PullId: int64(pull.PullId), 1549 + TargetRepo: string(f.RepoAt), 1550 + TargetBranch: pull.TargetBranch, 1551 + Patch: patch, // new patch 1552 + Source: recordPullSource, 1553 + }, 1554 + }, 1555 + }) 1556 + if err != nil { 1557 + log.Println("failed to update record", err) 1558 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1559 + return 1560 + } 1561 + 1562 + if err = tx.Commit(); err != nil { 1563 + log.Println("failed to commit transaction", err) 1564 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1565 + return 1566 + } 1567 + 1568 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1569 + return 1570 + } 1571 + 1572 + func (s *Pulls) resubmitStackedPullHelper( 1573 + w http.ResponseWriter, 1574 + r *http.Request, 1575 + f *reporesolver.ResolvedRepo, 1576 + user *oauth.User, 1577 + pull *db.Pull, 1578 + patch string, 1579 + stackId string, 1580 + ) { 1581 + targetBranch := pull.TargetBranch 1582 + 1583 + origStack, _ := r.Context().Value("stack").(db.Stack) 1584 + newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1585 + if err != nil { 1586 + log.Println("failed to create resubmitted stack", err) 1587 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1588 + return 1589 + } 1590 + 1591 + // find the diff between the stacks, first, map them by changeId 1592 + origById := make(map[string]*db.Pull) 1593 + newById := make(map[string]*db.Pull) 1594 + for _, p := range origStack { 1595 + origById[p.ChangeId] = p 1596 + } 1597 + for _, p := range newStack { 1598 + newById[p.ChangeId] = p 1599 + } 1600 + 1601 + // commits that got deleted: corresponding pull is closed 1602 + // commits that got added: new pull is created 1603 + // commits that got updated: corresponding pull is resubmitted & new round begins 1604 + // 1605 + // for commits that were unchanged: no changes, parent-change-id is updated as necessary 1606 + additions := make(map[string]*db.Pull) 1607 + deletions := make(map[string]*db.Pull) 1608 + unchanged := make(map[string]struct{}) 1609 + updated := make(map[string]struct{}) 1610 + 1611 + // pulls in orignal stack but not in new one 1612 + for _, op := range origStack { 1613 + if _, ok := newById[op.ChangeId]; !ok { 1614 + deletions[op.ChangeId] = op 1615 + } 1616 + } 1617 + 1618 + // pulls in new stack but not in original one 1619 + for _, np := range newStack { 1620 + if _, ok := origById[np.ChangeId]; !ok { 1621 + additions[np.ChangeId] = np 1622 + } 1623 + } 1624 + 1625 + // NOTE: this loop can be written in any of above blocks, 1626 + // but is written separately in the interest of simpler code 1627 + for _, np := range newStack { 1628 + if op, ok := origById[np.ChangeId]; ok { 1629 + // pull exists in both stacks 1630 + // TODO: can we avoid reparse? 1631 + origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch())) 1632 + newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch())) 1633 + 1634 + origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr) 1635 + newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr) 1636 + 1637 + patchutil.SortPatch(newFiles) 1638 + patchutil.SortPatch(origFiles) 1639 + 1640 + // text content of patch may be identical, but a jj rebase might have forwarded it 1641 + // 1642 + // we still need to update the hash in submission.Patch and submission.SourceRev 1643 + if patchutil.Equal(newFiles, origFiles) && 1644 + origHeader.Title == newHeader.Title && 1645 + origHeader.Body == newHeader.Body { 1646 + unchanged[op.ChangeId] = struct{}{} 1647 + } else { 1648 + updated[op.ChangeId] = struct{}{} 1649 + } 1650 + } 1651 + } 1652 + 1653 + tx, err := s.db.Begin() 1654 + if err != nil { 1655 + log.Println("failed to start transaction", err) 1656 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1657 + return 1658 + } 1659 + defer tx.Rollback() 1660 + 1661 + // pds updates to make 1662 + var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1663 + 1664 + // deleted pulls are marked as deleted in the DB 1665 + for _, p := range deletions { 1666 + err := db.DeletePull(tx, p.RepoAt, p.PullId) 1667 + if err != nil { 1668 + log.Println("failed to delete pull", err, p.PullId) 1669 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1670 + return 1671 + } 1672 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1673 + RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ 1674 + Collection: tangled.RepoPullNSID, 1675 + Rkey: p.Rkey, 1676 + }, 1677 + }) 1678 + } 1679 + 1680 + // new pulls are created 1681 + for _, p := range additions { 1682 + err := db.NewPull(tx, p) 1683 + if err != nil { 1684 + log.Println("failed to create pull", err, p.PullId) 1685 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1686 + return 1687 + } 1688 + 1689 + record := p.AsRecord() 1690 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1691 + RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1692 + Collection: tangled.RepoPullNSID, 1693 + Rkey: &p.Rkey, 1694 + Value: &lexutil.LexiconTypeDecoder{ 1695 + Val: &record, 1696 + }, 1697 + }, 1698 + }) 1699 + } 1700 + 1701 + // updated pulls are, well, updated; to start a new round 1702 + for id := range updated { 1703 + op, _ := origById[id] 1704 + np, _ := newById[id] 1705 + 1706 + submission := np.Submissions[np.LastRoundNumber()] 1707 + 1708 + // resubmit the old pull 1709 + err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev) 1710 + 1711 + if err != nil { 1712 + log.Println("failed to update pull", err, op.PullId) 1713 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1714 + return 1715 + } 1716 + 1717 + record := op.AsRecord() 1718 + record.Patch = submission.Patch 1719 + 1720 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1721 + RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1722 + Collection: tangled.RepoPullNSID, 1723 + Rkey: op.Rkey, 1724 + Value: &lexutil.LexiconTypeDecoder{ 1725 + Val: &record, 1726 + }, 1727 + }, 1728 + }) 1729 + } 1730 + 1731 + // unchanged pulls are edited without starting a new round 1732 + // 1733 + // update source-revs & patches without advancing rounds 1734 + for changeId := range unchanged { 1735 + op, _ := origById[changeId] 1736 + np, _ := newById[changeId] 1737 + 1738 + origSubmission := op.Submissions[op.LastRoundNumber()] 1739 + newSubmission := np.Submissions[np.LastRoundNumber()] 1740 + 1741 + log.Println("moving unchanged change id : ", changeId) 1742 + 1743 + err := db.UpdatePull( 1744 + tx, 1745 + newSubmission.Patch, 1746 + newSubmission.SourceRev, 1747 + db.FilterEq("id", origSubmission.ID), 1748 + ) 1749 + 1750 + if err != nil { 1751 + log.Println("failed to update pull", err, op.PullId) 1752 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1753 + return 1754 + } 1755 + 1756 + record := op.AsRecord() 1757 + record.Patch = newSubmission.Patch 1758 + 1759 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1760 + RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 1761 + Collection: tangled.RepoPullNSID, 1762 + Rkey: op.Rkey, 1763 + Value: &lexutil.LexiconTypeDecoder{ 1764 + Val: &record, 1765 + }, 1766 + }, 1767 + }) 1768 + } 1769 + 1770 + // update parent-change-id relations for the entire stack 1771 + for _, p := range newStack { 1772 + err := db.SetPullParentChangeId( 1773 + tx, 1774 + p.ParentChangeId, 1775 + // these should be enough filters to be unique per-stack 1776 + db.FilterEq("repo_at", p.RepoAt.String()), 1777 + db.FilterEq("owner_did", p.OwnerDid), 1778 + db.FilterEq("change_id", p.ChangeId), 1779 + ) 1780 + 1781 + if err != nil { 1782 + log.Println("failed to update pull", err, p.PullId) 1783 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1784 + return 1785 + } 1786 + } 1787 + 1788 + err = tx.Commit() 1789 + if err != nil { 1790 + log.Println("failed to resubmit pull", err) 1791 + s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 1792 + return 1793 + } 1794 + 1795 + client, err := s.oauth.AuthorizedClient(r) 1796 + if err != nil { 1797 + log.Println("failed to authorize client") 1798 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1799 + return 1800 + } 1801 + 1802 + _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1803 + Repo: user.Did, 1804 + Writes: writes, 1805 + }) 1806 + if err != nil { 1807 + log.Println("failed to create stacked pull request", err) 1808 + s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1809 + return 1810 + } 1811 + 1812 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1813 + return 1814 + } 1815 + 1816 + func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 1817 + f, err := s.repoResolver.Resolve(r) 1818 + if err != nil { 1819 + log.Println("failed to resolve repo:", err) 1820 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1821 + return 1822 + } 1823 + 1824 + pull, ok := r.Context().Value("pull").(*db.Pull) 1825 + if !ok { 1826 + log.Println("failed to get pull") 1827 + s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1828 + return 1829 + } 1830 + 1831 + var pullsToMerge db.Stack 1832 + pullsToMerge = append(pullsToMerge, pull) 1833 + if pull.IsStacked() { 1834 + stack, ok := r.Context().Value("stack").(db.Stack) 1835 + if !ok { 1836 + log.Println("failed to get stack") 1837 + s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.") 1838 + return 1839 + } 1840 + 1841 + // combine patches of substack 1842 + subStack := stack.StrictlyBelow(pull) 1843 + // collect the portion of the stack that is mergeable 1844 + mergeable := subStack.Mergeable() 1845 + // add to total patch 1846 + pullsToMerge = append(pullsToMerge, mergeable...) 1847 + } 1848 + 1849 + patch := pullsToMerge.CombinedPatch() 1850 + 1851 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 1852 + if err != nil { 1853 + log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1854 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1855 + return 1856 + } 1857 + 1858 + ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid) 1859 + if err != nil { 1860 + log.Printf("resolving identity: %s", err) 1861 + w.WriteHeader(http.StatusNotFound) 1862 + return 1863 + } 1864 + 1865 + email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1866 + if err != nil { 1867 + log.Printf("failed to get primary email: %s", err) 1868 + } 1869 + 1870 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1871 + if err != nil { 1872 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1873 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1874 + return 1875 + } 1876 + 1877 + // Merge the pull request 1878 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1879 + if err != nil { 1880 + log.Printf("failed to merge pull request: %s", err) 1881 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1882 + return 1883 + } 1884 + 1885 + if resp.StatusCode != http.StatusOK { 1886 + log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1887 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1888 + return 1889 + } 1890 + 1891 + tx, err := s.db.Begin() 1892 + if err != nil { 1893 + log.Println("failed to start transcation", err) 1894 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1895 + return 1896 + } 1897 + defer tx.Rollback() 1898 + 1899 + for _, p := range pullsToMerge { 1900 + err := db.MergePull(tx, f.RepoAt, p.PullId) 1901 + if err != nil { 1902 + log.Printf("failed to update pull request status in database: %s", err) 1903 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1904 + return 1905 + } 1906 + } 1907 + 1908 + err = tx.Commit() 1909 + if err != nil { 1910 + // TODO: this is unsound, we should also revert the merge from the knotserver here 1911 + log.Printf("failed to update pull request status in database: %s", err) 1912 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1913 + return 1914 + } 1915 + 1916 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1917 + } 1918 + 1919 + func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 1920 + user := s.oauth.GetUser(r) 1921 + 1922 + f, err := s.repoResolver.Resolve(r) 1923 + if err != nil { 1924 + log.Println("malformed middleware") 1925 + return 1926 + } 1927 + 1928 + pull, ok := r.Context().Value("pull").(*db.Pull) 1929 + if !ok { 1930 + log.Println("failed to get pull") 1931 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1932 + return 1933 + } 1934 + 1935 + // auth filter: only owner or collaborators can close 1936 + roles := f.RolesInRepo(user) 1937 + isCollaborator := roles.IsCollaborator() 1938 + isPullAuthor := user.Did == pull.OwnerDid 1939 + isCloseAllowed := isCollaborator || isPullAuthor 1940 + if !isCloseAllowed { 1941 + log.Println("failed to close pull") 1942 + s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1943 + return 1944 + } 1945 + 1946 + // Start a transaction 1947 + tx, err := s.db.BeginTx(r.Context(), nil) 1948 + if err != nil { 1949 + log.Println("failed to start transaction", err) 1950 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1951 + return 1952 + } 1953 + defer tx.Rollback() 1954 + 1955 + var pullsToClose []*db.Pull 1956 + pullsToClose = append(pullsToClose, pull) 1957 + 1958 + // if this PR is stacked, then we want to close all PRs below this one on the stack 1959 + if pull.IsStacked() { 1960 + stack := r.Context().Value("stack").(db.Stack) 1961 + subStack := stack.StrictlyBelow(pull) 1962 + pullsToClose = append(pullsToClose, subStack...) 1963 + } 1964 + 1965 + for _, p := range pullsToClose { 1966 + // Close the pull in the database 1967 + err = db.ClosePull(tx, f.RepoAt, p.PullId) 1968 + if err != nil { 1969 + log.Println("failed to close pull", err) 1970 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1971 + return 1972 + } 1973 + } 1974 + 1975 + // Commit the transaction 1976 + if err = tx.Commit(); err != nil { 1977 + log.Println("failed to commit transaction", err) 1978 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1979 + return 1980 + } 1981 + 1982 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1983 + return 1984 + } 1985 + 1986 + func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 1987 + user := s.oauth.GetUser(r) 1988 + 1989 + f, err := s.repoResolver.Resolve(r) 1990 + if err != nil { 1991 + log.Println("failed to resolve repo", err) 1992 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1993 + return 1994 + } 1995 + 1996 + pull, ok := r.Context().Value("pull").(*db.Pull) 1997 + if !ok { 1998 + log.Println("failed to get pull") 1999 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2000 + return 2001 + } 2002 + 2003 + // auth filter: only owner or collaborators can close 2004 + roles := f.RolesInRepo(user) 2005 + isCollaborator := roles.IsCollaborator() 2006 + isPullAuthor := user.Did == pull.OwnerDid 2007 + isCloseAllowed := isCollaborator || isPullAuthor 2008 + if !isCloseAllowed { 2009 + log.Println("failed to close pull") 2010 + s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2011 + return 2012 + } 2013 + 2014 + // Start a transaction 2015 + tx, err := s.db.BeginTx(r.Context(), nil) 2016 + if err != nil { 2017 + log.Println("failed to start transaction", err) 2018 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2019 + return 2020 + } 2021 + defer tx.Rollback() 2022 + 2023 + var pullsToReopen []*db.Pull 2024 + pullsToReopen = append(pullsToReopen, pull) 2025 + 2026 + // if this PR is stacked, then we want to reopen all PRs above this one on the stack 2027 + if pull.IsStacked() { 2028 + stack := r.Context().Value("stack").(db.Stack) 2029 + subStack := stack.StrictlyAbove(pull) 2030 + pullsToReopen = append(pullsToReopen, subStack...) 2031 + } 2032 + 2033 + for _, p := range pullsToReopen { 2034 + // Close the pull in the database 2035 + err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2036 + if err != nil { 2037 + log.Println("failed to close pull", err) 2038 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 2039 + return 2040 + } 2041 + } 2042 + 2043 + // Commit the transaction 2044 + if err = tx.Commit(); err != nil { 2045 + log.Println("failed to commit transaction", err) 2046 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2047 + return 2048 + } 2049 + 2050 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2051 + return 2052 + } 2053 + 2054 + func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2055 + formatPatches, err := patchutil.ExtractPatches(patch) 2056 + if err != nil { 2057 + return nil, fmt.Errorf("Failed to extract patches: %v", err) 2058 + } 2059 + 2060 + // must have atleast 1 patch to begin with 2061 + if len(formatPatches) == 0 { 2062 + return nil, fmt.Errorf("No patches found in the generated format-patch.") 2063 + } 2064 + 2065 + // the stack is identified by a UUID 2066 + var stack db.Stack 2067 + parentChangeId := "" 2068 + for _, fp := range formatPatches { 2069 + // all patches must have a jj change-id 2070 + changeId, err := fp.ChangeId() 2071 + if err != nil { 2072 + return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.") 2073 + } 2074 + 2075 + title := fp.Title 2076 + body := fp.Body 2077 + rkey := appview.TID() 2078 + 2079 + initialSubmission := db.PullSubmission{ 2080 + Patch: fp.Raw, 2081 + SourceRev: fp.SHA, 2082 + } 2083 + pull := db.Pull{ 2084 + Title: title, 2085 + Body: body, 2086 + TargetBranch: targetBranch, 2087 + OwnerDid: user.Did, 2088 + RepoAt: f.RepoAt, 2089 + Rkey: rkey, 2090 + Submissions: []*db.PullSubmission{ 2091 + &initialSubmission, 2092 + }, 2093 + PullSource: pullSource, 2094 + Created: time.Now(), 2095 + 2096 + StackId: stackId, 2097 + ChangeId: changeId, 2098 + ParentChangeId: parentChangeId, 2099 + } 2100 + 2101 + stack = append(stack, &pull) 2102 + 2103 + parentChangeId = changeId 2104 + } 2105 + 2106 + return stack, nil 2107 + }
+300
appview/reporesolver/resolver.go
··· 1 + package reporesolver 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "errors" 7 + "fmt" 8 + "log" 9 + "net/http" 10 + "net/url" 11 + "path" 12 + "strings" 13 + 14 + "github.com/bluesky-social/indigo/atproto/identity" 15 + "github.com/bluesky-social/indigo/atproto/syntax" 16 + securejoin "github.com/cyphar/filepath-securejoin" 17 + "github.com/go-chi/chi/v5" 18 + "tangled.sh/tangled.sh/core/appview" 19 + "tangled.sh/tangled.sh/core/appview/db" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 21 + "tangled.sh/tangled.sh/core/appview/pages" 22 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 + "tangled.sh/tangled.sh/core/knotclient" 24 + "tangled.sh/tangled.sh/core/rbac" 25 + ) 26 + 27 + type ResolvedRepo struct { 28 + Knot string 29 + OwnerId identity.Identity 30 + RepoName string 31 + RepoAt syntax.ATURI 32 + Description string 33 + CreatedAt string 34 + Ref string 35 + CurrentDir string 36 + 37 + rr *RepoResolver 38 + } 39 + 40 + type RepoResolver struct { 41 + config *appview.Config 42 + enforcer *rbac.Enforcer 43 + resolver *appview.Resolver 44 + execer db.Execer 45 + } 46 + 47 + func New(config *appview.Config, enforcer *rbac.Enforcer, resolver *appview.Resolver, execer db.Execer) *RepoResolver { 48 + return &RepoResolver{config: config, enforcer: enforcer, resolver: resolver, execer: execer} 49 + } 50 + 51 + func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { 52 + repoName := chi.URLParam(r, "repo") 53 + knot, ok := r.Context().Value("knot").(string) 54 + if !ok { 55 + log.Println("malformed middleware") 56 + return nil, fmt.Errorf("malformed middleware") 57 + } 58 + id, ok := r.Context().Value("resolvedId").(identity.Identity) 59 + if !ok { 60 + log.Println("malformed middleware") 61 + return nil, fmt.Errorf("malformed middleware") 62 + } 63 + 64 + repoAt, ok := r.Context().Value("repoAt").(string) 65 + if !ok { 66 + log.Println("malformed middleware") 67 + return nil, fmt.Errorf("malformed middleware") 68 + } 69 + 70 + parsedRepoAt, err := syntax.ParseATURI(repoAt) 71 + if err != nil { 72 + log.Println("malformed repo at-uri") 73 + return nil, fmt.Errorf("malformed middleware") 74 + } 75 + 76 + ref := chi.URLParam(r, "ref") 77 + 78 + if ref == "" { 79 + us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev) 80 + if err != nil { 81 + return nil, err 82 + } 83 + 84 + defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 85 + if err != nil { 86 + return nil, err 87 + } 88 + 89 + ref = defaultBranch.Branch 90 + } 91 + 92 + currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 93 + 94 + // pass through values from the middleware 95 + description, ok := r.Context().Value("repoDescription").(string) 96 + addedAt, ok := r.Context().Value("repoAddedAt").(string) 97 + 98 + return &ResolvedRepo{ 99 + Knot: knot, 100 + OwnerId: id, 101 + RepoName: repoName, 102 + RepoAt: parsedRepoAt, 103 + Description: description, 104 + CreatedAt: addedAt, 105 + Ref: ref, 106 + CurrentDir: currentDir, 107 + 108 + rr: rr, 109 + }, nil 110 + } 111 + 112 + func (f *ResolvedRepo) OwnerDid() string { 113 + return f.OwnerId.DID.String() 114 + } 115 + 116 + func (f *ResolvedRepo) OwnerHandle() string { 117 + return f.OwnerId.Handle.String() 118 + } 119 + 120 + func (f *ResolvedRepo) OwnerSlashRepo() string { 121 + handle := f.OwnerId.Handle 122 + 123 + var p string 124 + if handle != "" && !handle.IsInvalidHandle() { 125 + p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 126 + } else { 127 + p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 128 + } 129 + 130 + return p 131 + } 132 + 133 + func (f *ResolvedRepo) DidSlashRepo() string { 134 + p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 135 + return p 136 + } 137 + 138 + func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) { 139 + repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 140 + if err != nil { 141 + return nil, err 142 + } 143 + 144 + var collaborators []pages.Collaborator 145 + for _, item := range repoCollaborators { 146 + // currently only two roles: owner and member 147 + var role string 148 + if item[3] == "repo:owner" { 149 + role = "owner" 150 + } else if item[3] == "repo:collaborator" { 151 + role = "collaborator" 152 + } else { 153 + continue 154 + } 155 + 156 + did := item[0] 157 + 158 + c := pages.Collaborator{ 159 + Did: did, 160 + Handle: "", 161 + Role: role, 162 + } 163 + collaborators = append(collaborators, c) 164 + } 165 + 166 + // populate all collborators with handles 167 + identsToResolve := make([]string, len(collaborators)) 168 + for i, collab := range collaborators { 169 + identsToResolve[i] = collab.Did 170 + } 171 + 172 + resolvedIdents := f.rr.resolver.ResolveIdents(ctx, identsToResolve) 173 + for i, resolved := range resolvedIdents { 174 + if resolved != nil { 175 + collaborators[i].Handle = resolved.Handle.String() 176 + } 177 + } 178 + 179 + return collaborators, nil 180 + } 181 + 182 + // this function is a bit weird since it now returns RepoInfo from an entirely different 183 + // package. we should refactor this or get rid of RepoInfo entirely. 184 + func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 185 + isStarred := false 186 + if user != nil { 187 + isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 188 + } 189 + 190 + starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 191 + if err != nil { 192 + log.Println("failed to get star count for ", f.RepoAt) 193 + } 194 + issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 195 + if err != nil { 196 + log.Println("failed to get issue count for ", f.RepoAt) 197 + } 198 + pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 199 + if err != nil { 200 + log.Println("failed to get issue count for ", f.RepoAt) 201 + } 202 + source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 203 + if errors.Is(err, sql.ErrNoRows) { 204 + source = "" 205 + } else if err != nil { 206 + log.Println("failed to get repo source for ", f.RepoAt, err) 207 + } 208 + 209 + var sourceRepo *db.Repo 210 + if source != "" { 211 + sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source) 212 + if err != nil { 213 + log.Println("failed to get repo by at uri", err) 214 + } 215 + } 216 + 217 + var sourceHandle *identity.Identity 218 + if sourceRepo != nil { 219 + sourceHandle, err = f.rr.resolver.ResolveIdent(context.Background(), sourceRepo.Did) 220 + if err != nil { 221 + log.Println("failed to resolve source repo", err) 222 + } 223 + } 224 + 225 + knot := f.Knot 226 + var disableFork bool 227 + us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev) 228 + if err != nil { 229 + log.Printf("failed to create unsigned client for %s: %v", knot, err) 230 + } else { 231 + result, err := us.Branches(f.OwnerDid(), f.RepoName) 232 + if err != nil { 233 + log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 234 + } 235 + 236 + if len(result.Branches) == 0 { 237 + disableFork = true 238 + } 239 + } 240 + 241 + repoInfo := repoinfo.RepoInfo{ 242 + OwnerDid: f.OwnerDid(), 243 + OwnerHandle: f.OwnerHandle(), 244 + Name: f.RepoName, 245 + RepoAt: f.RepoAt, 246 + Description: f.Description, 247 + Ref: f.Ref, 248 + IsStarred: isStarred, 249 + Knot: knot, 250 + Roles: f.RolesInRepo(user), 251 + Stats: db.RepoStats{ 252 + StarCount: starCount, 253 + IssueCount: issueCount, 254 + PullCount: pullCount, 255 + }, 256 + DisableFork: disableFork, 257 + CurrentDir: f.CurrentDir, 258 + } 259 + 260 + if sourceRepo != nil { 261 + repoInfo.Source = sourceRepo 262 + repoInfo.SourceHandle = sourceHandle.Handle.String() 263 + } 264 + 265 + return repoInfo 266 + } 267 + 268 + func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo { 269 + if u != nil { 270 + r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 271 + return repoinfo.RolesInRepo{r} 272 + } else { 273 + return repoinfo.RolesInRepo{} 274 + } 275 + } 276 + 277 + // extractPathAfterRef gets the actual repository path 278 + // after the ref. for example: 279 + // 280 + // /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 281 + func extractPathAfterRef(fullPath, ref string) string { 282 + fullPath = strings.TrimPrefix(fullPath, "/") 283 + 284 + ref = url.PathEscape(ref) 285 + 286 + prefixes := []string{ 287 + fmt.Sprintf("blob/%s/", ref), 288 + fmt.Sprintf("tree/%s/", ref), 289 + fmt.Sprintf("raw/%s/", ref), 290 + } 291 + 292 + for _, prefix := range prefixes { 293 + idx := strings.Index(fullPath, prefix) 294 + if idx != -1 { 295 + return fullPath[idx+len(prefix):] 296 + } 297 + } 298 + 299 + return "" 300 + }
+6 -5
appview/state/artifact.go
··· 17 17 "tangled.sh/tangled.sh/core/appview" 18 18 "tangled.sh/tangled.sh/core/appview/db" 19 19 "tangled.sh/tangled.sh/core/appview/pages" 20 + "tangled.sh/tangled.sh/core/appview/reporesolver" 20 21 "tangled.sh/tangled.sh/core/knotclient" 21 22 "tangled.sh/tangled.sh/core/types" 22 23 ) ··· 25 26 func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { 26 27 user := s.oauth.GetUser(r) 27 28 tagParam := chi.URLParam(r, "tag") 28 - f, err := s.fullyResolvedRepo(r) 29 + f, err := s.repoResolver.Resolve(r) 29 30 if err != nil { 30 31 log.Println("failed to get repo and knot", err) 31 32 s.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution") ··· 124 125 125 126 s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 126 127 LoggedInUser: user, 127 - RepoInfo: f.RepoInfo(s, user), 128 + RepoInfo: f.RepoInfo(user), 128 129 Artifact: artifact, 129 130 }) 130 131 } ··· 133 134 func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { 134 135 tagParam := chi.URLParam(r, "tag") 135 136 filename := chi.URLParam(r, "file") 136 - f, err := s.fullyResolvedRepo(r) 137 + f, err := s.repoResolver.Resolve(r) 137 138 if err != nil { 138 139 log.Println("failed to get repo and knot", err) 139 140 return ··· 184 185 user := s.oauth.GetUser(r) 185 186 tagParam := chi.URLParam(r, "tag") 186 187 filename := chi.URLParam(r, "file") 187 - f, err := s.fullyResolvedRepo(r) 188 + f, err := s.repoResolver.Resolve(r) 188 189 if err != nil { 189 190 log.Println("failed to get repo and knot", err) 190 191 return ··· 258 259 w.Write([]byte{}) 259 260 } 260 261 261 - func (s *State) resolveTag(f *FullyResolvedRepo, tagParam string) (*types.TagReference, error) { 262 + func (s *State) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) { 262 263 tagParam, err := url.QueryUnescape(tagParam) 263 264 if err != nil { 264 265 return nil, err
+3 -3
appview/state/middleware.go
··· 62 62 http.Error(w, "Forbiden", http.StatusUnauthorized) 63 63 return 64 64 } 65 - f, err := s.fullyResolvedRepo(r) 65 + f, err := s.repoResolver.Resolve(r) 66 66 if err != nil { 67 67 http.Error(w, "malformed url", http.StatusBadRequest) 68 68 return ··· 149 149 func ResolvePull(s *State) middleware.Middleware { 150 150 return func(next http.Handler) http.Handler { 151 151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 - f, err := s.fullyResolvedRepo(r) 152 + f, err := s.repoResolver.Resolve(r) 153 153 if err != nil { 154 154 log.Println("failed to fully resolve repo", err) 155 155 http.Error(w, "invalid repo url", http.StatusNotFound) ··· 198 198 func GoImport(s *State) middleware.Middleware { 199 199 return func(next http.Handler) http.Handler { 200 200 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 201 - f, err := s.fullyResolvedRepo(r) 201 + f, err := s.repoResolver.Resolve(r) 202 202 if err != nil { 203 203 log.Println("failed to fully resolve repo", err) 204 204 http.Error(w, "invalid repo url", http.StatusNotFound)
+46 -45
appview/state/pull.go
··· 18 18 "tangled.sh/tangled.sh/core/appview/db" 19 19 "tangled.sh/tangled.sh/core/appview/oauth" 20 20 "tangled.sh/tangled.sh/core/appview/pages" 21 + "tangled.sh/tangled.sh/core/appview/reporesolver" 21 22 "tangled.sh/tangled.sh/core/knotclient" 22 23 "tangled.sh/tangled.sh/core/patchutil" 23 24 "tangled.sh/tangled.sh/core/types" ··· 36 37 switch r.Method { 37 38 case http.MethodGet: 38 39 user := s.oauth.GetUser(r) 39 - f, err := s.fullyResolvedRepo(r) 40 + f, err := s.repoResolver.Resolve(r) 40 41 if err != nil { 41 42 log.Println("failed to get repo and knot", err) 42 43 return ··· 71 72 72 73 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 73 74 LoggedInUser: user, 74 - RepoInfo: f.RepoInfo(s, user), 75 + RepoInfo: f.RepoInfo(user), 75 76 Pull: pull, 76 77 RoundNumber: roundNumber, 77 78 MergeCheck: mergeCheckResponse, ··· 84 85 85 86 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 86 87 user := s.oauth.GetUser(r) 87 - f, err := s.fullyResolvedRepo(r) 88 + f, err := s.repoResolver.Resolve(r) 88 89 if err != nil { 89 90 log.Println("failed to get repo and knot", err) 90 91 return ··· 136 137 137 138 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 138 139 LoggedInUser: user, 139 - RepoInfo: f.RepoInfo(s, user), 140 + RepoInfo: f.RepoInfo(user), 140 141 DidHandleMap: didHandleMap, 141 142 Pull: pull, 142 143 Stack: stack, ··· 146 147 }) 147 148 } 148 149 149 - func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse { 150 + func (s *State) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse { 150 151 if pull.State == db.PullMerged { 151 152 return types.MergeCheckResponse{} 152 153 } ··· 216 217 return mergeCheckResponse 217 218 } 218 219 219 - func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult { 220 + func (s *State) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult { 220 221 if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil { 221 222 return pages.Unknown 222 223 } ··· 271 272 272 273 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 273 274 user := s.oauth.GetUser(r) 274 - f, err := s.fullyResolvedRepo(r) 275 + f, err := s.repoResolver.Resolve(r) 275 276 if err != nil { 276 277 log.Println("failed to get repo and knot", err) 277 278 return ··· 311 312 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 312 313 LoggedInUser: user, 313 314 DidHandleMap: didHandleMap, 314 - RepoInfo: f.RepoInfo(s, user), 315 + RepoInfo: f.RepoInfo(user), 315 316 Pull: pull, 316 317 Stack: stack, 317 318 Round: roundIdInt, ··· 324 325 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 325 326 user := s.oauth.GetUser(r) 326 327 327 - f, err := s.fullyResolvedRepo(r) 328 + f, err := s.repoResolver.Resolve(r) 328 329 if err != nil { 329 330 log.Println("failed to get repo and knot", err) 330 331 return ··· 380 381 381 382 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 382 383 LoggedInUser: s.oauth.GetUser(r), 383 - RepoInfo: f.RepoInfo(s, user), 384 + RepoInfo: f.RepoInfo(user), 384 385 Pull: pull, 385 386 Round: roundIdInt, 386 387 DidHandleMap: didHandleMap, ··· 432 433 state = db.PullMerged 433 434 } 434 435 435 - f, err := s.fullyResolvedRepo(r) 436 + f, err := s.repoResolver.Resolve(r) 436 437 if err != nil { 437 438 log.Println("failed to get repo and knot", err) 438 439 return ··· 480 481 481 482 s.pages.RepoPulls(w, pages.RepoPullsParams{ 482 483 LoggedInUser: s.oauth.GetUser(r), 483 - RepoInfo: f.RepoInfo(s, user), 484 + RepoInfo: f.RepoInfo(user), 484 485 Pulls: pulls, 485 486 DidHandleMap: didHandleMap, 486 487 FilteringBy: state, ··· 490 491 491 492 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 492 493 user := s.oauth.GetUser(r) 493 - f, err := s.fullyResolvedRepo(r) 494 + f, err := s.repoResolver.Resolve(r) 494 495 if err != nil { 495 496 log.Println("failed to get repo and knot", err) 496 497 return ··· 515 516 case http.MethodGet: 516 517 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 517 518 LoggedInUser: user, 518 - RepoInfo: f.RepoInfo(s, user), 519 + RepoInfo: f.RepoInfo(user), 519 520 Pull: pull, 520 521 RoundNumber: roundNumber, 521 522 }) ··· 613 614 614 615 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 615 616 user := s.oauth.GetUser(r) 616 - f, err := s.fullyResolvedRepo(r) 617 + f, err := s.repoResolver.Resolve(r) 617 618 if err != nil { 618 619 log.Println("failed to get repo and knot", err) 619 620 return ··· 642 643 643 644 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 644 645 LoggedInUser: user, 645 - RepoInfo: f.RepoInfo(s, user), 646 + RepoInfo: f.RepoInfo(user), 646 647 Branches: result.Branches, 647 648 Strategy: strategy, 648 649 SourceBranch: sourceBranch, ··· 665 666 } 666 667 667 668 // Determine PR type based on input parameters 668 - isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 669 + isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed() 669 670 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 670 671 isForkBased := fromFork != "" && sourceBranch != "" 671 672 isPatchBased := patch != "" && !isBranchBased && !isForkBased ··· 736 737 func (s *State) handleBranchBasedPull( 737 738 w http.ResponseWriter, 738 739 r *http.Request, 739 - f *FullyResolvedRepo, 740 + f *reporesolver.ResolvedRepo, 740 741 user *oauth.User, 741 742 title, 742 743 body, ··· 777 778 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked) 778 779 } 779 780 780 - func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 781 + func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 781 782 if !patchutil.IsPatchValid(patch) { 782 783 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 783 784 return ··· 786 787 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked) 787 788 } 788 789 789 - func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 790 + func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 790 791 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 791 792 if errors.Is(err, sql.ErrNoRows) { 792 793 s.pages.Notice(w, "pull", "No such fork.") ··· 869 870 func (s *State) createPullRequest( 870 871 w http.ResponseWriter, 871 872 r *http.Request, 872 - f *FullyResolvedRepo, 873 + f *reporesolver.ResolvedRepo, 873 874 user *oauth.User, 874 875 title, body, targetBranch string, 875 876 patch string, ··· 998 999 func (s *State) createStackedPulLRequest( 999 1000 w http.ResponseWriter, 1000 1001 r *http.Request, 1001 - f *FullyResolvedRepo, 1002 + f *reporesolver.ResolvedRepo, 1002 1003 user *oauth.User, 1003 1004 targetBranch string, 1004 1005 patch string, ··· 1097 1098 } 1098 1099 1099 1100 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1100 - _, err := s.fullyResolvedRepo(r) 1101 + _, err := s.repoResolver.Resolve(r) 1101 1102 if err != nil { 1102 1103 log.Println("failed to get repo and knot", err) 1103 1104 return ··· 1123 1124 1124 1125 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1125 1126 user := s.oauth.GetUser(r) 1126 - f, err := s.fullyResolvedRepo(r) 1127 + f, err := s.repoResolver.Resolve(r) 1127 1128 if err != nil { 1128 1129 log.Println("failed to get repo and knot", err) 1129 1130 return 1130 1131 } 1131 1132 1132 1133 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1133 - RepoInfo: f.RepoInfo(s, user), 1134 + RepoInfo: f.RepoInfo(user), 1134 1135 }) 1135 1136 } 1136 1137 1137 1138 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1138 1139 user := s.oauth.GetUser(r) 1139 - f, err := s.fullyResolvedRepo(r) 1140 + f, err := s.repoResolver.Resolve(r) 1140 1141 if err != nil { 1141 1142 log.Println("failed to get repo and knot", err) 1142 1143 return ··· 1169 1170 } 1170 1171 1171 1172 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1172 - RepoInfo: f.RepoInfo(s, user), 1173 + RepoInfo: f.RepoInfo(user), 1173 1174 Branches: withoutDefault, 1174 1175 }) 1175 1176 } 1176 1177 1177 1178 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1178 1179 user := s.oauth.GetUser(r) 1179 - f, err := s.fullyResolvedRepo(r) 1180 + f, err := s.repoResolver.Resolve(r) 1180 1181 if err != nil { 1181 1182 log.Println("failed to get repo and knot", err) 1182 1183 return ··· 1189 1190 } 1190 1191 1191 1192 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1192 - RepoInfo: f.RepoInfo(s, user), 1193 + RepoInfo: f.RepoInfo(user), 1193 1194 Forks: forks, 1194 1195 Selected: r.URL.Query().Get("fork"), 1195 1196 }) ··· 1198 1199 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1199 1200 user := s.oauth.GetUser(r) 1200 1201 1201 - f, err := s.fullyResolvedRepo(r) 1202 + f, err := s.repoResolver.Resolve(r) 1202 1203 if err != nil { 1203 1204 log.Println("failed to get repo and knot", err) 1204 1205 return ··· 1245 1246 }) 1246 1247 1247 1248 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1248 - RepoInfo: f.RepoInfo(s, user), 1249 + RepoInfo: f.RepoInfo(user), 1249 1250 SourceBranches: sourceBranches, 1250 1251 TargetBranches: targetResult.Branches, 1251 1252 }) ··· 1253 1254 1254 1255 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1255 1256 user := s.oauth.GetUser(r) 1256 - f, err := s.fullyResolvedRepo(r) 1257 + f, err := s.repoResolver.Resolve(r) 1257 1258 if err != nil { 1258 1259 log.Println("failed to get repo and knot", err) 1259 1260 return ··· 1269 1270 switch r.Method { 1270 1271 case http.MethodGet: 1271 1272 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1272 - RepoInfo: f.RepoInfo(s, user), 1273 + RepoInfo: f.RepoInfo(user), 1273 1274 Pull: pull, 1274 1275 }) 1275 1276 return ··· 1297 1298 return 1298 1299 } 1299 1300 1300 - f, err := s.fullyResolvedRepo(r) 1301 + f, err := s.repoResolver.Resolve(r) 1301 1302 if err != nil { 1302 1303 log.Println("failed to get repo and knot", err) 1303 1304 return ··· 1324 1325 return 1325 1326 } 1326 1327 1327 - f, err := s.fullyResolvedRepo(r) 1328 + f, err := s.repoResolver.Resolve(r) 1328 1329 if err != nil { 1329 1330 log.Println("failed to get repo and knot", err) 1330 1331 return ··· 1336 1337 return 1337 1338 } 1338 1339 1339 - if !f.RepoInfo(s, user).Roles.IsPushAllowed() { 1340 + if !f.RepoInfo(user).Roles.IsPushAllowed() { 1340 1341 log.Println("unauthorized user") 1341 1342 w.WriteHeader(http.StatusUnauthorized) 1342 1343 return ··· 1372 1373 return 1373 1374 } 1374 1375 1375 - f, err := s.fullyResolvedRepo(r) 1376 + f, err := s.repoResolver.Resolve(r) 1376 1377 if err != nil { 1377 1378 log.Println("failed to get repo and knot", err) 1378 1379 return ··· 1455 1456 func (s *State) resubmitPullHelper( 1456 1457 w http.ResponseWriter, 1457 1458 r *http.Request, 1458 - f *FullyResolvedRepo, 1459 + f *reporesolver.ResolvedRepo, 1459 1460 user *oauth.User, 1460 1461 pull *db.Pull, 1461 1462 patch string, ··· 1557 1558 func (s *State) resubmitStackedPullHelper( 1558 1559 w http.ResponseWriter, 1559 1560 r *http.Request, 1560 - f *FullyResolvedRepo, 1561 + f *reporesolver.ResolvedRepo, 1561 1562 user *oauth.User, 1562 1563 pull *db.Pull, 1563 1564 patch string, ··· 1799 1800 } 1800 1801 1801 1802 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1802 - f, err := s.fullyResolvedRepo(r) 1803 + f, err := s.repoResolver.Resolve(r) 1803 1804 if err != nil { 1804 1805 log.Println("failed to resolve repo:", err) 1805 1806 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1904 1905 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1905 1906 user := s.oauth.GetUser(r) 1906 1907 1907 - f, err := s.fullyResolvedRepo(r) 1908 + f, err := s.repoResolver.Resolve(r) 1908 1909 if err != nil { 1909 1910 log.Println("malformed middleware") 1910 1911 return ··· 1918 1919 } 1919 1920 1920 1921 // auth filter: only owner or collaborators can close 1921 - roles := RolesInRepo(s, user, f) 1922 + roles := f.RolesInRepo(user) 1922 1923 isCollaborator := roles.IsCollaborator() 1923 1924 isPullAuthor := user.Did == pull.OwnerDid 1924 1925 isCloseAllowed := isCollaborator || isPullAuthor ··· 1971 1972 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1972 1973 user := s.oauth.GetUser(r) 1973 1974 1974 - f, err := s.fullyResolvedRepo(r) 1975 + f, err := s.repoResolver.Resolve(r) 1975 1976 if err != nil { 1976 1977 log.Println("failed to resolve repo", err) 1977 1978 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") ··· 1986 1987 } 1987 1988 1988 1989 // auth filter: only owner or collaborators can close 1989 - roles := RolesInRepo(s, user, f) 1990 + roles := f.RolesInRepo(user) 1990 1991 isCollaborator := roles.IsCollaborator() 1991 1992 isPullAuthor := user.Did == pull.OwnerDid 1992 1993 isCloseAllowed := isCollaborator || isPullAuthor ··· 2036 2037 return 2037 2038 } 2038 2039 2039 - func newStack(f *FullyResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2040 + func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) { 2040 2041 formatPatches, err := patchutil.ExtractPatches(patch) 2041 2042 if err != nil { 2042 2043 return nil, fmt.Errorf("Failed to extract patches: %v", err)
+53 -220
appview/state/repo.go
··· 1 1 package state 2 2 3 3 import ( 4 - "context" 5 4 "database/sql" 6 5 "encoding/json" 7 6 "errors" ··· 25 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 26 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 27 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 + "tangled.sh/tangled.sh/core/appview/reporesolver" 28 28 "tangled.sh/tangled.sh/core/knotclient" 29 29 "tangled.sh/tangled.sh/core/patchutil" 30 30 "tangled.sh/tangled.sh/core/types" 31 31 32 32 "github.com/bluesky-social/indigo/atproto/data" 33 - "github.com/bluesky-social/indigo/atproto/identity" 34 - "github.com/bluesky-social/indigo/atproto/syntax" 35 33 securejoin "github.com/cyphar/filepath-securejoin" 36 34 "github.com/go-chi/chi/v5" 37 35 "github.com/go-git/go-git/v5/plumbing" ··· 43 41 44 42 func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 45 43 ref := chi.URLParam(r, "ref") 46 - f, err := s.fullyResolvedRepo(r) 44 + f, err := s.repoResolver.Resolve(r) 47 45 if err != nil { 48 46 log.Println("failed to fully resolve repo", err) 49 47 return ··· 110 108 emails := uniqueEmails(commitsTrunc) 111 109 112 110 user := s.oauth.GetUser(r) 113 - repoInfo := f.RepoInfo(s, user) 111 + repoInfo := f.RepoInfo(user) 114 112 115 113 secret, err := db.GetRegistrationKey(s.db, f.Knot) 116 114 if err != nil { ··· 157 155 func getForkInfo( 158 156 repoInfo repoinfo.RepoInfo, 159 157 s *State, 160 - f *FullyResolvedRepo, 158 + f *reporesolver.ResolvedRepo, 161 159 user *oauth.User, 162 160 signedClient *knotclient.SignedClient, 163 161 ) (*types.ForkInfo, error) { ··· 219 217 } 220 218 221 219 func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 222 - f, err := s.fullyResolvedRepo(r) 220 + f, err := s.repoResolver.Resolve(r) 223 221 if err != nil { 224 222 log.Println("failed to fully resolve repo", err) 225 223 return ··· 266 264 s.pages.RepoLog(w, pages.RepoLogParams{ 267 265 LoggedInUser: user, 268 266 TagMap: tagMap, 269 - RepoInfo: f.RepoInfo(s, user), 267 + RepoInfo: f.RepoInfo(user), 270 268 RepoLogResponse: *repolog, 271 269 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 272 270 }) ··· 274 272 } 275 273 276 274 func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 277 - f, err := s.fullyResolvedRepo(r) 275 + f, err := s.repoResolver.Resolve(r) 278 276 if err != nil { 279 277 log.Println("failed to get repo and knot", err) 280 278 w.WriteHeader(http.StatusBadRequest) ··· 283 281 284 282 user := s.oauth.GetUser(r) 285 283 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 286 - RepoInfo: f.RepoInfo(s, user), 284 + RepoInfo: f.RepoInfo(user), 287 285 }) 288 286 return 289 287 } 290 288 291 289 func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 292 - f, err := s.fullyResolvedRepo(r) 290 + f, err := s.repoResolver.Resolve(r) 293 291 if err != nil { 294 292 log.Println("failed to get repo and knot", err) 295 293 w.WriteHeader(http.StatusBadRequest) ··· 309 307 switch r.Method { 310 308 case http.MethodGet: 311 309 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 312 - RepoInfo: f.RepoInfo(s, user), 310 + RepoInfo: f.RepoInfo(user), 313 311 }) 314 312 return 315 313 case http.MethodPut: ··· 362 360 return 363 361 } 364 362 365 - newRepoInfo := f.RepoInfo(s, user) 363 + newRepoInfo := f.RepoInfo(user) 366 364 newRepoInfo.Description = newDescription 367 365 368 366 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ ··· 373 371 } 374 372 375 373 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 376 - f, err := s.fullyResolvedRepo(r) 374 + f, err := s.repoResolver.Resolve(r) 377 375 if err != nil { 378 376 log.Println("failed to fully resolve repo", err) 379 377 return ··· 411 409 user := s.oauth.GetUser(r) 412 410 s.pages.RepoCommit(w, pages.RepoCommitParams{ 413 411 LoggedInUser: user, 414 - RepoInfo: f.RepoInfo(s, user), 412 + RepoInfo: f.RepoInfo(user), 415 413 RepoCommitResponse: result, 416 414 EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 417 415 }) ··· 419 417 } 420 418 421 419 func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 422 - f, err := s.fullyResolvedRepo(r) 420 + f, err := s.repoResolver.Resolve(r) 423 421 if err != nil { 424 422 log.Println("failed to fully resolve repo", err) 425 423 return ··· 475 473 BreadCrumbs: breadcrumbs, 476 474 BaseTreeLink: baseTreeLink, 477 475 BaseBlobLink: baseBlobLink, 478 - RepoInfo: f.RepoInfo(s, user), 476 + RepoInfo: f.RepoInfo(user), 479 477 RepoTreeResponse: result, 480 478 }) 481 479 return 482 480 } 483 481 484 482 func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 485 - f, err := s.fullyResolvedRepo(r) 483 + f, err := s.repoResolver.Resolve(r) 486 484 if err != nil { 487 485 log.Println("failed to get repo and knot", err) 488 486 return ··· 531 529 user := s.oauth.GetUser(r) 532 530 s.pages.RepoTags(w, pages.RepoTagsParams{ 533 531 LoggedInUser: user, 534 - RepoInfo: f.RepoInfo(s, user), 532 + RepoInfo: f.RepoInfo(user), 535 533 RepoTagsResponse: *result, 536 534 ArtifactMap: artifactMap, 537 535 DanglingArtifacts: danglingArtifacts, ··· 540 538 } 541 539 542 540 func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 543 - f, err := s.fullyResolvedRepo(r) 541 + f, err := s.repoResolver.Resolve(r) 544 542 if err != nil { 545 543 log.Println("failed to get repo and knot", err) 546 544 return ··· 578 576 user := s.oauth.GetUser(r) 579 577 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 580 578 LoggedInUser: user, 581 - RepoInfo: f.RepoInfo(s, user), 579 + RepoInfo: f.RepoInfo(user), 582 580 RepoBranchesResponse: *result, 583 581 }) 584 582 return 585 583 } 586 584 587 585 func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 588 - f, err := s.fullyResolvedRepo(r) 586 + f, err := s.repoResolver.Resolve(r) 589 587 if err != nil { 590 588 log.Println("failed to get repo and knot", err) 591 589 return ··· 635 633 user := s.oauth.GetUser(r) 636 634 s.pages.RepoBlob(w, pages.RepoBlobParams{ 637 635 LoggedInUser: user, 638 - RepoInfo: f.RepoInfo(s, user), 636 + RepoInfo: f.RepoInfo(user), 639 637 RepoBlobResponse: result, 640 638 BreadCrumbs: breadcrumbs, 641 639 ShowRendered: showRendered, ··· 645 643 } 646 644 647 645 func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 648 - f, err := s.fullyResolvedRepo(r) 646 + f, err := s.repoResolver.Resolve(r) 649 647 if err != nil { 650 648 log.Println("failed to get repo and knot", err) 651 649 return ··· 689 687 } 690 688 691 689 func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 692 - f, err := s.fullyResolvedRepo(r) 690 + f, err := s.repoResolver.Resolve(r) 693 691 if err != nil { 694 692 log.Println("failed to get repo and knot", err) 695 693 return ··· 780 778 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 781 779 user := s.oauth.GetUser(r) 782 780 783 - f, err := s.fullyResolvedRepo(r) 781 + f, err := s.repoResolver.Resolve(r) 784 782 if err != nil { 785 783 log.Println("failed to get repo and knot", err) 786 784 return ··· 888 886 } 889 887 890 888 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 891 - f, err := s.fullyResolvedRepo(r) 889 + f, err := s.repoResolver.Resolve(r) 892 890 if err != nil { 893 891 log.Println("failed to get repo and knot", err) 894 892 return ··· 927 925 } 928 926 929 927 func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 930 - f, err := s.fullyResolvedRepo(r) 928 + f, err := s.repoResolver.Resolve(r) 931 929 if err != nil { 932 930 log.Println("failed to get repo and knot", err) 933 931 return ··· 937 935 case http.MethodGet: 938 936 // for now, this is just pubkeys 939 937 user := s.oauth.GetUser(r) 940 - repoCollaborators, err := f.Collaborators(r.Context(), s) 938 + repoCollaborators, err := f.Collaborators(r.Context()) 941 939 if err != nil { 942 940 log.Println("failed to get collaborators", err) 943 941 } ··· 964 962 965 963 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 966 964 LoggedInUser: user, 967 - RepoInfo: f.RepoInfo(s, user), 965 + RepoInfo: f.RepoInfo(user), 968 966 Collaborators: repoCollaborators, 969 967 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 970 968 Branches: result.Branches, ··· 972 970 } 973 971 } 974 972 975 - type FullyResolvedRepo struct { 976 - Knot string 977 - OwnerId identity.Identity 978 - RepoName string 979 - RepoAt syntax.ATURI 980 - Description string 981 - CreatedAt string 982 - Ref string 983 - CurrentDir string 984 - } 985 - 986 - func (f *FullyResolvedRepo) OwnerDid() string { 987 - return f.OwnerId.DID.String() 988 - } 989 - 990 - func (f *FullyResolvedRepo) OwnerHandle() string { 991 - return f.OwnerId.Handle.String() 992 - } 993 - 994 - func (f *FullyResolvedRepo) OwnerSlashRepo() string { 995 - handle := f.OwnerId.Handle 996 - 997 - var p string 998 - if handle != "" && !handle.IsInvalidHandle() { 999 - p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 1000 - } else { 1001 - p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 1002 - } 1003 - 1004 - return p 1005 - } 1006 - 1007 - func (f *FullyResolvedRepo) DidSlashRepo() string { 1008 - p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 1009 - return p 1010 - } 1011 - 1012 - func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 1013 - repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 1014 - if err != nil { 1015 - return nil, err 1016 - } 1017 - 1018 - var collaborators []pages.Collaborator 1019 - for _, item := range repoCollaborators { 1020 - // currently only two roles: owner and member 1021 - var role string 1022 - if item[3] == "repo:owner" { 1023 - role = "owner" 1024 - } else if item[3] == "repo:collaborator" { 1025 - role = "collaborator" 1026 - } else { 1027 - continue 1028 - } 1029 - 1030 - did := item[0] 1031 - 1032 - c := pages.Collaborator{ 1033 - Did: did, 1034 - Handle: "", 1035 - Role: role, 1036 - } 1037 - collaborators = append(collaborators, c) 1038 - } 1039 - 1040 - // populate all collborators with handles 1041 - identsToResolve := make([]string, len(collaborators)) 1042 - for i, collab := range collaborators { 1043 - identsToResolve[i] = collab.Did 1044 - } 1045 - 1046 - resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 1047 - for i, resolved := range resolvedIdents { 1048 - if resolved != nil { 1049 - collaborators[i].Handle = resolved.Handle.String() 1050 - } 1051 - } 1052 - 1053 - return collaborators, nil 1054 - } 1055 - 1056 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { 1057 - isStarred := false 1058 - if u != nil { 1059 - isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 1060 - } 1061 - 1062 - starCount, err := db.GetStarCount(s.db, f.RepoAt) 1063 - if err != nil { 1064 - log.Println("failed to get star count for ", f.RepoAt) 1065 - } 1066 - issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 1067 - if err != nil { 1068 - log.Println("failed to get issue count for ", f.RepoAt) 1069 - } 1070 - pullCount, err := db.GetPullCount(s.db, f.RepoAt) 1071 - if err != nil { 1072 - log.Println("failed to get issue count for ", f.RepoAt) 1073 - } 1074 - source, err := db.GetRepoSource(s.db, f.RepoAt) 1075 - if errors.Is(err, sql.ErrNoRows) { 1076 - source = "" 1077 - } else if err != nil { 1078 - log.Println("failed to get repo source for ", f.RepoAt, err) 1079 - } 1080 - 1081 - var sourceRepo *db.Repo 1082 - if source != "" { 1083 - sourceRepo, err = db.GetRepoByAtUri(s.db, source) 1084 - if err != nil { 1085 - log.Println("failed to get repo by at uri", err) 1086 - } 1087 - } 1088 - 1089 - var sourceHandle *identity.Identity 1090 - if sourceRepo != nil { 1091 - sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did) 1092 - if err != nil { 1093 - log.Println("failed to resolve source repo", err) 1094 - } 1095 - } 1096 - 1097 - knot := f.Knot 1098 - var disableFork bool 1099 - us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 1100 - if err != nil { 1101 - log.Printf("failed to create unsigned client for %s: %v", knot, err) 1102 - } else { 1103 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1104 - if err != nil { 1105 - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 1106 - } 1107 - 1108 - if len(result.Branches) == 0 { 1109 - disableFork = true 1110 - } 1111 - } 1112 - 1113 - repoInfo := repoinfo.RepoInfo{ 1114 - OwnerDid: f.OwnerDid(), 1115 - OwnerHandle: f.OwnerHandle(), 1116 - Name: f.RepoName, 1117 - RepoAt: f.RepoAt, 1118 - Description: f.Description, 1119 - Ref: f.Ref, 1120 - IsStarred: isStarred, 1121 - Knot: knot, 1122 - Roles: RolesInRepo(s, u, f), 1123 - Stats: db.RepoStats{ 1124 - StarCount: starCount, 1125 - IssueCount: issueCount, 1126 - PullCount: pullCount, 1127 - }, 1128 - DisableFork: disableFork, 1129 - CurrentDir: f.CurrentDir, 1130 - } 1131 - 1132 - if sourceRepo != nil { 1133 - repoInfo.Source = sourceRepo 1134 - repoInfo.SourceHandle = sourceHandle.Handle.String() 1135 - } 1136 - 1137 - return repoInfo 1138 - } 1139 - 1140 973 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1141 974 user := s.oauth.GetUser(r) 1142 - f, err := s.fullyResolvedRepo(r) 975 + f, err := s.repoResolver.Resolve(r) 1143 976 if err != nil { 1144 977 log.Println("failed to get repo and knot", err) 1145 978 return ··· 1181 1014 1182 1015 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1183 1016 LoggedInUser: user, 1184 - RepoInfo: f.RepoInfo(s, user), 1017 + RepoInfo: f.RepoInfo(user), 1185 1018 Issue: *issue, 1186 1019 Comments: comments, 1187 1020 ··· 1193 1026 1194 1027 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1195 1028 user := s.oauth.GetUser(r) 1196 - f, err := s.fullyResolvedRepo(r) 1029 + f, err := s.repoResolver.Resolve(r) 1197 1030 if err != nil { 1198 1031 log.Println("failed to get repo and knot", err) 1199 1032 return ··· 1214 1047 return 1215 1048 } 1216 1049 1217 - collaborators, err := f.Collaborators(r.Context(), s) 1050 + collaborators, err := f.Collaborators(r.Context()) 1218 1051 if err != nil { 1219 1052 log.Println("failed to fetch repo collaborators: %w", err) 1220 1053 } ··· 1269 1102 1270 1103 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1271 1104 user := s.oauth.GetUser(r) 1272 - f, err := s.fullyResolvedRepo(r) 1105 + f, err := s.repoResolver.Resolve(r) 1273 1106 if err != nil { 1274 1107 log.Println("failed to get repo and knot", err) 1275 1108 return ··· 1290 1123 return 1291 1124 } 1292 1125 1293 - collaborators, err := f.Collaborators(r.Context(), s) 1126 + collaborators, err := f.Collaborators(r.Context()) 1294 1127 if err != nil { 1295 1128 log.Println("failed to fetch repo collaborators: %w", err) 1296 1129 } ··· 1317 1150 1318 1151 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1319 1152 user := s.oauth.GetUser(r) 1320 - f, err := s.fullyResolvedRepo(r) 1153 + f, err := s.repoResolver.Resolve(r) 1321 1154 if err != nil { 1322 1155 log.Println("failed to get repo and knot", err) 1323 1156 return ··· 1401 1234 1402 1235 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1403 1236 user := s.oauth.GetUser(r) 1404 - f, err := s.fullyResolvedRepo(r) 1237 + f, err := s.repoResolver.Resolve(r) 1405 1238 if err != nil { 1406 1239 log.Println("failed to get repo and knot", err) 1407 1240 return ··· 1451 1284 1452 1285 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1453 1286 LoggedInUser: user, 1454 - RepoInfo: f.RepoInfo(s, user), 1287 + RepoInfo: f.RepoInfo(user), 1455 1288 DidHandleMap: didHandleMap, 1456 1289 Issue: issue, 1457 1290 Comment: comment, ··· 1460 1293 1461 1294 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1462 1295 user := s.oauth.GetUser(r) 1463 - f, err := s.fullyResolvedRepo(r) 1296 + f, err := s.repoResolver.Resolve(r) 1464 1297 if err != nil { 1465 1298 log.Println("failed to get repo and knot", err) 1466 1299 return ··· 1504 1337 case http.MethodGet: 1505 1338 s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1506 1339 LoggedInUser: user, 1507 - RepoInfo: f.RepoInfo(s, user), 1340 + RepoInfo: f.RepoInfo(user), 1508 1341 Issue: issue, 1509 1342 Comment: comment, 1510 1343 }) ··· 1577 1410 // return new comment body with htmx 1578 1411 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1579 1412 LoggedInUser: user, 1580 - RepoInfo: f.RepoInfo(s, user), 1413 + RepoInfo: f.RepoInfo(user), 1581 1414 DidHandleMap: didHandleMap, 1582 1415 Issue: issue, 1583 1416 Comment: comment, ··· 1590 1423 1591 1424 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1592 1425 user := s.oauth.GetUser(r) 1593 - f, err := s.fullyResolvedRepo(r) 1426 + f, err := s.repoResolver.Resolve(r) 1594 1427 if err != nil { 1595 1428 log.Println("failed to get repo and knot", err) 1596 1429 return ··· 1672 1505 // htmx fragment of comment after deletion 1673 1506 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1674 1507 LoggedInUser: user, 1675 - RepoInfo: f.RepoInfo(s, user), 1508 + RepoInfo: f.RepoInfo(user), 1676 1509 DidHandleMap: didHandleMap, 1677 1510 Issue: issue, 1678 1511 Comment: comment, ··· 1700 1533 } 1701 1534 1702 1535 user := s.oauth.GetUser(r) 1703 - f, err := s.fullyResolvedRepo(r) 1536 + f, err := s.repoResolver.Resolve(r) 1704 1537 if err != nil { 1705 1538 log.Println("failed to get repo and knot", err) 1706 1539 return ··· 1729 1562 1730 1563 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1731 1564 LoggedInUser: s.oauth.GetUser(r), 1732 - RepoInfo: f.RepoInfo(s, user), 1565 + RepoInfo: f.RepoInfo(user), 1733 1566 Issues: issues, 1734 1567 DidHandleMap: didHandleMap, 1735 1568 FilteringByOpen: isOpen, ··· 1741 1574 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1742 1575 user := s.oauth.GetUser(r) 1743 1576 1744 - f, err := s.fullyResolvedRepo(r) 1577 + f, err := s.repoResolver.Resolve(r) 1745 1578 if err != nil { 1746 1579 log.Println("failed to get repo and knot", err) 1747 1580 return ··· 1751 1584 case http.MethodGet: 1752 1585 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1753 1586 LoggedInUser: user, 1754 - RepoInfo: f.RepoInfo(s, user), 1587 + RepoInfo: f.RepoInfo(user), 1755 1588 }) 1756 1589 case http.MethodPost: 1757 1590 title := r.FormValue("title") ··· 1839 1672 1840 1673 func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1841 1674 user := s.oauth.GetUser(r) 1842 - f, err := s.fullyResolvedRepo(r) 1675 + f, err := s.repoResolver.Resolve(r) 1843 1676 if err != nil { 1844 1677 log.Printf("failed to resolve source repo: %v", err) 1845 1678 return ··· 1881 1714 1882 1715 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1883 1716 user := s.oauth.GetUser(r) 1884 - f, err := s.fullyResolvedRepo(r) 1717 + f, err := s.repoResolver.Resolve(r) 1885 1718 if err != nil { 1886 1719 log.Printf("failed to resolve source repo: %v", err) 1887 1720 return ··· 1899 1732 s.pages.ForkRepo(w, pages.ForkRepoParams{ 1900 1733 LoggedInUser: user, 1901 1734 Knots: knots, 1902 - RepoInfo: f.RepoInfo(s, user), 1735 + RepoInfo: f.RepoInfo(user), 1903 1736 }) 1904 1737 1905 1738 case http.MethodPost: ··· 2059 1892 2060 1893 func (s *State) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 2061 1894 user := s.oauth.GetUser(r) 2062 - f, err := s.fullyResolvedRepo(r) 1895 + f, err := s.repoResolver.Resolve(r) 2063 1896 if err != nil { 2064 1897 log.Println("failed to get repo and knot", err) 2065 1898 return ··· 2110 1943 return 2111 1944 } 2112 1945 2113 - repoinfo := f.RepoInfo(s, user) 1946 + repoinfo := f.RepoInfo(user) 2114 1947 2115 1948 s.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 2116 1949 LoggedInUser: user, ··· 2124 1957 2125 1958 func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) { 2126 1959 user := s.oauth.GetUser(r) 2127 - f, err := s.fullyResolvedRepo(r) 1960 + f, err := s.repoResolver.Resolve(r) 2128 1961 if err != nil { 2129 1962 log.Println("failed to get repo and knot", err) 2130 1963 return ··· 2179 2012 } 2180 2013 diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 2181 2014 2182 - repoinfo := f.RepoInfo(s, user) 2015 + repoinfo := f.RepoInfo(user) 2183 2016 2184 2017 s.pages.RepoCompare(w, pages.RepoCompareParams{ 2185 2018 LoggedInUser: user,
-103
appview/state/repo_util.go
··· 6 6 "fmt" 7 7 "log" 8 8 "math/big" 9 - "net/http" 10 - "net/url" 11 - "path" 12 - "strings" 13 9 14 - "github.com/bluesky-social/indigo/atproto/identity" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 - "github.com/go-chi/chi/v5" 17 10 "github.com/go-git/go-git/v5/plumbing/object" 18 11 "tangled.sh/tangled.sh/core/appview/db" 19 - "tangled.sh/tangled.sh/core/appview/oauth" 20 - "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 21 - "tangled.sh/tangled.sh/core/knotclient" 22 12 ) 23 - 24 - func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 25 - repoName := chi.URLParam(r, "repo") 26 - knot, ok := r.Context().Value("knot").(string) 27 - if !ok { 28 - log.Println("malformed middleware") 29 - return nil, fmt.Errorf("malformed middleware") 30 - } 31 - id, ok := r.Context().Value("resolvedId").(identity.Identity) 32 - if !ok { 33 - log.Println("malformed middleware") 34 - return nil, fmt.Errorf("malformed middleware") 35 - } 36 - 37 - repoAt, ok := r.Context().Value("repoAt").(string) 38 - if !ok { 39 - log.Println("malformed middleware") 40 - return nil, fmt.Errorf("malformed middleware") 41 - } 42 - 43 - parsedRepoAt, err := syntax.ParseATURI(repoAt) 44 - if err != nil { 45 - log.Println("malformed repo at-uri") 46 - return nil, fmt.Errorf("malformed middleware") 47 - } 48 - 49 - ref := chi.URLParam(r, "ref") 50 - 51 - if ref == "" { 52 - us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 53 - if err != nil { 54 - return nil, err 55 - } 56 - 57 - defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 58 - if err != nil { 59 - return nil, err 60 - } 61 - 62 - ref = defaultBranch.Branch 63 - } 64 - 65 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 66 - 67 - // pass through values from the middleware 68 - description, ok := r.Context().Value("repoDescription").(string) 69 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 70 - 71 - return &FullyResolvedRepo{ 72 - Knot: knot, 73 - OwnerId: id, 74 - RepoName: repoName, 75 - RepoAt: parsedRepoAt, 76 - Description: description, 77 - CreatedAt: addedAt, 78 - Ref: ref, 79 - CurrentDir: currentDir, 80 - }, nil 81 - } 82 - 83 - func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { 84 - if u != nil { 85 - r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 86 - return repoinfo.RolesInRepo{r} 87 - } else { 88 - return repoinfo.RolesInRepo{} 89 - } 90 - } 91 - 92 - // extractPathAfterRef gets the actual repository path 93 - // after the ref. for example: 94 - // 95 - // /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 96 - func extractPathAfterRef(fullPath, ref string) string { 97 - fullPath = strings.TrimPrefix(fullPath, "/") 98 - 99 - ref = url.PathEscape(ref) 100 - 101 - prefixes := []string{ 102 - fmt.Sprintf("blob/%s/", ref), 103 - fmt.Sprintf("tree/%s/", ref), 104 - fmt.Sprintf("raw/%s/", ref), 105 - } 106 - 107 - for _, prefix := range prefixes { 108 - idx := strings.Index(fullPath, prefix) 109 - if idx != -1 { 110 - return fullPath[idx+len(prefix):] 111 - } 112 - } 113 - 114 - return "" 115 - } 116 13 117 14 func uniqueEmails(commits []*object.Commit) []string { 118 15 emails := make(map[string]struct{})
+14 -9
appview/state/state.go
··· 23 23 "tangled.sh/tangled.sh/core/appview/db" 24 24 "tangled.sh/tangled.sh/core/appview/oauth" 25 25 "tangled.sh/tangled.sh/core/appview/pages" 26 + "tangled.sh/tangled.sh/core/appview/reporesolver" 26 27 "tangled.sh/tangled.sh/core/jetstream" 27 28 "tangled.sh/tangled.sh/core/knotclient" 28 29 "tangled.sh/tangled.sh/core/rbac" 29 30 ) 30 31 31 32 type State struct { 32 - db *db.DB 33 - oauth *oauth.OAuth 34 - enforcer *rbac.Enforcer 35 - tidClock syntax.TIDClock 36 - pages *pages.Pages 37 - resolver *appview.Resolver 38 - posthog posthog.Client 39 - jc *jetstream.JetstreamClient 40 - config *appview.Config 33 + db *db.DB 34 + oauth *oauth.OAuth 35 + enforcer *rbac.Enforcer 36 + tidClock syntax.TIDClock 37 + pages *pages.Pages 38 + resolver *appview.Resolver 39 + posthog posthog.Client 40 + jc *jetstream.JetstreamClient 41 + config *appview.Config 42 + repoResolver *reporesolver.RepoResolver 41 43 } 42 44 43 45 func Make(config *appview.Config) (*State, error) { ··· 67 69 if err != nil { 68 70 return nil, fmt.Errorf("failed to create posthog client: %w", err) 69 71 } 72 + 73 + repoResolver := reporesolver.New(config, enforcer, resolver, d) 70 74 71 75 wrapper := db.DbWrapper{d} 72 76 jc, err := jetstream.NewJetstreamClient( ··· 102 106 posthog, 103 107 jc, 104 108 config, 109 + repoResolver, 105 110 } 106 111 107 112 return state, nil