···
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"
36
-
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
38
-
case http.MethodGet:
39
-
user := s.oauth.GetUser(r)
40
-
f, err := s.repoResolver.Resolve(r)
42
-
log.Println("failed to get repo and knot", err)
46
-
pull, ok := r.Context().Value("pull").(*db.Pull)
48
-
log.Println("failed to get pull")
49
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
53
-
// can be nil if this pull is not stacked
54
-
stack, _ := r.Context().Value("stack").(db.Stack)
56
-
roundNumberStr := chi.URLParam(r, "round")
57
-
roundNumber, err := strconv.Atoi(roundNumberStr)
59
-
roundNumber = pull.LastRoundNumber()
61
-
if roundNumber >= len(pull.Submissions) {
62
-
http.Error(w, "bad round id", http.StatusBadRequest)
63
-
log.Println("failed to parse round id", err)
67
-
mergeCheckResponse := s.mergeCheck(f, pull, stack)
68
-
resubmitResult := pages.Unknown
69
-
if user.Did == pull.OwnerDid {
70
-
resubmitResult = s.resubmitCheck(f, pull, stack)
73
-
s.pages.PullActionsFragment(w, pages.PullActionsParams{
75
-
RepoInfo: f.RepoInfo(user),
77
-
RoundNumber: roundNumber,
78
-
MergeCheck: mergeCheckResponse,
79
-
ResubmitCheck: resubmitResult,
86
-
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
87
-
user := s.oauth.GetUser(r)
88
-
f, err := s.repoResolver.Resolve(r)
90
-
log.Println("failed to get repo and knot", err)
94
-
pull, ok := r.Context().Value("pull").(*db.Pull)
96
-
log.Println("failed to get pull")
97
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
101
-
// can be nil if this pull is not stacked
102
-
stack, _ := r.Context().Value("stack").(db.Stack)
103
-
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*db.Pull)
106
-
for _, submission := range pull.Submissions {
107
-
totalIdents += len(submission.Comments)
110
-
identsToResolve := make([]string, totalIdents)
113
-
identsToResolve[0] = pull.OwnerDid
115
-
for _, submission := range pull.Submissions {
116
-
for _, comment := range submission.Comments {
117
-
identsToResolve[idx] = comment.OwnerDid
122
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
123
-
didHandleMap := make(map[string]string)
124
-
for _, identity := range resolvedIds {
125
-
if !identity.Handle.IsInvalidHandle() {
126
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
128
-
didHandleMap[identity.DID.String()] = identity.DID.String()
132
-
mergeCheckResponse := s.mergeCheck(f, pull, stack)
133
-
resubmitResult := pages.Unknown
134
-
if user != nil && user.Did == pull.OwnerDid {
135
-
resubmitResult = s.resubmitCheck(f, pull, stack)
138
-
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
139
-
LoggedInUser: user,
140
-
RepoInfo: f.RepoInfo(user),
141
-
DidHandleMap: didHandleMap,
144
-
AbandonedPulls: abandonedPulls,
145
-
MergeCheck: mergeCheckResponse,
146
-
ResubmitCheck: resubmitResult,
150
-
func (s *State) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) types.MergeCheckResponse {
151
-
if pull.State == db.PullMerged {
152
-
return types.MergeCheckResponse{}
155
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
157
-
log.Printf("failed to get registration key: %v", err)
158
-
return types.MergeCheckResponse{
159
-
Error: "failed to check merge status: this knot is unregistered",
163
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
165
-
log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
166
-
return types.MergeCheckResponse{
167
-
Error: "failed to check merge status",
171
-
patch := pull.LatestPatch()
172
-
if pull.IsStacked() {
173
-
// combine patches of substack
174
-
subStack := stack.Below(pull)
175
-
// collect the portion of the stack that is mergeable
176
-
mergeable := subStack.Mergeable()
177
-
// combine each patch
178
-
patch = mergeable.CombinedPatch()
181
-
resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch)
183
-
log.Println("failed to check for mergeability:", err)
184
-
return types.MergeCheckResponse{
185
-
Error: "failed to check merge status",
188
-
switch resp.StatusCode {
190
-
return types.MergeCheckResponse{
191
-
Error: "failed to check merge status: this knot does not support PRs",
194
-
return types.MergeCheckResponse{
195
-
Error: "failed to check merge status: does this knot support PRs?",
199
-
respBody, err := io.ReadAll(resp.Body)
201
-
log.Println("failed to read merge check response body")
202
-
return types.MergeCheckResponse{
203
-
Error: "failed to check merge status: knot is not speaking the right language",
206
-
defer resp.Body.Close()
208
-
var mergeCheckResponse types.MergeCheckResponse
209
-
err = json.Unmarshal(respBody, &mergeCheckResponse)
211
-
log.Println("failed to unmarshal merge check response", err)
212
-
return types.MergeCheckResponse{
213
-
Error: "failed to check merge status: knot is not speaking the right language",
217
-
return mergeCheckResponse
220
-
func (s *State) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult {
221
-
if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil {
222
-
return pages.Unknown
225
-
var knot, ownerDid, repoName string
227
-
if pull.PullSource.RepoAt != nil {
228
-
// fork-based pulls
229
-
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
231
-
log.Println("failed to get source repo", err)
232
-
return pages.Unknown
235
-
knot = sourceRepo.Knot
236
-
ownerDid = sourceRepo.Did
237
-
repoName = sourceRepo.Name
239
-
// pulls within the same repo
241
-
ownerDid = f.OwnerDid()
242
-
repoName = f.RepoName
245
-
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
247
-
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
248
-
return pages.Unknown
251
-
result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
253
-
log.Println("failed to reach knotserver", err)
254
-
return pages.Unknown
257
-
latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev
259
-
if pull.IsStacked() && stack != nil {
261
-
latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev
264
-
log.Println(latestSourceRev, result.Branch.Hash)
266
-
if latestSourceRev != result.Branch.Hash {
267
-
return pages.ShouldResubmit
270
-
return pages.ShouldNotResubmit
273
-
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
274
-
user := s.oauth.GetUser(r)
275
-
f, err := s.repoResolver.Resolve(r)
277
-
log.Println("failed to get repo and knot", err)
281
-
pull, ok := r.Context().Value("pull").(*db.Pull)
283
-
log.Println("failed to get pull")
284
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
288
-
stack, _ := r.Context().Value("stack").(db.Stack)
290
-
roundId := chi.URLParam(r, "round")
291
-
roundIdInt, err := strconv.Atoi(roundId)
292
-
if err != nil || roundIdInt >= len(pull.Submissions) {
293
-
http.Error(w, "bad round id", http.StatusBadRequest)
294
-
log.Println("failed to parse round id", err)
298
-
identsToResolve := []string{pull.OwnerDid}
299
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
300
-
didHandleMap := make(map[string]string)
301
-
for _, identity := range resolvedIds {
302
-
if !identity.Handle.IsInvalidHandle() {
303
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
305
-
didHandleMap[identity.DID.String()] = identity.DID.String()
309
-
patch := pull.Submissions[roundIdInt].Patch
310
-
diff := patchutil.AsNiceDiff(patch, pull.TargetBranch)
312
-
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
313
-
LoggedInUser: user,
314
-
DidHandleMap: didHandleMap,
315
-
RepoInfo: f.RepoInfo(user),
319
-
Submission: pull.Submissions[roundIdInt],
325
-
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
326
-
user := s.oauth.GetUser(r)
328
-
f, err := s.repoResolver.Resolve(r)
330
-
log.Println("failed to get repo and knot", err)
334
-
pull, ok := r.Context().Value("pull").(*db.Pull)
336
-
log.Println("failed to get pull")
337
-
s.pages.Notice(w, "pull-error", "Failed to get pull.")
341
-
roundId := chi.URLParam(r, "round")
342
-
roundIdInt, err := strconv.Atoi(roundId)
343
-
if err != nil || roundIdInt >= len(pull.Submissions) {
344
-
http.Error(w, "bad round id", http.StatusBadRequest)
345
-
log.Println("failed to parse round id", err)
349
-
if roundIdInt == 0 {
350
-
http.Error(w, "bad round id", http.StatusBadRequest)
351
-
log.Println("cannot interdiff initial submission")
355
-
identsToResolve := []string{pull.OwnerDid}
356
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
357
-
didHandleMap := make(map[string]string)
358
-
for _, identity := range resolvedIds {
359
-
if !identity.Handle.IsInvalidHandle() {
360
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
362
-
didHandleMap[identity.DID.String()] = identity.DID.String()
366
-
currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch)
368
-
log.Println("failed to interdiff; current patch malformed")
369
-
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
373
-
previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch)
375
-
log.Println("failed to interdiff; previous patch malformed")
376
-
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
380
-
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
382
-
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
383
-
LoggedInUser: s.oauth.GetUser(r),
384
-
RepoInfo: f.RepoInfo(user),
387
-
DidHandleMap: didHandleMap,
388
-
Interdiff: interdiff,
393
-
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
394
-
pull, ok := r.Context().Value("pull").(*db.Pull)
396
-
log.Println("failed to get pull")
397
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
401
-
roundId := chi.URLParam(r, "round")
402
-
roundIdInt, err := strconv.Atoi(roundId)
403
-
if err != nil || roundIdInt >= len(pull.Submissions) {
404
-
http.Error(w, "bad round id", http.StatusBadRequest)
405
-
log.Println("failed to parse round id", err)
409
-
identsToResolve := []string{pull.OwnerDid}
410
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
411
-
didHandleMap := make(map[string]string)
412
-
for _, identity := range resolvedIds {
413
-
if !identity.Handle.IsInvalidHandle() {
414
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
416
-
didHandleMap[identity.DID.String()] = identity.DID.String()
420
-
w.Header().Set("Content-Type", "text/plain")
421
-
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
424
-
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
425
-
user := s.oauth.GetUser(r)
426
-
params := r.URL.Query()
428
-
state := db.PullOpen
429
-
switch params.Get("state") {
431
-
state = db.PullClosed
433
-
state = db.PullMerged
436
-
f, err := s.repoResolver.Resolve(r)
438
-
log.Println("failed to get repo and knot", err)
442
-
pulls, err := db.GetPulls(
444
-
db.FilterEq("repo_at", f.RepoAt),
445
-
db.FilterEq("state", state),
448
-
log.Println("failed to get pulls", err)
449
-
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
453
-
for _, p := range pulls {
454
-
var pullSourceRepo *db.Repo
455
-
if p.PullSource != nil {
456
-
if p.PullSource.RepoAt != nil {
457
-
pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String())
459
-
log.Printf("failed to get repo by at uri: %v", err)
462
-
p.PullSource.Repo = pullSourceRepo
468
-
identsToResolve := make([]string, len(pulls))
469
-
for i, pull := range pulls {
470
-
identsToResolve[i] = pull.OwnerDid
472
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
473
-
didHandleMap := make(map[string]string)
474
-
for _, identity := range resolvedIds {
475
-
if !identity.Handle.IsInvalidHandle() {
476
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
478
-
didHandleMap[identity.DID.String()] = identity.DID.String()
482
-
s.pages.RepoPulls(w, pages.RepoPullsParams{
483
-
LoggedInUser: s.oauth.GetUser(r),
484
-
RepoInfo: f.RepoInfo(user),
486
-
DidHandleMap: didHandleMap,
487
-
FilteringBy: state,
492
-
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
493
-
user := s.oauth.GetUser(r)
494
-
f, err := s.repoResolver.Resolve(r)
496
-
log.Println("failed to get repo and knot", err)
500
-
pull, ok := r.Context().Value("pull").(*db.Pull)
502
-
log.Println("failed to get pull")
503
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
507
-
roundNumberStr := chi.URLParam(r, "round")
508
-
roundNumber, err := strconv.Atoi(roundNumberStr)
509
-
if err != nil || roundNumber >= len(pull.Submissions) {
510
-
http.Error(w, "bad round id", http.StatusBadRequest)
511
-
log.Println("failed to parse round id", err)
516
-
case http.MethodGet:
517
-
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
518
-
LoggedInUser: user,
519
-
RepoInfo: f.RepoInfo(user),
521
-
RoundNumber: roundNumber,
524
-
case http.MethodPost:
525
-
body := r.FormValue("body")
527
-
s.pages.Notice(w, "pull", "Comment body is required")
531
-
// Start a transaction
532
-
tx, err := s.db.BeginTx(r.Context(), nil)
534
-
log.Println("failed to start transaction", err)
535
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
538
-
defer tx.Rollback()
540
-
createdAt := time.Now().Format(time.RFC3339)
541
-
ownerDid := user.Did
543
-
pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId)
545
-
log.Println("failed to get pull at", err)
546
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
550
-
atUri := f.RepoAt.String()
551
-
client, err := s.oauth.AuthorizedClient(r)
553
-
log.Println("failed to get authorized client", err)
554
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
557
-
atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
558
-
Collection: tangled.RepoPullCommentNSID,
560
-
Rkey: appview.TID(),
561
-
Record: &lexutil.LexiconTypeDecoder{
562
-
Val: &tangled.RepoPullComment{
564
-
Pull: string(pullAt),
567
-
CreatedAt: createdAt,
572
-
log.Println("failed to create pull comment", err)
573
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
577
-
// Create the pull comment in the database with the commentAt field
578
-
commentId, err := db.NewPullComment(tx, &db.PullComment{
579
-
OwnerDid: user.Did,
580
-
RepoAt: f.RepoAt.String(),
581
-
PullId: pull.PullId,
583
-
CommentAt: atResp.Uri,
584
-
SubmissionId: pull.Submissions[roundNumber].ID,
587
-
log.Println("failed to create pull comment", err)
588
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
592
-
// Commit the transaction
593
-
if err = tx.Commit(); err != nil {
594
-
log.Println("failed to commit transaction", err)
595
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
599
-
if !s.config.Core.Dev {
600
-
err = s.posthog.Enqueue(posthog.Capture{
601
-
DistinctId: user.Did,
602
-
Event: "new_pull_comment",
603
-
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pull.PullId},
606
-
log.Println("failed to enqueue posthog event:", err)
610
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
615
-
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
616
-
user := s.oauth.GetUser(r)
617
-
f, err := s.repoResolver.Resolve(r)
619
-
log.Println("failed to get repo and knot", err)
624
-
case http.MethodGet:
625
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
627
-
log.Printf("failed to create unsigned client for %s", f.Knot)
628
-
s.pages.Error503(w)
632
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
634
-
log.Println("failed to fetch branches", err)
638
-
// can be one of "patch", "branch" or "fork"
639
-
strategy := r.URL.Query().Get("strategy")
640
-
// ignored if strategy is "patch"
641
-
sourceBranch := r.URL.Query().Get("sourceBranch")
642
-
targetBranch := r.URL.Query().Get("targetBranch")
644
-
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
645
-
LoggedInUser: user,
646
-
RepoInfo: f.RepoInfo(user),
647
-
Branches: result.Branches,
648
-
Strategy: strategy,
649
-
SourceBranch: sourceBranch,
650
-
TargetBranch: targetBranch,
651
-
Title: r.URL.Query().Get("title"),
652
-
Body: r.URL.Query().Get("body"),
655
-
case http.MethodPost:
656
-
title := r.FormValue("title")
657
-
body := r.FormValue("body")
658
-
targetBranch := r.FormValue("targetBranch")
659
-
fromFork := r.FormValue("fork")
660
-
sourceBranch := r.FormValue("sourceBranch")
661
-
patch := r.FormValue("patch")
663
-
if targetBranch == "" {
664
-
s.pages.Notice(w, "pull", "Target branch is required.")
668
-
// Determine PR type based on input parameters
669
-
isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed()
670
-
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
671
-
isForkBased := fromFork != "" && sourceBranch != ""
672
-
isPatchBased := patch != "" && !isBranchBased && !isForkBased
673
-
isStacked := r.FormValue("isStacked") == "on"
675
-
if isPatchBased && !patchutil.IsFormatPatch(patch) {
677
-
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
682
-
// Validate we have at least one valid PR creation method
683
-
if !isBranchBased && !isPatchBased && !isForkBased {
684
-
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
688
-
// Can't mix branch-based and patch-based approaches
689
-
if isBranchBased && patch != "" {
690
-
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
694
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
696
-
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
697
-
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
701
-
caps, err := us.Capabilities()
703
-
log.Println("error fetching knot caps", f.Knot, err)
704
-
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
708
-
if !caps.PullRequests.FormatPatch {
709
-
s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.")
713
-
// Handle the PR creation based on the type
715
-
if !caps.PullRequests.BranchSubmissions {
716
-
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
719
-
s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked)
720
-
} else if isForkBased {
721
-
if !caps.PullRequests.ForkSubmissions {
722
-
s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
725
-
s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked)
726
-
} else if isPatchBased {
727
-
if !caps.PullRequests.PatchSubmissions {
728
-
s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
731
-
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked)
737
-
func (s *State) handleBranchBasedPull(
738
-
w http.ResponseWriter,
740
-
f *reporesolver.ResolvedRepo,
745
-
sourceBranch string,
748
-
pullSource := &db.PullSource{
749
-
Branch: sourceBranch,
751
-
recordPullSource := &tangled.RepoPull_Source{
752
-
Branch: sourceBranch,
755
-
// Generate a patch using /compare
756
-
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
758
-
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
759
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
763
-
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
765
-
log.Println("failed to compare", err)
766
-
s.pages.Notice(w, "pull", err.Error())
770
-
sourceRev := comparison.Rev2
771
-
patch := comparison.Patch
773
-
if !patchutil.IsPatchValid(patch) {
774
-
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
778
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
781
-
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
782
-
if !patchutil.IsPatchValid(patch) {
783
-
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
787
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked)
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) {
791
-
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
792
-
if errors.Is(err, sql.ErrNoRows) {
793
-
s.pages.Notice(w, "pull", "No such fork.")
795
-
} else if err != nil {
796
-
log.Println("failed to fetch fork:", err)
797
-
s.pages.Notice(w, "pull", "Failed to fetch fork.")
801
-
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
803
-
log.Println("failed to fetch registration key:", err)
804
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
808
-
sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
810
-
log.Println("failed to create signed client:", err)
811
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
815
-
us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
817
-
log.Println("failed to create unsigned client:", err)
818
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
822
-
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
824
-
log.Println("failed to create hidden ref:", err, resp.StatusCode)
825
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
829
-
switch resp.StatusCode {
832
-
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
836
-
hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)
837
-
// We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking
838
-
// the targetBranch on the target repository. This code is a bit confusing, but here's an example:
839
-
// hiddenRef: hidden/feature-1/main (on repo-fork)
840
-
// targetBranch: main (on repo-1)
841
-
// sourceBranch: feature-1 (on repo-fork)
842
-
comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
844
-
log.Println("failed to compare across branches", err)
845
-
s.pages.Notice(w, "pull", err.Error())
849
-
sourceRev := comparison.Rev2
850
-
patch := comparison.Patch
852
-
if !patchutil.IsPatchValid(patch) {
853
-
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
857
-
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
859
-
log.Println("failed to parse fork AT URI", err)
860
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
864
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
865
-
Branch: sourceBranch,
866
-
RepoAt: &forkAtUri,
867
-
}, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked)
870
-
func (s *State) createPullRequest(
871
-
w http.ResponseWriter,
873
-
f *reporesolver.ResolvedRepo,
875
-
title, body, targetBranch string,
878
-
pullSource *db.PullSource,
879
-
recordPullSource *tangled.RepoPull_Source,
883
-
// creates a series of PRs, each linking to the previous, identified by jj's change-id
884
-
s.createStackedPulLRequest(
897
-
client, err := s.oauth.AuthorizedClient(r)
899
-
log.Println("failed to get authorized client", err)
900
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
904
-
tx, err := s.db.BeginTx(r.Context(), nil)
906
-
log.Println("failed to start tx")
907
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
910
-
defer tx.Rollback()
912
-
// We've already checked earlier if it's diff-based and title is empty,
913
-
// so if it's still empty now, it's intentionally skipped owing to format-patch.
915
-
formatPatches, err := patchutil.ExtractPatches(patch)
917
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
920
-
if len(formatPatches) == 0 {
921
-
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
925
-
title = formatPatches[0].Title
926
-
body = formatPatches[0].Body
929
-
rkey := appview.TID()
930
-
initialSubmission := db.PullSubmission{
932
-
SourceRev: sourceRev,
934
-
err = db.NewPull(tx, &db.Pull{
937
-
TargetBranch: targetBranch,
938
-
OwnerDid: user.Did,
941
-
Submissions: []*db.PullSubmission{
942
-
&initialSubmission,
944
-
PullSource: pullSource,
947
-
log.Println("failed to create pull request", err)
948
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
951
-
pullId, err := db.NextPullId(tx, f.RepoAt)
953
-
log.Println("failed to get pull id", err)
954
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
958
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
959
-
Collection: tangled.RepoPullNSID,
962
-
Record: &lexutil.LexiconTypeDecoder{
963
-
Val: &tangled.RepoPull{
965
-
PullId: int64(pullId),
966
-
TargetRepo: string(f.RepoAt),
967
-
TargetBranch: targetBranch,
969
-
Source: recordPullSource,
974
-
log.Println("failed to create pull request", err)
975
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
979
-
if err = tx.Commit(); err != nil {
980
-
log.Println("failed to create pull request", err)
981
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
985
-
if !s.config.Core.Dev {
986
-
err = s.posthog.Enqueue(posthog.Capture{
987
-
DistinctId: user.Did,
989
-
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId},
992
-
log.Println("failed to enqueue posthog event:", err)
996
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
999
-
func (s *State) createStackedPulLRequest(
1000
-
w http.ResponseWriter,
1002
-
f *reporesolver.ResolvedRepo,
1004
-
targetBranch string,
1007
-
pullSource *db.PullSource,
1009
-
// run some necessary checks for stacked-prs first
1011
-
// must be branch or fork based
1012
-
if sourceRev == "" {
1013
-
log.Println("stacked PR from patch-based pull")
1014
-
s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.")
1018
-
formatPatches, err := patchutil.ExtractPatches(patch)
1020
-
log.Println("failed to extract patches", err)
1021
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
1025
-
// must have atleast 1 patch to begin with
1026
-
if len(formatPatches) == 0 {
1027
-
log.Println("empty patches")
1028
-
s.pages.Notice(w, "pull", "No patches found in the generated format-patch.")
1032
-
// build a stack out of this patch
1033
-
stackId := uuid.New()
1034
-
stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1036
-
log.Println("failed to create stack", err)
1037
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
1041
-
client, err := s.oauth.AuthorizedClient(r)
1043
-
log.Println("failed to get authorized client", err)
1044
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1048
-
// apply all record creations at once
1049
-
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1050
-
for _, p := range stack {
1051
-
record := p.AsRecord()
1052
-
write := comatproto.RepoApplyWrites_Input_Writes_Elem{
1053
-
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1054
-
Collection: tangled.RepoPullNSID,
1056
-
Value: &lexutil.LexiconTypeDecoder{
1061
-
writes = append(writes, &write)
1063
-
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1068
-
log.Println("failed to create stacked pull request", err)
1069
-
s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.")
1073
-
// create all pulls at once
1074
-
tx, err := s.db.BeginTx(r.Context(), nil)
1076
-
log.Println("failed to start tx")
1077
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1080
-
defer tx.Rollback()
1082
-
for _, p := range stack {
1083
-
err = db.NewPull(tx, p)
1085
-
log.Println("failed to create pull request", err)
1086
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1091
-
if err = tx.Commit(); err != nil {
1092
-
log.Println("failed to create pull request", err)
1093
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1097
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1100
-
func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
1101
-
_, err := s.repoResolver.Resolve(r)
1103
-
log.Println("failed to get repo and knot", err)
1107
-
patch := r.FormValue("patch")
1109
-
s.pages.Notice(w, "patch-error", "Patch is required.")
1113
-
if patch == "" || !patchutil.IsPatchValid(patch) {
1114
-
s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")
1118
-
if patchutil.IsFormatPatch(patch) {
1119
-
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.")
1121
-
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
1125
-
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1126
-
user := s.oauth.GetUser(r)
1127
-
f, err := s.repoResolver.Resolve(r)
1129
-
log.Println("failed to get repo and knot", err)
1133
-
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1134
-
RepoInfo: f.RepoInfo(user),
1138
-
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1139
-
user := s.oauth.GetUser(r)
1140
-
f, err := s.repoResolver.Resolve(r)
1142
-
log.Println("failed to get repo and knot", err)
1146
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1148
-
log.Printf("failed to create unsigned client for %s", f.Knot)
1149
-
s.pages.Error503(w)
1153
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1155
-
log.Println("failed to reach knotserver", err)
1159
-
branches := result.Branches
1160
-
sort.Slice(branches, func(i int, j int) bool {
1161
-
return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When)
1164
-
withoutDefault := []types.Branch{}
1165
-
for _, b := range branches {
1169
-
withoutDefault = append(withoutDefault, b)
1172
-
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1173
-
RepoInfo: f.RepoInfo(user),
1174
-
Branches: withoutDefault,
1178
-
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1179
-
user := s.oauth.GetUser(r)
1180
-
f, err := s.repoResolver.Resolve(r)
1182
-
log.Println("failed to get repo and knot", err)
1186
-
forks, err := db.GetForksByDid(s.db, user.Did)
1188
-
log.Println("failed to get forks", err)
1192
-
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1193
-
RepoInfo: f.RepoInfo(user),
1195
-
Selected: r.URL.Query().Get("fork"),
1199
-
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1200
-
user := s.oauth.GetUser(r)
1202
-
f, err := s.repoResolver.Resolve(r)
1204
-
log.Println("failed to get repo and knot", err)
1208
-
forkVal := r.URL.Query().Get("fork")
1211
-
repo, err := db.GetRepo(s.db, user.Did, forkVal)
1213
-
log.Println("failed to get repo", user.Did, forkVal)
1217
-
sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1219
-
log.Printf("failed to create unsigned client for %s", repo.Knot)
1220
-
s.pages.Error503(w)
1224
-
sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name)
1226
-
log.Println("failed to reach knotserver for source branches", err)
1230
-
targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1232
-
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1233
-
s.pages.Error503(w)
1237
-
targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
1239
-
log.Println("failed to reach knotserver for target branches", err)
1243
-
sourceBranches := sourceResult.Branches
1244
-
sort.Slice(sourceBranches, func(i int, j int) bool {
1245
-
return sourceBranches[i].Commit.Committer.When.After(sourceBranches[j].Commit.Committer.When)
1248
-
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1249
-
RepoInfo: f.RepoInfo(user),
1250
-
SourceBranches: sourceBranches,
1251
-
TargetBranches: targetResult.Branches,
1255
-
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1256
-
user := s.oauth.GetUser(r)
1257
-
f, err := s.repoResolver.Resolve(r)
1259
-
log.Println("failed to get repo and knot", err)
1263
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1265
-
log.Println("failed to get pull")
1266
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1271
-
case http.MethodGet:
1272
-
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1273
-
RepoInfo: f.RepoInfo(user),
1277
-
case http.MethodPost:
1278
-
if pull.IsPatchBased() {
1279
-
s.resubmitPatch(w, r)
1281
-
} else if pull.IsBranchBased() {
1282
-
s.resubmitBranch(w, r)
1284
-
} else if pull.IsForkBased() {
1285
-
s.resubmitFork(w, r)
1291
-
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1292
-
user := s.oauth.GetUser(r)
1294
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1296
-
log.Println("failed to get pull")
1297
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1301
-
f, err := s.repoResolver.Resolve(r)
1303
-
log.Println("failed to get repo and knot", err)
1307
-
if user.Did != pull.OwnerDid {
1308
-
log.Println("unauthorized user")
1309
-
w.WriteHeader(http.StatusUnauthorized)
1313
-
patch := r.FormValue("patch")
1315
-
s.resubmitPullHelper(w, r, f, user, pull, patch, "")
1318
-
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1319
-
user := s.oauth.GetUser(r)
1321
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1323
-
log.Println("failed to get pull")
1324
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1328
-
f, err := s.repoResolver.Resolve(r)
1330
-
log.Println("failed to get repo and knot", err)
1334
-
if user.Did != pull.OwnerDid {
1335
-
log.Println("unauthorized user")
1336
-
w.WriteHeader(http.StatusUnauthorized)
1340
-
if !f.RepoInfo(user).Roles.IsPushAllowed() {
1341
-
log.Println("unauthorized user")
1342
-
w.WriteHeader(http.StatusUnauthorized)
1346
-
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1348
-
log.Printf("failed to create client for %s: %s", f.Knot, err)
1349
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1353
-
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
1355
-
log.Printf("compare request failed: %s", err)
1356
-
s.pages.Notice(w, "resubmit-error", err.Error())
1360
-
sourceRev := comparison.Rev2
1361
-
patch := comparison.Patch
1363
-
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1366
-
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
1367
-
user := s.oauth.GetUser(r)
1369
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1371
-
log.Println("failed to get pull")
1372
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1376
-
f, err := s.repoResolver.Resolve(r)
1378
-
log.Println("failed to get repo and knot", err)
1382
-
if user.Did != pull.OwnerDid {
1383
-
log.Println("unauthorized user")
1384
-
w.WriteHeader(http.StatusUnauthorized)
1388
-
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
1390
-
log.Println("failed to get source repo", err)
1391
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1395
-
// extract patch by performing compare
1396
-
ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
1398
-
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
1399
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1403
-
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
1405
-
log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
1406
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1410
-
// update the hidden tracking branch to latest
1411
-
signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
1413
-
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
1414
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1418
-
resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
1419
-
if err != nil || resp.StatusCode != http.StatusNoContent {
1420
-
log.Printf("failed to update tracking branch: %s", err)
1421
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1425
-
hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)
1426
-
comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
1428
-
log.Printf("failed to compare branches: %s", err)
1429
-
s.pages.Notice(w, "resubmit-error", err.Error())
1433
-
sourceRev := comparison.Rev2
1434
-
patch := comparison.Patch
1436
-
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1439
-
// validate a resubmission against a pull request
1440
-
func validateResubmittedPatch(pull *db.Pull, patch string) error {
1442
-
return fmt.Errorf("Patch is empty.")
1445
-
if patch == pull.LatestPatch() {
1446
-
return fmt.Errorf("Patch is identical to previous submission.")
1449
-
if !patchutil.IsPatchValid(patch) {
1450
-
return fmt.Errorf("Invalid patch format. Please provide a valid diff.")
1456
-
func (s *State) resubmitPullHelper(
1457
-
w http.ResponseWriter,
1459
-
f *reporesolver.ResolvedRepo,
1465
-
if pull.IsStacked() {
1466
-
log.Println("resubmitting stacked PR")
1467
-
s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1471
-
if err := validateResubmittedPatch(pull, patch); err != nil {
1472
-
s.pages.Notice(w, "resubmit-error", err.Error())
1476
-
// validate sourceRev if branch/fork based
1477
-
if pull.IsBranchBased() || pull.IsForkBased() {
1478
-
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
1479
-
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
1484
-
tx, err := s.db.BeginTx(r.Context(), nil)
1486
-
log.Println("failed to start tx")
1487
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1490
-
defer tx.Rollback()
1492
-
err = db.ResubmitPull(tx, pull, patch, sourceRev)
1494
-
log.Println("failed to create pull request", err)
1495
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1498
-
client, err := s.oauth.AuthorizedClient(r)
1500
-
log.Println("failed to authorize client")
1501
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1505
-
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1507
-
// failed to get record
1508
-
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1512
-
var recordPullSource *tangled.RepoPull_Source
1513
-
if pull.IsBranchBased() {
1514
-
recordPullSource = &tangled.RepoPull_Source{
1515
-
Branch: pull.PullSource.Branch,
1518
-
if pull.IsForkBased() {
1519
-
repoAt := pull.PullSource.RepoAt.String()
1520
-
recordPullSource = &tangled.RepoPull_Source{
1521
-
Branch: pull.PullSource.Branch,
1526
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1527
-
Collection: tangled.RepoPullNSID,
1530
-
SwapRecord: ex.Cid,
1531
-
Record: &lexutil.LexiconTypeDecoder{
1532
-
Val: &tangled.RepoPull{
1533
-
Title: pull.Title,
1534
-
PullId: int64(pull.PullId),
1535
-
TargetRepo: string(f.RepoAt),
1536
-
TargetBranch: pull.TargetBranch,
1537
-
Patch: patch, // new patch
1538
-
Source: recordPullSource,
1543
-
log.Println("failed to update record", err)
1544
-
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
1548
-
if err = tx.Commit(); err != nil {
1549
-
log.Println("failed to commit transaction", err)
1550
-
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
1554
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1558
-
func (s *State) resubmitStackedPullHelper(
1559
-
w http.ResponseWriter,
1561
-
f *reporesolver.ResolvedRepo,
1567
-
targetBranch := pull.TargetBranch
1569
-
origStack, _ := r.Context().Value("stack").(db.Stack)
1570
-
newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
1572
-
log.Println("failed to create resubmitted stack", err)
1573
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1577
-
// find the diff between the stacks, first, map them by changeId
1578
-
origById := make(map[string]*db.Pull)
1579
-
newById := make(map[string]*db.Pull)
1580
-
for _, p := range origStack {
1581
-
origById[p.ChangeId] = p
1583
-
for _, p := range newStack {
1584
-
newById[p.ChangeId] = p
1587
-
// commits that got deleted: corresponding pull is closed
1588
-
// commits that got added: new pull is created
1589
-
// commits that got updated: corresponding pull is resubmitted & new round begins
1591
-
// for commits that were unchanged: no changes, parent-change-id is updated as necessary
1592
-
additions := make(map[string]*db.Pull)
1593
-
deletions := make(map[string]*db.Pull)
1594
-
unchanged := make(map[string]struct{})
1595
-
updated := make(map[string]struct{})
1597
-
// pulls in orignal stack but not in new one
1598
-
for _, op := range origStack {
1599
-
if _, ok := newById[op.ChangeId]; !ok {
1600
-
deletions[op.ChangeId] = op
1604
-
// pulls in new stack but not in original one
1605
-
for _, np := range newStack {
1606
-
if _, ok := origById[np.ChangeId]; !ok {
1607
-
additions[np.ChangeId] = np
1611
-
// NOTE: this loop can be written in any of above blocks,
1612
-
// but is written separately in the interest of simpler code
1613
-
for _, np := range newStack {
1614
-
if op, ok := origById[np.ChangeId]; ok {
1615
-
// pull exists in both stacks
1616
-
// TODO: can we avoid reparse?
1617
-
origFiles, origHeaderStr, _ := gitdiff.Parse(strings.NewReader(op.LatestPatch()))
1618
-
newFiles, newHeaderStr, _ := gitdiff.Parse(strings.NewReader(np.LatestPatch()))
1620
-
origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr)
1621
-
newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr)
1623
-
patchutil.SortPatch(newFiles)
1624
-
patchutil.SortPatch(origFiles)
1626
-
// text content of patch may be identical, but a jj rebase might have forwarded it
1628
-
// we still need to update the hash in submission.Patch and submission.SourceRev
1629
-
if patchutil.Equal(newFiles, origFiles) &&
1630
-
origHeader.Title == newHeader.Title &&
1631
-
origHeader.Body == newHeader.Body {
1632
-
unchanged[op.ChangeId] = struct{}{}
1634
-
updated[op.ChangeId] = struct{}{}
1639
-
tx, err := s.db.Begin()
1641
-
log.Println("failed to start transaction", err)
1642
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1645
-
defer tx.Rollback()
1647
-
// pds updates to make
1648
-
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1650
-
// deleted pulls are marked as deleted in the DB
1651
-
for _, p := range deletions {
1652
-
err := db.DeletePull(tx, p.RepoAt, p.PullId)
1654
-
log.Println("failed to delete pull", err, p.PullId)
1655
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1658
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1659
-
RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
1660
-
Collection: tangled.RepoPullNSID,
1666
-
// new pulls are created
1667
-
for _, p := range additions {
1668
-
err := db.NewPull(tx, p)
1670
-
log.Println("failed to create pull", err, p.PullId)
1671
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1675
-
record := p.AsRecord()
1676
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1677
-
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1678
-
Collection: tangled.RepoPullNSID,
1680
-
Value: &lexutil.LexiconTypeDecoder{
1687
-
// updated pulls are, well, updated; to start a new round
1688
-
for id := range updated {
1689
-
op, _ := origById[id]
1690
-
np, _ := newById[id]
1692
-
submission := np.Submissions[np.LastRoundNumber()]
1694
-
// resubmit the old pull
1695
-
err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev)
1698
-
log.Println("failed to update pull", err, op.PullId)
1699
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1703
-
record := op.AsRecord()
1704
-
record.Patch = submission.Patch
1706
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1707
-
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1708
-
Collection: tangled.RepoPullNSID,
1710
-
Value: &lexutil.LexiconTypeDecoder{
1717
-
// unchanged pulls are edited without starting a new round
1719
-
// update source-revs & patches without advancing rounds
1720
-
for changeId := range unchanged {
1721
-
op, _ := origById[changeId]
1722
-
np, _ := newById[changeId]
1724
-
origSubmission := op.Submissions[op.LastRoundNumber()]
1725
-
newSubmission := np.Submissions[np.LastRoundNumber()]
1727
-
log.Println("moving unchanged change id : ", changeId)
1729
-
err := db.UpdatePull(
1731
-
newSubmission.Patch,
1732
-
newSubmission.SourceRev,
1733
-
db.FilterEq("id", origSubmission.ID),
1737
-
log.Println("failed to update pull", err, op.PullId)
1738
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1742
-
record := op.AsRecord()
1743
-
record.Patch = newSubmission.Patch
1745
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1746
-
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1747
-
Collection: tangled.RepoPullNSID,
1749
-
Value: &lexutil.LexiconTypeDecoder{
1756
-
// update parent-change-id relations for the entire stack
1757
-
for _, p := range newStack {
1758
-
err := db.SetPullParentChangeId(
1761
-
// these should be enough filters to be unique per-stack
1762
-
db.FilterEq("repo_at", p.RepoAt.String()),
1763
-
db.FilterEq("owner_did", p.OwnerDid),
1764
-
db.FilterEq("change_id", p.ChangeId),
1768
-
log.Println("failed to update pull", err, p.PullId)
1769
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1776
-
log.Println("failed to resubmit pull", err)
1777
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1781
-
client, err := s.oauth.AuthorizedClient(r)
1783
-
log.Println("failed to authorize client")
1784
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1788
-
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1793
-
log.Println("failed to create stacked pull request", err)
1794
-
s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.")
1798
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1802
-
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
1803
-
f, err := s.repoResolver.Resolve(r)
1805
-
log.Println("failed to resolve repo:", err)
1806
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1810
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1812
-
log.Println("failed to get pull")
1813
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1817
-
var pullsToMerge db.Stack
1818
-
pullsToMerge = append(pullsToMerge, pull)
1819
-
if pull.IsStacked() {
1820
-
stack, ok := r.Context().Value("stack").(db.Stack)
1822
-
log.Println("failed to get stack")
1823
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1827
-
// combine patches of substack
1828
-
subStack := stack.StrictlyBelow(pull)
1829
-
// collect the portion of the stack that is mergeable
1830
-
mergeable := subStack.Mergeable()
1831
-
// add to total patch
1832
-
pullsToMerge = append(pullsToMerge, mergeable...)
1835
-
patch := pullsToMerge.CombinedPatch()
1837
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
1839
-
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
1840
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1844
-
ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid)
1846
-
log.Printf("resolving identity: %s", err)
1847
-
w.WriteHeader(http.StatusNotFound)
1851
-
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
1853
-
log.Printf("failed to get primary email: %s", err)
1856
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
1858
-
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
1859
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1863
-
// Merge the pull request
1864
-
resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address)
1866
-
log.Printf("failed to merge pull request: %s", err)
1867
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1871
-
if resp.StatusCode != http.StatusOK {
1872
-
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
1873
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1877
-
tx, err := s.db.Begin()
1879
-
log.Println("failed to start transcation", err)
1880
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1883
-
defer tx.Rollback()
1885
-
for _, p := range pullsToMerge {
1886
-
err := db.MergePull(tx, f.RepoAt, p.PullId)
1888
-
log.Printf("failed to update pull request status in database: %s", err)
1889
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1896
-
// TODO: this is unsound, we should also revert the merge from the knotserver here
1897
-
log.Printf("failed to update pull request status in database: %s", err)
1898
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1902
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
1905
-
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1906
-
user := s.oauth.GetUser(r)
1908
-
f, err := s.repoResolver.Resolve(r)
1910
-
log.Println("malformed middleware")
1914
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1916
-
log.Println("failed to get pull")
1917
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1921
-
// auth filter: only owner or collaborators can close
1922
-
roles := f.RolesInRepo(user)
1923
-
isCollaborator := roles.IsCollaborator()
1924
-
isPullAuthor := user.Did == pull.OwnerDid
1925
-
isCloseAllowed := isCollaborator || isPullAuthor
1926
-
if !isCloseAllowed {
1927
-
log.Println("failed to close pull")
1928
-
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
1932
-
// Start a transaction
1933
-
tx, err := s.db.BeginTx(r.Context(), nil)
1935
-
log.Println("failed to start transaction", err)
1936
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1939
-
defer tx.Rollback()
1941
-
var pullsToClose []*db.Pull
1942
-
pullsToClose = append(pullsToClose, pull)
1944
-
// if this PR is stacked, then we want to close all PRs below this one on the stack
1945
-
if pull.IsStacked() {
1946
-
stack := r.Context().Value("stack").(db.Stack)
1947
-
subStack := stack.StrictlyBelow(pull)
1948
-
pullsToClose = append(pullsToClose, subStack...)
1951
-
for _, p := range pullsToClose {
1952
-
// Close the pull in the database
1953
-
err = db.ClosePull(tx, f.RepoAt, p.PullId)
1955
-
log.Println("failed to close pull", err)
1956
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1961
-
// Commit the transaction
1962
-
if err = tx.Commit(); err != nil {
1963
-
log.Println("failed to commit transaction", err)
1964
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1968
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1972
-
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1973
-
user := s.oauth.GetUser(r)
1975
-
f, err := s.repoResolver.Resolve(r)
1977
-
log.Println("failed to resolve repo", err)
1978
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1982
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1984
-
log.Println("failed to get pull")
1985
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1989
-
// auth filter: only owner or collaborators can close
1990
-
roles := f.RolesInRepo(user)
1991
-
isCollaborator := roles.IsCollaborator()
1992
-
isPullAuthor := user.Did == pull.OwnerDid
1993
-
isCloseAllowed := isCollaborator || isPullAuthor
1994
-
if !isCloseAllowed {
1995
-
log.Println("failed to close pull")
1996
-
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
2000
-
// Start a transaction
2001
-
tx, err := s.db.BeginTx(r.Context(), nil)
2003
-
log.Println("failed to start transaction", err)
2004
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2007
-
defer tx.Rollback()
2009
-
var pullsToReopen []*db.Pull
2010
-
pullsToReopen = append(pullsToReopen, pull)
2012
-
// if this PR is stacked, then we want to reopen all PRs above this one on the stack
2013
-
if pull.IsStacked() {
2014
-
stack := r.Context().Value("stack").(db.Stack)
2015
-
subStack := stack.StrictlyAbove(pull)
2016
-
pullsToReopen = append(pullsToReopen, subStack...)
2019
-
for _, p := range pullsToReopen {
2020
-
// Close the pull in the database
2021
-
err = db.ReopenPull(tx, f.RepoAt, p.PullId)
2023
-
log.Println("failed to close pull", err)
2024
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
2029
-
// Commit the transaction
2030
-
if err = tx.Commit(); err != nil {
2031
-
log.Println("failed to commit transaction", err)
2032
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2036
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2040
-
func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *db.PullSource, stackId string) (db.Stack, error) {
2041
-
formatPatches, err := patchutil.ExtractPatches(patch)
2043
-
return nil, fmt.Errorf("Failed to extract patches: %v", err)
2046
-
// must have atleast 1 patch to begin with
2047
-
if len(formatPatches) == 0 {
2048
-
return nil, fmt.Errorf("No patches found in the generated format-patch.")
2051
-
// the stack is identified by a UUID
2052
-
var stack db.Stack
2053
-
parentChangeId := ""
2054
-
for _, fp := range formatPatches {
2055
-
// all patches must have a jj change-id
2056
-
changeId, err := fp.ChangeId()
2058
-
return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.")
2063
-
rkey := appview.TID()
2065
-
initialSubmission := db.PullSubmission{
2067
-
SourceRev: fp.SHA,
2072
-
TargetBranch: targetBranch,
2073
-
OwnerDid: user.Did,
2076
-
Submissions: []*db.PullSubmission{
2077
-
&initialSubmission,
2079
-
PullSource: pullSource,
2080
-
Created: time.Now(),
2083
-
ChangeId: changeId,
2084
-
ParentChangeId: parentChangeId,
2087
-
stack = append(stack, &pull)
2089
-
parentChangeId = changeId