···
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"
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"
37
+
repoResolver *reporesolver.RepoResolver
39
+
resolver *appview.Resolver
41
+
config *appview.Config
42
+
posthog posthog.Client
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}
50
+
func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
52
+
case http.MethodGet:
53
+
user := s.oauth.GetUser(r)
54
+
f, err := s.repoResolver.Resolve(r)
56
+
log.Println("failed to get repo and knot", err)
60
+
pull, ok := r.Context().Value("pull").(*db.Pull)
62
+
log.Println("failed to get pull")
63
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
67
+
// can be nil if this pull is not stacked
68
+
stack, _ := r.Context().Value("stack").(db.Stack)
70
+
roundNumberStr := chi.URLParam(r, "round")
71
+
roundNumber, err := strconv.Atoi(roundNumberStr)
73
+
roundNumber = pull.LastRoundNumber()
75
+
if roundNumber >= len(pull.Submissions) {
76
+
http.Error(w, "bad round id", http.StatusBadRequest)
77
+
log.Println("failed to parse round id", err)
81
+
mergeCheckResponse := s.mergeCheck(f, pull, stack)
82
+
resubmitResult := pages.Unknown
83
+
if user.Did == pull.OwnerDid {
84
+
resubmitResult = s.resubmitCheck(f, pull, stack)
87
+
s.pages.PullActionsFragment(w, pages.PullActionsParams{
89
+
RepoInfo: f.RepoInfo(user),
91
+
RoundNumber: roundNumber,
92
+
MergeCheck: mergeCheckResponse,
93
+
ResubmitCheck: resubmitResult,
100
+
func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
101
+
user := s.oauth.GetUser(r)
102
+
f, err := s.repoResolver.Resolve(r)
104
+
log.Println("failed to get repo and knot", err)
108
+
pull, ok := r.Context().Value("pull").(*db.Pull)
110
+
log.Println("failed to get pull")
111
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
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)
120
+
for _, submission := range pull.Submissions {
121
+
totalIdents += len(submission.Comments)
124
+
identsToResolve := make([]string, totalIdents)
127
+
identsToResolve[0] = pull.OwnerDid
129
+
for _, submission := range pull.Submissions {
130
+
for _, comment := range submission.Comments {
131
+
identsToResolve[idx] = comment.OwnerDid
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())
142
+
didHandleMap[identity.DID.String()] = identity.DID.String()
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)
152
+
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
153
+
LoggedInUser: user,
154
+
RepoInfo: f.RepoInfo(user),
155
+
DidHandleMap: didHandleMap,
158
+
AbandonedPulls: abandonedPulls,
159
+
MergeCheck: mergeCheckResponse,
160
+
ResubmitCheck: resubmitResult,
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{}
169
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
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",
177
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
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",
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()
195
+
resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch)
197
+
log.Println("failed to check for mergeability:", err)
198
+
return types.MergeCheckResponse{
199
+
Error: "failed to check merge status",
202
+
switch resp.StatusCode {
204
+
return types.MergeCheckResponse{
205
+
Error: "failed to check merge status: this knot does not support PRs",
208
+
return types.MergeCheckResponse{
209
+
Error: "failed to check merge status: does this knot support PRs?",
213
+
respBody, err := io.ReadAll(resp.Body)
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",
220
+
defer resp.Body.Close()
222
+
var mergeCheckResponse types.MergeCheckResponse
223
+
err = json.Unmarshal(respBody, &mergeCheckResponse)
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",
231
+
return mergeCheckResponse
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
239
+
var knot, ownerDid, repoName string
241
+
if pull.PullSource.RepoAt != nil {
242
+
// fork-based pulls
243
+
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
245
+
log.Println("failed to get source repo", err)
246
+
return pages.Unknown
249
+
knot = sourceRepo.Knot
250
+
ownerDid = sourceRepo.Did
251
+
repoName = sourceRepo.Name
253
+
// pulls within the same repo
255
+
ownerDid = f.OwnerDid()
256
+
repoName = f.RepoName
259
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
261
+
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
262
+
return pages.Unknown
265
+
result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
267
+
log.Println("failed to reach knotserver", err)
268
+
return pages.Unknown
271
+
latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev
273
+
if pull.IsStacked() && stack != nil {
275
+
latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev
278
+
log.Println(latestSourceRev, result.Branch.Hash)
280
+
if latestSourceRev != result.Branch.Hash {
281
+
return pages.ShouldResubmit
284
+
return pages.ShouldNotResubmit
287
+
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
288
+
user := s.oauth.GetUser(r)
289
+
f, err := s.repoResolver.Resolve(r)
291
+
log.Println("failed to get repo and knot", err)
295
+
pull, ok := r.Context().Value("pull").(*db.Pull)
297
+
log.Println("failed to get pull")
298
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
302
+
stack, _ := r.Context().Value("stack").(db.Stack)
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)
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())
319
+
didHandleMap[identity.DID.String()] = identity.DID.String()
323
+
patch := pull.Submissions[roundIdInt].Patch
324
+
diff := patchutil.AsNiceDiff(patch, pull.TargetBranch)
326
+
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
327
+
LoggedInUser: user,
328
+
DidHandleMap: didHandleMap,
329
+
RepoInfo: f.RepoInfo(user),
333
+
Submission: pull.Submissions[roundIdInt],
339
+
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
340
+
user := s.oauth.GetUser(r)
342
+
f, err := s.repoResolver.Resolve(r)
344
+
log.Println("failed to get repo and knot", err)
348
+
pull, ok := r.Context().Value("pull").(*db.Pull)
350
+
log.Println("failed to get pull")
351
+
s.pages.Notice(w, "pull-error", "Failed to get pull.")
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)
363
+
if roundIdInt == 0 {
364
+
http.Error(w, "bad round id", http.StatusBadRequest)
365
+
log.Println("cannot interdiff initial submission")
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())
376
+
didHandleMap[identity.DID.String()] = identity.DID.String()
380
+
currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch)
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.")
387
+
previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch)
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.")
394
+
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
396
+
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
397
+
LoggedInUser: s.oauth.GetUser(r),
398
+
RepoInfo: f.RepoInfo(user),
401
+
DidHandleMap: didHandleMap,
402
+
Interdiff: interdiff,
407
+
func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
408
+
pull, ok := r.Context().Value("pull").(*db.Pull)
410
+
log.Println("failed to get pull")
411
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
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)
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())
430
+
didHandleMap[identity.DID.String()] = identity.DID.String()
434
+
w.Header().Set("Content-Type", "text/plain")
435
+
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
438
+
func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
439
+
user := s.oauth.GetUser(r)
440
+
params := r.URL.Query()
442
+
state := db.PullOpen
443
+
switch params.Get("state") {
445
+
state = db.PullClosed
447
+
state = db.PullMerged
450
+
f, err := s.repoResolver.Resolve(r)
452
+
log.Println("failed to get repo and knot", err)
456
+
pulls, err := db.GetPulls(
458
+
db.FilterEq("repo_at", f.RepoAt),
459
+
db.FilterEq("state", state),
462
+
log.Println("failed to get pulls", err)
463
+
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
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())
473
+
log.Printf("failed to get repo by at uri: %v", err)
476
+
p.PullSource.Repo = pullSourceRepo
482
+
identsToResolve := make([]string, len(pulls))
483
+
for i, pull := range pulls {
484
+
identsToResolve[i] = pull.OwnerDid
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())
492
+
didHandleMap[identity.DID.String()] = identity.DID.String()
496
+
s.pages.RepoPulls(w, pages.RepoPullsParams{
497
+
LoggedInUser: s.oauth.GetUser(r),
498
+
RepoInfo: f.RepoInfo(user),
500
+
DidHandleMap: didHandleMap,
501
+
FilteringBy: state,
506
+
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
507
+
user := s.oauth.GetUser(r)
508
+
f, err := s.repoResolver.Resolve(r)
510
+
log.Println("failed to get repo and knot", err)
514
+
pull, ok := r.Context().Value("pull").(*db.Pull)
516
+
log.Println("failed to get pull")
517
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
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)
530
+
case http.MethodGet:
531
+
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
532
+
LoggedInUser: user,
533
+
RepoInfo: f.RepoInfo(user),
535
+
RoundNumber: roundNumber,
538
+
case http.MethodPost:
539
+
body := r.FormValue("body")
541
+
s.pages.Notice(w, "pull", "Comment body is required")
545
+
// Start a transaction
546
+
tx, err := s.db.BeginTx(r.Context(), nil)
548
+
log.Println("failed to start transaction", err)
549
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
552
+
defer tx.Rollback()
554
+
createdAt := time.Now().Format(time.RFC3339)
555
+
ownerDid := user.Did
557
+
pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId)
559
+
log.Println("failed to get pull at", err)
560
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
564
+
atUri := f.RepoAt.String()
565
+
client, err := s.oauth.AuthorizedClient(r)
567
+
log.Println("failed to get authorized client", err)
568
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
571
+
atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
572
+
Collection: tangled.RepoPullCommentNSID,
574
+
Rkey: appview.TID(),
575
+
Record: &lexutil.LexiconTypeDecoder{
576
+
Val: &tangled.RepoPullComment{
578
+
Pull: string(pullAt),
581
+
CreatedAt: createdAt,
586
+
log.Println("failed to create pull comment", err)
587
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
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,
597
+
CommentAt: atResp.Uri,
598
+
SubmissionId: pull.Submissions[roundNumber].ID,
601
+
log.Println("failed to create pull comment", err)
602
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
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.")
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},
620
+
log.Println("failed to enqueue posthog event:", err)
624
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
629
+
func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) {
630
+
user := s.oauth.GetUser(r)
631
+
f, err := s.repoResolver.Resolve(r)
633
+
log.Println("failed to get repo and knot", err)
638
+
case http.MethodGet:
639
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
641
+
log.Printf("failed to create unsigned client for %s", f.Knot)
642
+
s.pages.Error503(w)
646
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
648
+
log.Println("failed to fetch branches", err)
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")
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"),
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")
677
+
if targetBranch == "" {
678
+
s.pages.Notice(w, "pull", "Target branch is required.")
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"
689
+
if isPatchBased && !patchutil.IsFormatPatch(patch) {
691
+
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
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.")
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.")
708
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
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.")
715
+
caps, err := us.Capabilities()
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.")
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.")
727
+
// Handle the PR creation based on the type
729
+
if !caps.PullRequests.BranchSubmissions {
730
+
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
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?")
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.")
745
+
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked)
751
+
func (s *Pulls) handleBranchBasedPull(
752
+
w http.ResponseWriter,
754
+
f *reporesolver.ResolvedRepo,
759
+
sourceBranch string,
762
+
pullSource := &db.PullSource{
763
+
Branch: sourceBranch,
765
+
recordPullSource := &tangled.RepoPull_Source{
766
+
Branch: sourceBranch,
769
+
// Generate a patch using /compare
770
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
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.")
777
+
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
779
+
log.Println("failed to compare", err)
780
+
s.pages.Notice(w, "pull", err.Error())
784
+
sourceRev := comparison.Rev2
785
+
patch := comparison.Patch
787
+
if !patchutil.IsPatchValid(patch) {
788
+
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
792
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
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.")
801
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked)
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.")
809
+
} else if err != nil {
810
+
log.Println("failed to fetch fork:", err)
811
+
s.pages.Notice(w, "pull", "Failed to fetch fork.")
815
+
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
817
+
log.Println("failed to fetch registration key:", err)
818
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
822
+
sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
824
+
log.Println("failed to create signed client:", err)
825
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
829
+
us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
831
+
log.Println("failed to create unsigned client:", err)
832
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
836
+
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
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.")
843
+
switch resp.StatusCode {
846
+
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
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)
858
+
log.Println("failed to compare across branches", err)
859
+
s.pages.Notice(w, "pull", err.Error())
863
+
sourceRev := comparison.Rev2
864
+
patch := comparison.Patch
866
+
if !patchutil.IsPatchValid(patch) {
867
+
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
871
+
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
873
+
log.Println("failed to parse fork AT URI", err)
874
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
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)
884
+
func (s *Pulls) createPullRequest(
885
+
w http.ResponseWriter,
887
+
f *reporesolver.ResolvedRepo,
889
+
title, body, targetBranch string,
892
+
pullSource *db.PullSource,
893
+
recordPullSource *tangled.RepoPull_Source,
897
+
// creates a series of PRs, each linking to the previous, identified by jj's change-id
898
+
s.createStackedPulLRequest(
911
+
client, err := s.oauth.AuthorizedClient(r)
913
+
log.Println("failed to get authorized client", err)
914
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
918
+
tx, err := s.db.BeginTx(r.Context(), nil)
920
+
log.Println("failed to start tx")
921
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
924
+
defer tx.Rollback()
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.
929
+
formatPatches, err := patchutil.ExtractPatches(patch)
931
+
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
934
+
if len(formatPatches) == 0 {
935
+
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
939
+
title = formatPatches[0].Title
940
+
body = formatPatches[0].Body
943
+
rkey := appview.TID()
944
+
initialSubmission := db.PullSubmission{
946
+
SourceRev: sourceRev,
948
+
err = db.NewPull(tx, &db.Pull{
951
+
TargetBranch: targetBranch,
952
+
OwnerDid: user.Did,
955
+
Submissions: []*db.PullSubmission{
956
+
&initialSubmission,
958
+
PullSource: pullSource,
961
+
log.Println("failed to create pull request", err)
962
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
965
+
pullId, err := db.NextPullId(tx, f.RepoAt)
967
+
log.Println("failed to get pull id", err)
968
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
972
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
973
+
Collection: tangled.RepoPullNSID,
976
+
Record: &lexutil.LexiconTypeDecoder{
977
+
Val: &tangled.RepoPull{
979
+
PullId: int64(pullId),
980
+
TargetRepo: string(f.RepoAt),
981
+
TargetBranch: targetBranch,
983
+
Source: recordPullSource,
988
+
log.Println("failed to create pull request", err)
989
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
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.")
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},
1006
+
log.Println("failed to enqueue posthog event:", err)
1010
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1013
+
func (s *Pulls) createStackedPulLRequest(
1014
+
w http.ResponseWriter,
1016
+
f *reporesolver.ResolvedRepo,
1018
+
targetBranch string,
1021
+
pullSource *db.PullSource,
1023
+
// run some necessary checks for stacked-prs first
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.")
1032
+
formatPatches, err := patchutil.ExtractPatches(patch)
1034
+
log.Println("failed to extract patches", err)
1035
+
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
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.")
1046
+
// build a stack out of this patch
1047
+
stackId := uuid.New()
1048
+
stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1050
+
log.Println("failed to create stack", err)
1051
+
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
1055
+
client, err := s.oauth.AuthorizedClient(r)
1057
+
log.Println("failed to get authorized client", err)
1058
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
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,
1070
+
Value: &lexutil.LexiconTypeDecoder{
1075
+
writes = append(writes, &write)
1077
+
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
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.")
1087
+
// create all pulls at once
1088
+
tx, err := s.db.BeginTx(r.Context(), nil)
1090
+
log.Println("failed to start tx")
1091
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1094
+
defer tx.Rollback()
1096
+
for _, p := range stack {
1097
+
err = db.NewPull(tx, p)
1099
+
log.Println("failed to create pull request", err)
1100
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
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.")
1111
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1114
+
func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) {
1115
+
_, err := s.repoResolver.Resolve(r)
1117
+
log.Println("failed to get repo and knot", err)
1121
+
patch := r.FormValue("patch")
1123
+
s.pages.Notice(w, "patch-error", "Patch is required.")
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.")
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.")
1135
+
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
1139
+
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1140
+
user := s.oauth.GetUser(r)
1141
+
f, err := s.repoResolver.Resolve(r)
1143
+
log.Println("failed to get repo and knot", err)
1147
+
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1148
+
RepoInfo: f.RepoInfo(user),
1152
+
func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1153
+
user := s.oauth.GetUser(r)
1154
+
f, err := s.repoResolver.Resolve(r)
1156
+
log.Println("failed to get repo and knot", err)
1160
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1162
+
log.Printf("failed to create unsigned client for %s", f.Knot)
1163
+
s.pages.Error503(w)
1167
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1169
+
log.Println("failed to reach knotserver", err)
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)
1178
+
withoutDefault := []types.Branch{}
1179
+
for _, b := range branches {
1183
+
withoutDefault = append(withoutDefault, b)
1186
+
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1187
+
RepoInfo: f.RepoInfo(user),
1188
+
Branches: withoutDefault,
1192
+
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1193
+
user := s.oauth.GetUser(r)
1194
+
f, err := s.repoResolver.Resolve(r)
1196
+
log.Println("failed to get repo and knot", err)
1200
+
forks, err := db.GetForksByDid(s.db, user.Did)
1202
+
log.Println("failed to get forks", err)
1206
+
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1207
+
RepoInfo: f.RepoInfo(user),
1209
+
Selected: r.URL.Query().Get("fork"),
1213
+
func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1214
+
user := s.oauth.GetUser(r)
1216
+
f, err := s.repoResolver.Resolve(r)
1218
+
log.Println("failed to get repo and knot", err)
1222
+
forkVal := r.URL.Query().Get("fork")
1225
+
repo, err := db.GetRepo(s.db, user.Did, forkVal)
1227
+
log.Println("failed to get repo", user.Did, forkVal)
1231
+
sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1233
+
log.Printf("failed to create unsigned client for %s", repo.Knot)
1234
+
s.pages.Error503(w)
1238
+
sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name)
1240
+
log.Println("failed to reach knotserver for source branches", err)
1244
+
targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1246
+
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1247
+
s.pages.Error503(w)
1251
+
targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
1253
+
log.Println("failed to reach knotserver for target branches", err)
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)
1262
+
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1263
+
RepoInfo: f.RepoInfo(user),
1264
+
SourceBranches: sourceBranches,
1265
+
TargetBranches: targetResult.Branches,
1269
+
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1270
+
user := s.oauth.GetUser(r)
1271
+
f, err := s.repoResolver.Resolve(r)
1273
+
log.Println("failed to get repo and knot", err)
1277
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1279
+
log.Println("failed to get pull")
1280
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1285
+
case http.MethodGet:
1286
+
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1287
+
RepoInfo: f.RepoInfo(user),
1291
+
case http.MethodPost:
1292
+
if pull.IsPatchBased() {
1293
+
s.resubmitPatch(w, r)
1295
+
} else if pull.IsBranchBased() {
1296
+
s.resubmitBranch(w, r)
1298
+
} else if pull.IsForkBased() {
1299
+
s.resubmitFork(w, r)
1305
+
func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1306
+
user := s.oauth.GetUser(r)
1308
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1310
+
log.Println("failed to get pull")
1311
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1315
+
f, err := s.repoResolver.Resolve(r)
1317
+
log.Println("failed to get repo and knot", err)
1321
+
if user.Did != pull.OwnerDid {
1322
+
log.Println("unauthorized user")
1323
+
w.WriteHeader(http.StatusUnauthorized)
1327
+
patch := r.FormValue("patch")
1329
+
s.resubmitPullHelper(w, r, f, user, pull, patch, "")
1332
+
func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1333
+
user := s.oauth.GetUser(r)
1335
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1337
+
log.Println("failed to get pull")
1338
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1342
+
f, err := s.repoResolver.Resolve(r)
1344
+
log.Println("failed to get repo and knot", err)
1348
+
if user.Did != pull.OwnerDid {
1349
+
log.Println("unauthorized user")
1350
+
w.WriteHeader(http.StatusUnauthorized)
1354
+
if !f.RepoInfo(user).Roles.IsPushAllowed() {
1355
+
log.Println("unauthorized user")
1356
+
w.WriteHeader(http.StatusUnauthorized)
1360
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
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.")
1367
+
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
1369
+
log.Printf("compare request failed: %s", err)
1370
+
s.pages.Notice(w, "resubmit-error", err.Error())
1374
+
sourceRev := comparison.Rev2
1375
+
patch := comparison.Patch
1377
+
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1380
+
func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
1381
+
user := s.oauth.GetUser(r)
1383
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1385
+
log.Println("failed to get pull")
1386
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1390
+
f, err := s.repoResolver.Resolve(r)
1392
+
log.Println("failed to get repo and knot", err)
1396
+
if user.Did != pull.OwnerDid {
1397
+
log.Println("unauthorized user")
1398
+
w.WriteHeader(http.StatusUnauthorized)
1402
+
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
1404
+
log.Println("failed to get source repo", err)
1405
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1409
+
// extract patch by performing compare
1410
+
ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
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.")
1417
+
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
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.")
1424
+
// update the hidden tracking branch to latest
1425
+
signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
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.")
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.")
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)
1442
+
log.Printf("failed to compare branches: %s", err)
1443
+
s.pages.Notice(w, "resubmit-error", err.Error())
1447
+
sourceRev := comparison.Rev2
1448
+
patch := comparison.Patch
1450
+
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1453
+
// validate a resubmission against a pull request
1454
+
func validateResubmittedPatch(pull *db.Pull, patch string) error {
1456
+
return fmt.Errorf("Patch is empty.")
1459
+
if patch == pull.LatestPatch() {
1460
+
return fmt.Errorf("Patch is identical to previous submission.")
1463
+
if !patchutil.IsPatchValid(patch) {
1464
+
return fmt.Errorf("Invalid patch format. Please provide a valid diff.")
1470
+
func (s *Pulls) resubmitPullHelper(
1471
+
w http.ResponseWriter,
1473
+
f *reporesolver.ResolvedRepo,
1479
+
if pull.IsStacked() {
1480
+
log.Println("resubmitting stacked PR")
1481
+
s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1485
+
if err := validateResubmittedPatch(pull, patch); err != nil {
1486
+
s.pages.Notice(w, "resubmit-error", err.Error())
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.")
1498
+
tx, err := s.db.BeginTx(r.Context(), nil)
1500
+
log.Println("failed to start tx")
1501
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1504
+
defer tx.Rollback()
1506
+
err = db.ResubmitPull(tx, pull, patch, sourceRev)
1508
+
log.Println("failed to create pull request", err)
1509
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1512
+
client, err := s.oauth.AuthorizedClient(r)
1514
+
log.Println("failed to authorize client")
1515
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1519
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1521
+
// failed to get record
1522
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1526
+
var recordPullSource *tangled.RepoPull_Source
1527
+
if pull.IsBranchBased() {
1528
+
recordPullSource = &tangled.RepoPull_Source{
1529
+
Branch: pull.PullSource.Branch,
1532
+
if pull.IsForkBased() {
1533
+
repoAt := pull.PullSource.RepoAt.String()
1534
+
recordPullSource = &tangled.RepoPull_Source{
1535
+
Branch: pull.PullSource.Branch,
1540
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1541
+
Collection: tangled.RepoPullNSID,
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,
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.")
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.")
1568
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1572
+
func (s *Pulls) resubmitStackedPullHelper(
1573
+
w http.ResponseWriter,
1575
+
f *reporesolver.ResolvedRepo,
1581
+
targetBranch := pull.TargetBranch
1583
+
origStack, _ := r.Context().Value("stack").(db.Stack)
1584
+
newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
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.")
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
1597
+
for _, p := range newStack {
1598
+
newById[p.ChangeId] = p
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
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{})
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
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
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()))
1634
+
origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr)
1635
+
newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr)
1637
+
patchutil.SortPatch(newFiles)
1638
+
patchutil.SortPatch(origFiles)
1640
+
// text content of patch may be identical, but a jj rebase might have forwarded it
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{}{}
1648
+
updated[op.ChangeId] = struct{}{}
1653
+
tx, err := s.db.Begin()
1655
+
log.Println("failed to start transaction", err)
1656
+
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1659
+
defer tx.Rollback()
1661
+
// pds updates to make
1662
+
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1664
+
// deleted pulls are marked as deleted in the DB
1665
+
for _, p := range deletions {
1666
+
err := db.DeletePull(tx, p.RepoAt, p.PullId)
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.")
1672
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1673
+
RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
1674
+
Collection: tangled.RepoPullNSID,
1680
+
// new pulls are created
1681
+
for _, p := range additions {
1682
+
err := db.NewPull(tx, p)
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.")
1689
+
record := p.AsRecord()
1690
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1691
+
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1692
+
Collection: tangled.RepoPullNSID,
1694
+
Value: &lexutil.LexiconTypeDecoder{
1701
+
// updated pulls are, well, updated; to start a new round
1702
+
for id := range updated {
1703
+
op, _ := origById[id]
1704
+
np, _ := newById[id]
1706
+
submission := np.Submissions[np.LastRoundNumber()]
1708
+
// resubmit the old pull
1709
+
err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev)
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.")
1717
+
record := op.AsRecord()
1718
+
record.Patch = submission.Patch
1720
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1721
+
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1722
+
Collection: tangled.RepoPullNSID,
1724
+
Value: &lexutil.LexiconTypeDecoder{
1731
+
// unchanged pulls are edited without starting a new round
1733
+
// update source-revs & patches without advancing rounds
1734
+
for changeId := range unchanged {
1735
+
op, _ := origById[changeId]
1736
+
np, _ := newById[changeId]
1738
+
origSubmission := op.Submissions[op.LastRoundNumber()]
1739
+
newSubmission := np.Submissions[np.LastRoundNumber()]
1741
+
log.Println("moving unchanged change id : ", changeId)
1743
+
err := db.UpdatePull(
1745
+
newSubmission.Patch,
1746
+
newSubmission.SourceRev,
1747
+
db.FilterEq("id", origSubmission.ID),
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.")
1756
+
record := op.AsRecord()
1757
+
record.Patch = newSubmission.Patch
1759
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1760
+
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1761
+
Collection: tangled.RepoPullNSID,
1763
+
Value: &lexutil.LexiconTypeDecoder{
1770
+
// update parent-change-id relations for the entire stack
1771
+
for _, p := range newStack {
1772
+
err := db.SetPullParentChangeId(
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),
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.")
1790
+
log.Println("failed to resubmit pull", err)
1791
+
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1795
+
client, err := s.oauth.AuthorizedClient(r)
1797
+
log.Println("failed to authorize client")
1798
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1802
+
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
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.")
1812
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1816
+
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
1817
+
f, err := s.repoResolver.Resolve(r)
1819
+
log.Println("failed to resolve repo:", err)
1820
+
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1824
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1826
+
log.Println("failed to get pull")
1827
+
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1831
+
var pullsToMerge db.Stack
1832
+
pullsToMerge = append(pullsToMerge, pull)
1833
+
if pull.IsStacked() {
1834
+
stack, ok := r.Context().Value("stack").(db.Stack)
1836
+
log.Println("failed to get stack")
1837
+
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
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...)
1849
+
patch := pullsToMerge.CombinedPatch()
1851
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
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.")
1858
+
ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid)
1860
+
log.Printf("resolving identity: %s", err)
1861
+
w.WriteHeader(http.StatusNotFound)
1865
+
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
1867
+
log.Printf("failed to get primary email: %s", err)
1870
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
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.")
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)
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.")
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.")
1891
+
tx, err := s.db.Begin()
1893
+
log.Println("failed to start transcation", err)
1894
+
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1897
+
defer tx.Rollback()
1899
+
for _, p := range pullsToMerge {
1900
+
err := db.MergePull(tx, f.RepoAt, p.PullId)
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.")
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.")
1916
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
1919
+
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
1920
+
user := s.oauth.GetUser(r)
1922
+
f, err := s.repoResolver.Resolve(r)
1924
+
log.Println("malformed middleware")
1928
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1930
+
log.Println("failed to get pull")
1931
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
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.")
1946
+
// Start a transaction
1947
+
tx, err := s.db.BeginTx(r.Context(), nil)
1949
+
log.Println("failed to start transaction", err)
1950
+
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1953
+
defer tx.Rollback()
1955
+
var pullsToClose []*db.Pull
1956
+
pullsToClose = append(pullsToClose, pull)
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...)
1965
+
for _, p := range pullsToClose {
1966
+
// Close the pull in the database
1967
+
err = db.ClosePull(tx, f.RepoAt, p.PullId)
1969
+
log.Println("failed to close pull", err)
1970
+
s.pages.Notice(w, "pull-close", "Failed to close pull.")
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.")
1982
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1986
+
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
1987
+
user := s.oauth.GetUser(r)
1989
+
f, err := s.repoResolver.Resolve(r)
1991
+
log.Println("failed to resolve repo", err)
1992
+
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1996
+
pull, ok := r.Context().Value("pull").(*db.Pull)
1998
+
log.Println("failed to get pull")
1999
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
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.")
2014
+
// Start a transaction
2015
+
tx, err := s.db.BeginTx(r.Context(), nil)
2017
+
log.Println("failed to start transaction", err)
2018
+
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2021
+
defer tx.Rollback()
2023
+
var pullsToReopen []*db.Pull
2024
+
pullsToReopen = append(pullsToReopen, pull)
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...)
2033
+
for _, p := range pullsToReopen {
2034
+
// Close the pull in the database
2035
+
err = db.ReopenPull(tx, f.RepoAt, p.PullId)
2037
+
log.Println("failed to close pull", err)
2038
+
s.pages.Notice(w, "pull-close", "Failed to close pull.")
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.")
2050
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
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)
2057
+
return nil, fmt.Errorf("Failed to extract patches: %v", err)
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.")
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()
2072
+
return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.")
2077
+
rkey := appview.TID()
2079
+
initialSubmission := db.PullSubmission{
2081
+
SourceRev: fp.SHA,
2086
+
TargetBranch: targetBranch,
2087
+
OwnerDid: user.Did,
2090
+
Submissions: []*db.PullSubmission{
2091
+
&initialSubmission,
2093
+
PullSource: pullSource,
2094
+
Created: time.Now(),
2097
+
ChangeId: changeId,
2098
+
ParentChangeId: parentChangeId,
2101
+
stack = append(stack, &pull)
2103
+
parentChangeId = changeId