Monorepo for Tangled tangled.org

appview: fork-based pull requests

anirudh.fi 5959c27f 1287e6b7

verified
Changed files
+440 -138
appview
+4
appview/db/pulls.go
··· 128 128 return false 129 129 } 130 130 131 + func (p *Pull) IsPatch() bool { 132 + return p.PullSource == nil 133 + } 134 + 131 135 func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff { 132 136 patch := s.Patch 133 137
+87
appview/db/repos.go
··· 190 190 return nullableSource.String, nil 191 191 } 192 192 193 + func GetForksByDid(e Execer, did string) ([]Repo, error) { 194 + var repos []Repo 195 + 196 + rows, err := e.Query( 197 + `select did, name, knot, rkey, description, created, at_uri, source 198 + from repos 199 + where did = ? and source is not null and source != '' 200 + order by created desc`, 201 + did, 202 + ) 203 + if err != nil { 204 + return nil, err 205 + } 206 + defer rows.Close() 207 + 208 + for rows.Next() { 209 + var repo Repo 210 + var createdAt string 211 + var nullableDescription sql.NullString 212 + var nullableSource sql.NullString 213 + 214 + err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 215 + if err != nil { 216 + return nil, err 217 + } 218 + 219 + if nullableDescription.Valid { 220 + repo.Description = nullableDescription.String 221 + } 222 + 223 + if nullableSource.Valid { 224 + repo.Source = nullableSource.String 225 + } 226 + 227 + createdAtTime, err := time.Parse(time.RFC3339, createdAt) 228 + if err != nil { 229 + repo.Created = time.Now() 230 + } else { 231 + repo.Created = createdAtTime 232 + } 233 + 234 + repos = append(repos, repo) 235 + } 236 + 237 + if err := rows.Err(); err != nil { 238 + return nil, err 239 + } 240 + 241 + return repos, nil 242 + } 243 + 244 + func GetForkByDid(e Execer, did string, name string) (*Repo, error) { 245 + var repo Repo 246 + var createdAt string 247 + var nullableDescription sql.NullString 248 + var nullableSource sql.NullString 249 + 250 + row := e.QueryRow( 251 + `select did, name, knot, rkey, description, created, at_uri, source 252 + from repos 253 + where did = ? and name = ? and source is not null and source != ''`, 254 + did, name, 255 + ) 256 + 257 + err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 258 + if err != nil { 259 + return nil, err 260 + } 261 + 262 + if nullableDescription.Valid { 263 + repo.Description = nullableDescription.String 264 + } 265 + 266 + if nullableSource.Valid { 267 + repo.Source = nullableSource.String 268 + } 269 + 270 + createdAtTime, err := time.Parse(time.RFC3339, createdAt) 271 + if err != nil { 272 + repo.Created = time.Now() 273 + } else { 274 + repo.Created = createdAtTime 275 + } 276 + 277 + return &repo, nil 278 + } 279 + 193 280 func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { 194 281 _, err := e.Exec( 195 282 `insert into collaborators (did, repo)
+1
appview/pages/pages.go
··· 575 575 type RepoNewPullParams struct { 576 576 LoggedInUser *auth.User 577 577 RepoInfo RepoInfo 578 + Forks []db.Repo 578 579 Branches []types.Branch 579 580 Active string 580 581 }
+1 -1
appview/pages/templates/fragments/pullActions.html
··· 43 43 {{ $disabled = "disabled" }} 44 44 {{ end }} 45 45 <button id="resubmitBtn" 46 - {{ if $isSameRepoBranch }} 46 + {{ if not .Pull.IsPatch }} 47 47 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 48 48 {{ else }} 49 49 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+1 -1
appview/pages/templates/fragments/pullPatchUpload.html
··· 11 11 12 12 {{ if .RepoInfo.Roles.IsPushAllowed }} 13 13 <div class="mt-4 text-sm"> 14 - you can also submit a pull request from a branch 14 + You can also submit a pull request from a branch. 15 15 <button 16 16 class="btn text-sm" 17 17 hx-get="/{{ .RepoInfo.FullName }}/pulls/new/compare-branches"
+6 -4
appview/pages/templates/repo/pulls/new.html
··· 1 - {{ define "title" }}new pull | {{ .RepoInfo.FullName }}{{ end }} 1 + {{ define "title" }}new pull &middot; {{ .RepoInfo.FullName }}{{ end }} 2 2 3 3 {{ define "repoContent" }} 4 4 <section class="prose dark:prose-invert"> ··· 12 12 <li class="leading-relaxed">Paste the diff output in the form below.</li> 13 13 </ul> 14 14 </p> 15 - </section> 15 + </section> 16 16 <form 17 17 hx-post="/{{ .RepoInfo.FullName }}/pulls/new" 18 18 class="mt-6 space-y-6" ··· 41 41 {{ template "fragments/pullCompareBranches" . }} 42 42 {{ end }} 43 43 44 - <div class="flex justify-end items-center gap-2"> 45 - <button type="submit" class="btn">create</button> 44 + Or, select a fork to create a pull request from. 45 + 46 + <div class="flex justify-start items-center gap-2"> 47 + <button type="submit" class="btn">create pull</button> 46 48 </div> 47 49 48 50 </div>
+7 -1
appview/pages/templates/repo/pulls/pull.html
··· 45 45 {{ .Pull.TargetBranch }} 46 46 </span> 47 47 </span> 48 - {{ if .Pull.IsSameRepoBranch }} 48 + {{ if not .Pull.IsPatch }} 49 49 <span>from 50 + {{ if not .Pull.IsSameRepoBranch }} 51 + <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 52 + {{ .Pull.PullSource.Repo }} 53 + </span> 54 + <span class="select-none">/</span> 55 + {{ end }} 50 56 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 51 57 {{ .Pull.PullSource.Branch }} 52 58 </span>
+332 -130
appview/state/pull.go
··· 1 1 package state 2 2 3 3 import ( 4 + "database/sql" 4 5 "encoding/json" 6 + "errors" 5 7 "fmt" 6 8 "io" 7 9 "log" 8 10 "net/http" 11 + "net/url" 9 12 "strconv" 10 13 "strings" 11 14 "time" 12 15 13 16 "github.com/go-chi/chi/v5" 14 17 "tangled.sh/tangled.sh/core/api/tangled" 18 + "tangled.sh/tangled.sh/core/appview/auth" 15 19 "tangled.sh/tangled.sh/core/appview/db" 16 20 "tangled.sh/tangled.sh/core/appview/pages" 17 21 "tangled.sh/tangled.sh/core/types" 18 22 19 23 comatproto "github.com/bluesky-social/indigo/api/atproto" 24 + "github.com/bluesky-social/indigo/atproto/syntax" 20 25 lexutil "github.com/bluesky-social/indigo/lex/util" 21 26 ) 22 27 ··· 186 191 } 187 192 188 193 func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult { 189 - if pull.State == db.PullMerged { 194 + if pull.State == db.PullMerged || pull.PullSource == nil { 190 195 return pages.Unknown 191 196 } 192 197 193 - if pull.PullSource == nil { 194 - return pages.Unknown 198 + var knot, ownerDid, repoName string 199 + 200 + if pull.PullSource.Repo != nil { 201 + // fork-based pulls 202 + sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.Repo.String()) 203 + if err != nil { 204 + log.Println("failed to get source repo", err) 205 + return pages.Unknown 206 + } 207 + 208 + knot = sourceRepo.Knot 209 + ownerDid = sourceRepo.Did 210 + repoName = sourceRepo.Name 211 + } else { 212 + // pulls within the same repo 213 + knot = f.Knot 214 + ownerDid = f.OwnerDid() 215 + repoName = f.RepoName 195 216 } 196 217 197 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 218 + us, err := NewUnsignedClient(knot, s.config.Dev) 198 219 if err != nil { 199 - log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 220 + log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 200 221 return pages.Unknown 201 222 } 202 223 203 - resp, err := us.Branch(f.OwnerDid(), f.RepoName, pull.PullSource.Branch) 224 + resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 204 225 if err != nil { 205 226 log.Println("failed to reach knotserver", err) 206 227 return pages.Unknown ··· 208 229 209 230 body, err := io.ReadAll(resp.Body) 210 231 if err != nil { 211 - log.Printf("Error reading response body: %v", err) 232 + log.Printf("error reading response body: %v", err) 212 233 return pages.Unknown 213 234 } 235 + defer resp.Body.Close() 214 236 215 237 var result types.RepoBranchResponse 216 - err = json.Unmarshal(body, &result) 217 - if err != nil { 238 + if err := json.Unmarshal(body, &result); err != nil { 218 239 log.Println("failed to parse response:", err) 219 240 return pages.Unknown 220 241 } 221 242 222 - if pull.Submissions[pull.LastRoundNumber()].SourceRev != result.Branch.Hash { 223 - log.Println(pull.Submissions[pull.LastRoundNumber()].SourceRev, result.Branch.Hash) 243 + latestSubmission := pull.Submissions[pull.LastRoundNumber()] 244 + if latestSubmission.SourceRev != result.Branch.Hash { 224 245 return pages.ShouldResubmit 225 - } else { 226 - return pages.ShouldNotResubmit 227 246 } 247 + 248 + return pages.ShouldNotResubmit 228 249 } 229 250 230 251 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { ··· 468 489 return 469 490 } 470 491 492 + forks, err := db.GetForksByDid(s.db, user.Did) 493 + if err != nil { 494 + log.Println("failed to get forks", err) 495 + return 496 + } 497 + 471 498 switch r.Method { 472 499 case http.MethodGet: 473 500 us, err := NewUnsignedClient(f.Knot, s.config.Dev) ··· 499 526 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 500 527 LoggedInUser: user, 501 528 RepoInfo: f.RepoInfo(s, user), 529 + Forks: forks, 502 530 Branches: result.Branches, 503 531 }) 504 532 case http.MethodPost: 505 - isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 506 533 title := r.FormValue("title") 507 534 body := r.FormValue("body") 508 535 targetBranch := r.FormValue("targetBranch") 536 + fromFork := r.FormValue("fromFork") 509 537 sourceBranch := r.FormValue("sourceBranch") 510 538 patch := r.FormValue("patch") 511 539 512 - isBranchBased := isPushAllowed && (sourceBranch != "") 540 + isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 541 + isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 513 542 isPatchBased := patch != "" 543 + isForkBased := fromFork != "" && sourceBranch != "" 514 544 515 - if !isBranchBased && !isPatchBased { 545 + if !isBranchBased && !isPatchBased && !isForkBased { 516 546 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 517 547 return 518 548 } ··· 527 557 return 528 558 } 529 559 530 - // TODO: check if knot has this capability 531 - var sourceRev string 532 - var pullSource *db.PullSource 533 - var recordPullSource *tangled.RepoPull_Source 534 560 if isBranchBased { 535 - pullSource = &db.PullSource{ 536 - Branch: sourceBranch, 537 - } 538 - recordPullSource = &tangled.RepoPull_Source{ 539 - Branch: sourceBranch, 540 - } 541 - // generate a patch using /compare 542 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 543 - if err != nil { 544 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 545 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 546 - return 547 - } 561 + s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch) 562 + } else if isPatchBased { 563 + s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch) 564 + } else if isForkBased { 565 + s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch) 566 + } 567 + return 568 + } 569 + } 548 570 549 - resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 550 - switch resp.StatusCode { 551 - case 404: 552 - case 400: 553 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 554 - } 571 + func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 572 + pullSource := &db.PullSource{ 573 + Branch: sourceBranch, 574 + } 575 + recordPullSource := &tangled.RepoPull_Source{ 576 + Branch: sourceBranch, 577 + } 578 + 579 + // Generate a patch using /compare 580 + ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 581 + if err != nil { 582 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 583 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 584 + return 585 + } 555 586 556 - respBody, err := io.ReadAll(resp.Body) 557 - if err != nil { 558 - log.Println("failed to compare across branches") 559 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 560 - } 561 - defer resp.Body.Close() 587 + resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 588 + switch resp.StatusCode { 589 + case 404: 590 + case 400: 591 + s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 592 + return 593 + } 562 594 563 - var diffTreeResponse types.RepoDiffTreeResponse 564 - err = json.Unmarshal(respBody, &diffTreeResponse) 565 - if err != nil { 566 - log.Println("failed to unmarshal diff tree response", err) 567 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 568 - } 595 + respBody, err := io.ReadAll(resp.Body) 596 + if err != nil { 597 + log.Println("failed to compare across branches") 598 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 599 + return 600 + } 601 + defer resp.Body.Close() 569 602 570 - sourceRev = diffTreeResponse.DiffTree.Rev2 571 - patch = diffTreeResponse.DiffTree.Patch 572 - } 603 + var diffTreeResponse types.RepoDiffTreeResponse 604 + err = json.Unmarshal(respBody, &diffTreeResponse) 605 + if err != nil { 606 + log.Println("failed to unmarshal diff tree response", err) 607 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 608 + return 609 + } 573 610 574 - // Validate patch format 575 - if !isPatchValid(patch) { 576 - s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 577 - return 578 - } 611 + sourceRev := diffTreeResponse.DiffTree.Rev2 612 + patch := diffTreeResponse.DiffTree.Patch 579 613 580 - tx, err := s.db.BeginTx(r.Context(), nil) 581 - if err != nil { 582 - log.Println("failed to start tx") 583 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 584 - return 585 - } 586 - defer tx.Rollback() 614 + if !isPatchValid(patch) { 615 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 616 + return 617 + } 587 618 588 - rkey := s.TID() 589 - initialSubmission := db.PullSubmission{ 590 - Patch: patch, 591 - SourceRev: sourceRev, 592 - } 593 - err = db.NewPull(tx, &db.Pull{ 594 - Title: title, 595 - Body: body, 596 - TargetBranch: targetBranch, 597 - OwnerDid: user.Did, 598 - RepoAt: f.RepoAt, 599 - Rkey: rkey, 600 - Submissions: []*db.PullSubmission{ 601 - &initialSubmission, 602 - }, 603 - PullSource: pullSource, 604 - }) 605 - if err != nil { 606 - log.Println("failed to create pull request", err) 607 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 608 - return 609 - } 610 - client, _ := s.auth.AuthorizedClient(r) 611 - pullId, err := db.NextPullId(s.db, f.RepoAt) 612 - if err != nil { 613 - log.Println("failed to get pull id", err) 614 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 615 - return 616 - } 619 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 620 + } 617 621 618 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 - Collection: tangled.RepoPullNSID, 620 - Repo: user.Did, 621 - Rkey: rkey, 622 - Record: &lexutil.LexiconTypeDecoder{ 623 - Val: &tangled.RepoPull{ 624 - Title: title, 625 - PullId: int64(pullId), 626 - TargetRepo: string(f.RepoAt), 627 - TargetBranch: targetBranch, 628 - Patch: patch, 629 - Source: recordPullSource, 630 - }, 631 - }, 632 - }) 622 + func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 623 + if !isPatchValid(patch) { 624 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 625 + return 626 + } 633 627 634 - err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 635 - if err != nil { 636 - log.Println("failed to get pull id", err) 637 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 638 - return 639 - } 628 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 629 + } 640 630 641 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 631 + func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 632 + fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 633 + if errors.Is(err, sql.ErrNoRows) { 634 + s.pages.Notice(w, "pull", "No such fork.") 635 + return 636 + } else if err != nil { 637 + log.Println("failed to fetch fork:", err) 638 + s.pages.Notice(w, "pull", "Failed to fetch fork.") 642 639 return 643 640 } 641 + 642 + secret, err := db.GetRegistrationKey(s.db, fork.Knot) 643 + if err != nil { 644 + log.Println("failed to fetch registration key:", err) 645 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 646 + return 647 + } 648 + 649 + sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 650 + if err != nil { 651 + log.Println("failed to create signed client:", err) 652 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 653 + return 654 + } 655 + 656 + us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 657 + if err != nil { 658 + log.Println("failed to create unsigned client:", err) 659 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 660 + return 661 + } 662 + 663 + resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 664 + if err != nil { 665 + log.Println("failed to create hidden ref:", err, resp.StatusCode) 666 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 667 + return 668 + } 669 + 670 + switch resp.StatusCode { 671 + case 404: 672 + case 400: 673 + s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 674 + return 675 + } 676 + 677 + hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 678 + // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 679 + // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 680 + // hiddenRef: hidden/feature-1/main (on repo-fork) 681 + // targetBranch: main (on repo-1) 682 + // sourceBranch: feature-1 (on repo-fork) 683 + diffResp, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 684 + if err != nil { 685 + log.Println("failed to compare across branches", err) 686 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 687 + return 688 + } 689 + 690 + respBody, err := io.ReadAll(diffResp.Body) 691 + if err != nil { 692 + log.Println("failed to read response body", err) 693 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 694 + return 695 + } 696 + 697 + defer resp.Body.Close() 698 + 699 + var diffTreeResponse types.RepoDiffTreeResponse 700 + err = json.Unmarshal(respBody, &diffTreeResponse) 701 + if err != nil { 702 + log.Println("failed to unmarshal diff tree response", err) 703 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 704 + return 705 + } 706 + 707 + sourceRev := diffTreeResponse.DiffTree.Rev2 708 + patch := diffTreeResponse.DiffTree.Patch 709 + 710 + if !isPatchValid(patch) { 711 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 712 + return 713 + } 714 + 715 + forkAtUri, err := syntax.ParseATURI(fork.AtUri) 716 + if err != nil { 717 + log.Println("failed to parse fork AT URI", err) 718 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 719 + return 720 + } 721 + 722 + s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 723 + Branch: sourceBranch, 724 + Repo: &forkAtUri, 725 + }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) 726 + } 727 + 728 + func (s *State) createPullRequest(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch, sourceRev string, pullSource *db.PullSource, recordPullSource *tangled.RepoPull_Source) { 729 + tx, err := s.db.BeginTx(r.Context(), nil) 730 + if err != nil { 731 + log.Println("failed to start tx") 732 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 733 + return 734 + } 735 + defer tx.Rollback() 736 + 737 + rkey := s.TID() 738 + initialSubmission := db.PullSubmission{ 739 + Patch: patch, 740 + SourceRev: sourceRev, 741 + } 742 + err = db.NewPull(tx, &db.Pull{ 743 + Title: title, 744 + Body: body, 745 + TargetBranch: targetBranch, 746 + OwnerDid: user.Did, 747 + RepoAt: f.RepoAt, 748 + Rkey: rkey, 749 + Submissions: []*db.PullSubmission{ 750 + &initialSubmission, 751 + }, 752 + PullSource: pullSource, 753 + }) 754 + if err != nil { 755 + log.Println("failed to create pull request", err) 756 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 757 + return 758 + } 759 + client, _ := s.auth.AuthorizedClient(r) 760 + pullId, err := db.NextPullId(s.db, f.RepoAt) 761 + if err != nil { 762 + log.Println("failed to get pull id", err) 763 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 764 + return 765 + } 766 + 767 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 768 + Collection: tangled.RepoPullNSID, 769 + Repo: user.Did, 770 + Rkey: rkey, 771 + Record: &lexutil.LexiconTypeDecoder{ 772 + Val: &tangled.RepoPull{ 773 + Title: title, 774 + PullId: int64(pullId), 775 + TargetRepo: string(f.RepoAt), 776 + TargetBranch: targetBranch, 777 + Patch: patch, 778 + Source: recordPullSource, 779 + }, 780 + }, 781 + }) 782 + 783 + err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 784 + if err != nil { 785 + log.Println("failed to get pull id", err) 786 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 787 + return 788 + } 789 + 790 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 644 791 } 645 792 646 793 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { ··· 723 870 var sourceRev string 724 871 var recordPullSource *tangled.RepoPull_Source 725 872 726 - // this pull is a branch based pull 873 + var ownerDid, repoName, knotName string 874 + var isSameRepo bool = pull.IsSameRepoBranch() 875 + sourceBranch := pull.PullSource.Branch 876 + targetBranch := pull.TargetBranch 877 + recordPullSource = &tangled.RepoPull_Source{ 878 + Branch: sourceBranch, 879 + } 880 + 727 881 isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 728 - if pull.IsSameRepoBranch() && isPushAllowed { 729 - sourceBranch := pull.PullSource.Branch 730 - targetBranch := pull.TargetBranch 731 - recordPullSource = &tangled.RepoPull_Source{ 732 - Branch: sourceBranch, 882 + if isSameRepo && isPushAllowed { 883 + ownerDid = f.OwnerDid() 884 + repoName = f.RepoName 885 + knotName = f.Knot 886 + } else if !isSameRepo { 887 + sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.Repo.String()) 888 + if err != nil { 889 + log.Println("failed to get source repo", err) 890 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 891 + return 733 892 } 893 + ownerDid = sourceRepo.Did 894 + repoName = sourceRepo.Name 895 + knotName = sourceRepo.Knot 896 + } 897 + 898 + if sourceBranch != "" && knotName != "" { 734 899 // extract patch by performing compare 735 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 900 + ksClient, err := NewUnsignedClient(knotName, s.config.Dev) 901 + if err != nil { 902 + log.Printf("failed to create client for %s: %s", knotName, err) 903 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 904 + return 905 + } 906 + 907 + if !isSameRepo { 908 + secret, err := db.GetRegistrationKey(s.db, knotName) 909 + if err != nil { 910 + log.Printf("failed to get registration key for %s: %s", knotName, err) 911 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 912 + return 913 + } 914 + // update the hidden tracking branch to latest 915 + signedClient, err := NewSignedClient(knotName, secret, s.config.Dev) 916 + if err != nil { 917 + log.Printf("failed to create signed client for %s: %s", knotName, err) 918 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 919 + return 920 + } 921 + resp, err := signedClient.NewHiddenRef(ownerDid, repoName, sourceBranch, targetBranch) 922 + if err != nil || resp.StatusCode != http.StatusNoContent { 923 + log.Printf("failed to update tracking branch: %s", err) 924 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 925 + return 926 + } 927 + } 928 + 929 + var compareResp *http.Response 930 + if !isSameRepo { 931 + hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 932 + compareResp, err = ksClient.Compare(ownerDid, repoName, hiddenRef, sourceBranch) 933 + } else { 934 + compareResp, err = ksClient.Compare(ownerDid, repoName, targetBranch, sourceBranch) 935 + } 736 936 if err != nil { 737 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 937 + log.Printf("failed to compare branches: %s", err) 738 938 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 739 939 return 740 940 } 941 + defer compareResp.Body.Close() 741 942 742 - resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 743 - switch resp.StatusCode { 943 + switch compareResp.StatusCode { 744 944 case 404: 745 945 case 400: 746 946 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 947 + return 747 948 } 748 949 749 - respBody, err := io.ReadAll(resp.Body) 950 + respBody, err := io.ReadAll(compareResp.Body) 750 951 if err != nil { 751 952 log.Println("failed to compare across branches") 752 953 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 954 + return 753 955 } 754 - defer resp.Body.Close() 956 + defer compareResp.Body.Close() 755 957 756 958 var diffTreeResponse types.RepoDiffTreeResponse 757 959 err = json.Unmarshal(respBody, &diffTreeResponse) 758 960 if err != nil { 759 961 log.Println("failed to unmarshal diff tree response", err) 760 962 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 963 + return 761 964 } 762 965 763 966 sourceRev = diffTreeResponse.DiffTree.Rev2 ··· 779 982 return 780 983 } 781 984 782 - // Validate patch format 783 985 if !isPatchValid(patch) { 784 986 s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") 785 987 return
+1 -1
appview/state/signer.go
··· 370 370 Method = "GET" 371 371 ) 372 372 373 - endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, rev1, rev2) 373 + endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 374 374 375 375 req, err := us.newRequest(Method, endpoint, nil) 376 376 if err != nil {