+2
-2
appview/middleware/middleware.go
+2
-2
appview/middleware/middleware.go
···
24
24
type Middleware struct {
25
25
oauth *oauth.OAuth
26
26
db *db.DB
27
-
enforcer rbac.Enforcer
27
+
enforcer *rbac.Enforcer
28
28
repoResolver *reporesolver.RepoResolver
29
29
resolver *appview.Resolver
30
30
pages *pages.Pages
31
31
}
32
32
33
-
func New(oauth *oauth.OAuth, db *db.DB, enforcer rbac.Enforcer, repoResolver *reporesolver.RepoResolver, resolver *appview.Resolver, pages *pages.Pages) Middleware {
33
+
func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, resolver *appview.Resolver, pages *pages.Pages) Middleware {
34
34
return Middleware{
35
35
oauth: oauth,
36
36
db: db,
+59
appview/pulls/router.go
+59
appview/pulls/router.go
···
1
+
package pulls
2
+
3
+
import (
4
+
"net/http"
5
+
6
+
"github.com/go-chi/chi/v5"
7
+
"tangled.sh/tangled.sh/core/appview/middleware"
8
+
)
9
+
10
+
func (s *Pulls) Router(mw *middleware.Middleware) http.Handler {
11
+
r := chi.NewRouter()
12
+
r.Get("/", s.RepoPulls)
13
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
14
+
r.Get("/", s.NewPull)
15
+
r.Get("/patch-upload", s.PatchUploadFragment)
16
+
r.Post("/validate-patch", s.ValidatePatch)
17
+
r.Get("/compare-branches", s.CompareBranchesFragment)
18
+
r.Get("/compare-forks", s.CompareForksFragment)
19
+
r.Get("/fork-branches", s.CompareForksBranchesFragment)
20
+
r.Post("/", s.NewPull)
21
+
})
22
+
23
+
r.Route("/{pull}", func(r chi.Router) {
24
+
r.Use(mw.ResolvePull())
25
+
r.Get("/", s.RepoSinglePull)
26
+
27
+
r.Route("/round/{round}", func(r chi.Router) {
28
+
r.Get("/", s.RepoPullPatch)
29
+
r.Get("/interdiff", s.RepoPullInterdiff)
30
+
r.Get("/actions", s.PullActions)
31
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
32
+
r.Get("/", s.PullComment)
33
+
r.Post("/", s.PullComment)
34
+
})
35
+
})
36
+
37
+
r.Route("/round/{round}.patch", func(r chi.Router) {
38
+
r.Get("/", s.RepoPullPatchRaw)
39
+
})
40
+
41
+
r.Group(func(r chi.Router) {
42
+
r.Use(middleware.AuthMiddleware(s.oauth))
43
+
r.Route("/resubmit", func(r chi.Router) {
44
+
r.Get("/", s.ResubmitPull)
45
+
r.Post("/", s.ResubmitPull)
46
+
})
47
+
r.Post("/close", s.ClosePull)
48
+
r.Post("/reopen", s.ReopenPull)
49
+
// collaborators only
50
+
r.Group(func(r chi.Router) {
51
+
r.Use(mw.RepoPermissionMiddleware("repo:push"))
52
+
r.Post("/merge", s.MergePull)
53
+
// maybe lock, etc.
54
+
})
55
+
})
56
+
})
57
+
return r
58
+
59
+
}
-2093
appview/state/pull.go
-2093
appview/state/pull.go
···
1
-
package state
2
-
3
-
import (
4
-
"database/sql"
5
-
"encoding/json"
6
-
"errors"
7
-
"fmt"
8
-
"io"
9
-
"log"
10
-
"net/http"
11
-
"sort"
12
-
"strconv"
13
-
"strings"
14
-
"time"
15
-
16
-
"tangled.sh/tangled.sh/core/api/tangled"
17
-
"tangled.sh/tangled.sh/core/appview"
18
-
"tangled.sh/tangled.sh/core/appview/db"
19
-
"tangled.sh/tangled.sh/core/appview/oauth"
20
-
"tangled.sh/tangled.sh/core/appview/pages"
21
-
"tangled.sh/tangled.sh/core/appview/reporesolver"
22
-
"tangled.sh/tangled.sh/core/knotclient"
23
-
"tangled.sh/tangled.sh/core/patchutil"
24
-
"tangled.sh/tangled.sh/core/types"
25
-
26
-
"github.com/bluekeyes/go-gitdiff/gitdiff"
27
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
28
-
"github.com/bluesky-social/indigo/atproto/syntax"
29
-
lexutil "github.com/bluesky-social/indigo/lex/util"
30
-
"github.com/go-chi/chi/v5"
31
-
"github.com/google/uuid"
32
-
"github.com/posthog/posthog-go"
33
-
)
34
-
35
-
// htmx fragment
36
-
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
37
-
switch r.Method {
38
-
case http.MethodGet:
39
-
user := s.oauth.GetUser(r)
40
-
f, err := s.repoResolver.Resolve(r)
41
-
if err != nil {
42
-
log.Println("failed to get repo and knot", err)
43
-
return
44
-
}
45
-
46
-
pull, ok := r.Context().Value("pull").(*db.Pull)
47
-
if !ok {
48
-
log.Println("failed to get pull")
49
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
50
-
return
51
-
}
52
-
53
-
// can be nil if this pull is not stacked
54
-
stack, _ := r.Context().Value("stack").(db.Stack)
55
-
56
-
roundNumberStr := chi.URLParam(r, "round")
57
-
roundNumber, err := strconv.Atoi(roundNumberStr)
58
-
if err != nil {
59
-
roundNumber = pull.LastRoundNumber()
60
-
}
61
-
if roundNumber >= len(pull.Submissions) {
62
-
http.Error(w, "bad round id", http.StatusBadRequest)
63
-
log.Println("failed to parse round id", err)
64
-
return
65
-
}
66
-
67
-
mergeCheckResponse := s.mergeCheck(f, pull, stack)
68
-
resubmitResult := pages.Unknown
69
-
if user.Did == pull.OwnerDid {
70
-
resubmitResult = s.resubmitCheck(f, pull, stack)
71
-
}
72
-
73
-
s.pages.PullActionsFragment(w, pages.PullActionsParams{
74
-
LoggedInUser: user,
75
-
RepoInfo: f.RepoInfo(user),
76
-
Pull: pull,
77
-
RoundNumber: roundNumber,
78
-
MergeCheck: mergeCheckResponse,
79
-
ResubmitCheck: resubmitResult,
80
-
Stack: stack,
81
-
})
82
-
return
83
-
}
84
-
}
85
-
86
-
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
87
-
user := s.oauth.GetUser(r)
88
-
f, err := s.repoResolver.Resolve(r)
89
-
if err != nil {
90
-
log.Println("failed to get repo and knot", err)
91
-
return
92
-
}
93
-
94
-
pull, ok := r.Context().Value("pull").(*db.Pull)
95
-
if !ok {
96
-
log.Println("failed to get pull")
97
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
98
-
return
99
-
}
100
-
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)
104
-
105
-
totalIdents := 1
106
-
for _, submission := range pull.Submissions {
107
-
totalIdents += len(submission.Comments)
108
-
}
109
-
110
-
identsToResolve := make([]string, totalIdents)
111
-
112
-
// populate idents
113
-
identsToResolve[0] = pull.OwnerDid
114
-
idx := 1
115
-
for _, submission := range pull.Submissions {
116
-
for _, comment := range submission.Comments {
117
-
identsToResolve[idx] = comment.OwnerDid
118
-
idx += 1
119
-
}
120
-
}
121
-
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())
127
-
} else {
128
-
didHandleMap[identity.DID.String()] = identity.DID.String()
129
-
}
130
-
}
131
-
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)
136
-
}
137
-
138
-
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
139
-
LoggedInUser: user,
140
-
RepoInfo: f.RepoInfo(user),
141
-
DidHandleMap: didHandleMap,
142
-
Pull: pull,
143
-
Stack: stack,
144
-
AbandonedPulls: abandonedPulls,
145
-
MergeCheck: mergeCheckResponse,
146
-
ResubmitCheck: resubmitResult,
147
-
})
148
-
}
149
-
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{}
153
-
}
154
-
155
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
156
-
if err != nil {
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",
160
-
}
161
-
}
162
-
163
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
164
-
if err != nil {
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",
168
-
}
169
-
}
170
-
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()
179
-
}
180
-
181
-
resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch)
182
-
if err != nil {
183
-
log.Println("failed to check for mergeability:", err)
184
-
return types.MergeCheckResponse{
185
-
Error: "failed to check merge status",
186
-
}
187
-
}
188
-
switch resp.StatusCode {
189
-
case 404:
190
-
return types.MergeCheckResponse{
191
-
Error: "failed to check merge status: this knot does not support PRs",
192
-
}
193
-
case 400:
194
-
return types.MergeCheckResponse{
195
-
Error: "failed to check merge status: does this knot support PRs?",
196
-
}
197
-
}
198
-
199
-
respBody, err := io.ReadAll(resp.Body)
200
-
if err != nil {
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",
204
-
}
205
-
}
206
-
defer resp.Body.Close()
207
-
208
-
var mergeCheckResponse types.MergeCheckResponse
209
-
err = json.Unmarshal(respBody, &mergeCheckResponse)
210
-
if err != nil {
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",
214
-
}
215
-
}
216
-
217
-
return mergeCheckResponse
218
-
}
219
-
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
223
-
}
224
-
225
-
var knot, ownerDid, repoName string
226
-
227
-
if pull.PullSource.RepoAt != nil {
228
-
// fork-based pulls
229
-
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
230
-
if err != nil {
231
-
log.Println("failed to get source repo", err)
232
-
return pages.Unknown
233
-
}
234
-
235
-
knot = sourceRepo.Knot
236
-
ownerDid = sourceRepo.Did
237
-
repoName = sourceRepo.Name
238
-
} else {
239
-
// pulls within the same repo
240
-
knot = f.Knot
241
-
ownerDid = f.OwnerDid()
242
-
repoName = f.RepoName
243
-
}
244
-
245
-
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
246
-
if err != nil {
247
-
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
248
-
return pages.Unknown
249
-
}
250
-
251
-
result, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
252
-
if err != nil {
253
-
log.Println("failed to reach knotserver", err)
254
-
return pages.Unknown
255
-
}
256
-
257
-
latestSourceRev := pull.Submissions[pull.LastRoundNumber()].SourceRev
258
-
259
-
if pull.IsStacked() && stack != nil {
260
-
top := stack[0]
261
-
latestSourceRev = top.Submissions[top.LastRoundNumber()].SourceRev
262
-
}
263
-
264
-
log.Println(latestSourceRev, result.Branch.Hash)
265
-
266
-
if latestSourceRev != result.Branch.Hash {
267
-
return pages.ShouldResubmit
268
-
}
269
-
270
-
return pages.ShouldNotResubmit
271
-
}
272
-
273
-
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
274
-
user := s.oauth.GetUser(r)
275
-
f, err := s.repoResolver.Resolve(r)
276
-
if err != nil {
277
-
log.Println("failed to get repo and knot", err)
278
-
return
279
-
}
280
-
281
-
pull, ok := r.Context().Value("pull").(*db.Pull)
282
-
if !ok {
283
-
log.Println("failed to get pull")
284
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
285
-
return
286
-
}
287
-
288
-
stack, _ := r.Context().Value("stack").(db.Stack)
289
-
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)
295
-
return
296
-
}
297
-
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())
304
-
} else {
305
-
didHandleMap[identity.DID.String()] = identity.DID.String()
306
-
}
307
-
}
308
-
309
-
patch := pull.Submissions[roundIdInt].Patch
310
-
diff := patchutil.AsNiceDiff(patch, pull.TargetBranch)
311
-
312
-
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
313
-
LoggedInUser: user,
314
-
DidHandleMap: didHandleMap,
315
-
RepoInfo: f.RepoInfo(user),
316
-
Pull: pull,
317
-
Stack: stack,
318
-
Round: roundIdInt,
319
-
Submission: pull.Submissions[roundIdInt],
320
-
Diff: &diff,
321
-
})
322
-
323
-
}
324
-
325
-
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
326
-
user := s.oauth.GetUser(r)
327
-
328
-
f, err := s.repoResolver.Resolve(r)
329
-
if err != nil {
330
-
log.Println("failed to get repo and knot", err)
331
-
return
332
-
}
333
-
334
-
pull, ok := r.Context().Value("pull").(*db.Pull)
335
-
if !ok {
336
-
log.Println("failed to get pull")
337
-
s.pages.Notice(w, "pull-error", "Failed to get pull.")
338
-
return
339
-
}
340
-
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)
346
-
return
347
-
}
348
-
349
-
if roundIdInt == 0 {
350
-
http.Error(w, "bad round id", http.StatusBadRequest)
351
-
log.Println("cannot interdiff initial submission")
352
-
return
353
-
}
354
-
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())
361
-
} else {
362
-
didHandleMap[identity.DID.String()] = identity.DID.String()
363
-
}
364
-
}
365
-
366
-
currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch)
367
-
if err != nil {
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.")
370
-
return
371
-
}
372
-
373
-
previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].Patch)
374
-
if err != nil {
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.")
377
-
return
378
-
}
379
-
380
-
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
381
-
382
-
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
383
-
LoggedInUser: s.oauth.GetUser(r),
384
-
RepoInfo: f.RepoInfo(user),
385
-
Pull: pull,
386
-
Round: roundIdInt,
387
-
DidHandleMap: didHandleMap,
388
-
Interdiff: interdiff,
389
-
})
390
-
return
391
-
}
392
-
393
-
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
394
-
pull, ok := r.Context().Value("pull").(*db.Pull)
395
-
if !ok {
396
-
log.Println("failed to get pull")
397
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
398
-
return
399
-
}
400
-
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)
406
-
return
407
-
}
408
-
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())
415
-
} else {
416
-
didHandleMap[identity.DID.String()] = identity.DID.String()
417
-
}
418
-
}
419
-
420
-
w.Header().Set("Content-Type", "text/plain")
421
-
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
422
-
}
423
-
424
-
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
425
-
user := s.oauth.GetUser(r)
426
-
params := r.URL.Query()
427
-
428
-
state := db.PullOpen
429
-
switch params.Get("state") {
430
-
case "closed":
431
-
state = db.PullClosed
432
-
case "merged":
433
-
state = db.PullMerged
434
-
}
435
-
436
-
f, err := s.repoResolver.Resolve(r)
437
-
if err != nil {
438
-
log.Println("failed to get repo and knot", err)
439
-
return
440
-
}
441
-
442
-
pulls, err := db.GetPulls(
443
-
s.db,
444
-
db.FilterEq("repo_at", f.RepoAt),
445
-
db.FilterEq("state", state),
446
-
)
447
-
if err != nil {
448
-
log.Println("failed to get pulls", err)
449
-
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
450
-
return
451
-
}
452
-
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())
458
-
if err != nil {
459
-
log.Printf("failed to get repo by at uri: %v", err)
460
-
continue
461
-
} else {
462
-
p.PullSource.Repo = pullSourceRepo
463
-
}
464
-
}
465
-
}
466
-
}
467
-
468
-
identsToResolve := make([]string, len(pulls))
469
-
for i, pull := range pulls {
470
-
identsToResolve[i] = pull.OwnerDid
471
-
}
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())
477
-
} else {
478
-
didHandleMap[identity.DID.String()] = identity.DID.String()
479
-
}
480
-
}
481
-
482
-
s.pages.RepoPulls(w, pages.RepoPullsParams{
483
-
LoggedInUser: s.oauth.GetUser(r),
484
-
RepoInfo: f.RepoInfo(user),
485
-
Pulls: pulls,
486
-
DidHandleMap: didHandleMap,
487
-
FilteringBy: state,
488
-
})
489
-
return
490
-
}
491
-
492
-
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
493
-
user := s.oauth.GetUser(r)
494
-
f, err := s.repoResolver.Resolve(r)
495
-
if err != nil {
496
-
log.Println("failed to get repo and knot", err)
497
-
return
498
-
}
499
-
500
-
pull, ok := r.Context().Value("pull").(*db.Pull)
501
-
if !ok {
502
-
log.Println("failed to get pull")
503
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
504
-
return
505
-
}
506
-
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)
512
-
return
513
-
}
514
-
515
-
switch r.Method {
516
-
case http.MethodGet:
517
-
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
518
-
LoggedInUser: user,
519
-
RepoInfo: f.RepoInfo(user),
520
-
Pull: pull,
521
-
RoundNumber: roundNumber,
522
-
})
523
-
return
524
-
case http.MethodPost:
525
-
body := r.FormValue("body")
526
-
if body == "" {
527
-
s.pages.Notice(w, "pull", "Comment body is required")
528
-
return
529
-
}
530
-
531
-
// Start a transaction
532
-
tx, err := s.db.BeginTx(r.Context(), nil)
533
-
if err != nil {
534
-
log.Println("failed to start transaction", err)
535
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
536
-
return
537
-
}
538
-
defer tx.Rollback()
539
-
540
-
createdAt := time.Now().Format(time.RFC3339)
541
-
ownerDid := user.Did
542
-
543
-
pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId)
544
-
if err != nil {
545
-
log.Println("failed to get pull at", err)
546
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
547
-
return
548
-
}
549
-
550
-
atUri := f.RepoAt.String()
551
-
client, err := s.oauth.AuthorizedClient(r)
552
-
if err != nil {
553
-
log.Println("failed to get authorized client", err)
554
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
555
-
return
556
-
}
557
-
atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
558
-
Collection: tangled.RepoPullCommentNSID,
559
-
Repo: user.Did,
560
-
Rkey: appview.TID(),
561
-
Record: &lexutil.LexiconTypeDecoder{
562
-
Val: &tangled.RepoPullComment{
563
-
Repo: &atUri,
564
-
Pull: string(pullAt),
565
-
Owner: &ownerDid,
566
-
Body: body,
567
-
CreatedAt: createdAt,
568
-
},
569
-
},
570
-
})
571
-
if err != nil {
572
-
log.Println("failed to create pull comment", err)
573
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
574
-
return
575
-
}
576
-
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,
582
-
Body: body,
583
-
CommentAt: atResp.Uri,
584
-
SubmissionId: pull.Submissions[roundNumber].ID,
585
-
})
586
-
if err != nil {
587
-
log.Println("failed to create pull comment", err)
588
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
589
-
return
590
-
}
591
-
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.")
596
-
return
597
-
}
598
-
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},
604
-
})
605
-
if err != nil {
606
-
log.Println("failed to enqueue posthog event:", err)
607
-
}
608
-
}
609
-
610
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
611
-
return
612
-
}
613
-
}
614
-
615
-
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
616
-
user := s.oauth.GetUser(r)
617
-
f, err := s.repoResolver.Resolve(r)
618
-
if err != nil {
619
-
log.Println("failed to get repo and knot", err)
620
-
return
621
-
}
622
-
623
-
switch r.Method {
624
-
case http.MethodGet:
625
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
626
-
if err != nil {
627
-
log.Printf("failed to create unsigned client for %s", f.Knot)
628
-
s.pages.Error503(w)
629
-
return
630
-
}
631
-
632
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
633
-
if err != nil {
634
-
log.Println("failed to fetch branches", err)
635
-
return
636
-
}
637
-
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")
643
-
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"),
653
-
})
654
-
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")
662
-
663
-
if targetBranch == "" {
664
-
s.pages.Notice(w, "pull", "Target branch is required.")
665
-
return
666
-
}
667
-
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"
674
-
675
-
if isPatchBased && !patchutil.IsFormatPatch(patch) {
676
-
if title == "" {
677
-
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
678
-
return
679
-
}
680
-
}
681
-
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.")
685
-
return
686
-
}
687
-
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.")
691
-
return
692
-
}
693
-
694
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
695
-
if err != nil {
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.")
698
-
return
699
-
}
700
-
701
-
caps, err := us.Capabilities()
702
-
if err != nil {
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.")
705
-
return
706
-
}
707
-
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.")
710
-
return
711
-
}
712
-
713
-
// Handle the PR creation based on the type
714
-
if isBranchBased {
715
-
if !caps.PullRequests.BranchSubmissions {
716
-
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
717
-
return
718
-
}
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?")
723
-
return
724
-
}
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.")
729
-
return
730
-
}
731
-
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked)
732
-
}
733
-
return
734
-
}
735
-
}
736
-
737
-
func (s *State) handleBranchBasedPull(
738
-
w http.ResponseWriter,
739
-
r *http.Request,
740
-
f *reporesolver.ResolvedRepo,
741
-
user *oauth.User,
742
-
title,
743
-
body,
744
-
targetBranch,
745
-
sourceBranch string,
746
-
isStacked bool,
747
-
) {
748
-
pullSource := &db.PullSource{
749
-
Branch: sourceBranch,
750
-
}
751
-
recordPullSource := &tangled.RepoPull_Source{
752
-
Branch: sourceBranch,
753
-
}
754
-
755
-
// Generate a patch using /compare
756
-
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
757
-
if err != nil {
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.")
760
-
return
761
-
}
762
-
763
-
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
764
-
if err != nil {
765
-
log.Println("failed to compare", err)
766
-
s.pages.Notice(w, "pull", err.Error())
767
-
return
768
-
}
769
-
770
-
sourceRev := comparison.Rev2
771
-
patch := comparison.Patch
772
-
773
-
if !patchutil.IsPatchValid(patch) {
774
-
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
775
-
return
776
-
}
777
-
778
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
779
-
}
780
-
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.")
784
-
return
785
-
}
786
-
787
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil, isStacked)
788
-
}
789
-
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.")
794
-
return
795
-
} else if err != nil {
796
-
log.Println("failed to fetch fork:", err)
797
-
s.pages.Notice(w, "pull", "Failed to fetch fork.")
798
-
return
799
-
}
800
-
801
-
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
802
-
if err != nil {
803
-
log.Println("failed to fetch registration key:", err)
804
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
805
-
return
806
-
}
807
-
808
-
sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
809
-
if err != nil {
810
-
log.Println("failed to create signed client:", err)
811
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
812
-
return
813
-
}
814
-
815
-
us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
816
-
if err != nil {
817
-
log.Println("failed to create unsigned client:", err)
818
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
819
-
return
820
-
}
821
-
822
-
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
823
-
if err != nil {
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.")
826
-
return
827
-
}
828
-
829
-
switch resp.StatusCode {
830
-
case 404:
831
-
case 400:
832
-
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
833
-
return
834
-
}
835
-
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)
843
-
if err != nil {
844
-
log.Println("failed to compare across branches", err)
845
-
s.pages.Notice(w, "pull", err.Error())
846
-
return
847
-
}
848
-
849
-
sourceRev := comparison.Rev2
850
-
patch := comparison.Patch
851
-
852
-
if !patchutil.IsPatchValid(patch) {
853
-
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
854
-
return
855
-
}
856
-
857
-
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
858
-
if err != nil {
859
-
log.Println("failed to parse fork AT URI", err)
860
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
861
-
return
862
-
}
863
-
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)
868
-
}
869
-
870
-
func (s *State) createPullRequest(
871
-
w http.ResponseWriter,
872
-
r *http.Request,
873
-
f *reporesolver.ResolvedRepo,
874
-
user *oauth.User,
875
-
title, body, targetBranch string,
876
-
patch string,
877
-
sourceRev string,
878
-
pullSource *db.PullSource,
879
-
recordPullSource *tangled.RepoPull_Source,
880
-
isStacked bool,
881
-
) {
882
-
if isStacked {
883
-
// creates a series of PRs, each linking to the previous, identified by jj's change-id
884
-
s.createStackedPulLRequest(
885
-
w,
886
-
r,
887
-
f,
888
-
user,
889
-
targetBranch,
890
-
patch,
891
-
sourceRev,
892
-
pullSource,
893
-
)
894
-
return
895
-
}
896
-
897
-
client, err := s.oauth.AuthorizedClient(r)
898
-
if err != nil {
899
-
log.Println("failed to get authorized client", err)
900
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
901
-
return
902
-
}
903
-
904
-
tx, err := s.db.BeginTx(r.Context(), nil)
905
-
if err != nil {
906
-
log.Println("failed to start tx")
907
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
908
-
return
909
-
}
910
-
defer tx.Rollback()
911
-
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.
914
-
if title == "" {
915
-
formatPatches, err := patchutil.ExtractPatches(patch)
916
-
if err != nil {
917
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
918
-
return
919
-
}
920
-
if len(formatPatches) == 0 {
921
-
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
922
-
return
923
-
}
924
-
925
-
title = formatPatches[0].Title
926
-
body = formatPatches[0].Body
927
-
}
928
-
929
-
rkey := appview.TID()
930
-
initialSubmission := db.PullSubmission{
931
-
Patch: patch,
932
-
SourceRev: sourceRev,
933
-
}
934
-
err = db.NewPull(tx, &db.Pull{
935
-
Title: title,
936
-
Body: body,
937
-
TargetBranch: targetBranch,
938
-
OwnerDid: user.Did,
939
-
RepoAt: f.RepoAt,
940
-
Rkey: rkey,
941
-
Submissions: []*db.PullSubmission{
942
-
&initialSubmission,
943
-
},
944
-
PullSource: pullSource,
945
-
})
946
-
if err != nil {
947
-
log.Println("failed to create pull request", err)
948
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
949
-
return
950
-
}
951
-
pullId, err := db.NextPullId(tx, f.RepoAt)
952
-
if err != nil {
953
-
log.Println("failed to get pull id", err)
954
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
955
-
return
956
-
}
957
-
958
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
959
-
Collection: tangled.RepoPullNSID,
960
-
Repo: user.Did,
961
-
Rkey: rkey,
962
-
Record: &lexutil.LexiconTypeDecoder{
963
-
Val: &tangled.RepoPull{
964
-
Title: title,
965
-
PullId: int64(pullId),
966
-
TargetRepo: string(f.RepoAt),
967
-
TargetBranch: targetBranch,
968
-
Patch: patch,
969
-
Source: recordPullSource,
970
-
},
971
-
},
972
-
})
973
-
if err != nil {
974
-
log.Println("failed to create pull request", err)
975
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
976
-
return
977
-
}
978
-
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.")
982
-
return
983
-
}
984
-
985
-
if !s.config.Core.Dev {
986
-
err = s.posthog.Enqueue(posthog.Capture{
987
-
DistinctId: user.Did,
988
-
Event: "new_pull",
989
-
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "pull_id": pullId},
990
-
})
991
-
if err != nil {
992
-
log.Println("failed to enqueue posthog event:", err)
993
-
}
994
-
}
995
-
996
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
997
-
}
998
-
999
-
func (s *State) createStackedPulLRequest(
1000
-
w http.ResponseWriter,
1001
-
r *http.Request,
1002
-
f *reporesolver.ResolvedRepo,
1003
-
user *oauth.User,
1004
-
targetBranch string,
1005
-
patch string,
1006
-
sourceRev string,
1007
-
pullSource *db.PullSource,
1008
-
) {
1009
-
// run some necessary checks for stacked-prs first
1010
-
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.")
1015
-
return
1016
-
}
1017
-
1018
-
formatPatches, err := patchutil.ExtractPatches(patch)
1019
-
if err != nil {
1020
-
log.Println("failed to extract patches", err)
1021
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
1022
-
return
1023
-
}
1024
-
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.")
1029
-
return
1030
-
}
1031
-
1032
-
// build a stack out of this patch
1033
-
stackId := uuid.New()
1034
-
stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1035
-
if err != nil {
1036
-
log.Println("failed to create stack", err)
1037
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
1038
-
return
1039
-
}
1040
-
1041
-
client, err := s.oauth.AuthorizedClient(r)
1042
-
if err != nil {
1043
-
log.Println("failed to get authorized client", err)
1044
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1045
-
return
1046
-
}
1047
-
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,
1055
-
Rkey: &p.Rkey,
1056
-
Value: &lexutil.LexiconTypeDecoder{
1057
-
Val: &record,
1058
-
},
1059
-
},
1060
-
}
1061
-
writes = append(writes, &write)
1062
-
}
1063
-
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1064
-
Repo: user.Did,
1065
-
Writes: writes,
1066
-
})
1067
-
if err != nil {
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.")
1070
-
return
1071
-
}
1072
-
1073
-
// create all pulls at once
1074
-
tx, err := s.db.BeginTx(r.Context(), nil)
1075
-
if err != nil {
1076
-
log.Println("failed to start tx")
1077
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1078
-
return
1079
-
}
1080
-
defer tx.Rollback()
1081
-
1082
-
for _, p := range stack {
1083
-
err = db.NewPull(tx, p)
1084
-
if err != nil {
1085
-
log.Println("failed to create pull request", err)
1086
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1087
-
return
1088
-
}
1089
-
}
1090
-
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.")
1094
-
return
1095
-
}
1096
-
1097
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1098
-
}
1099
-
1100
-
func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
1101
-
_, err := s.repoResolver.Resolve(r)
1102
-
if err != nil {
1103
-
log.Println("failed to get repo and knot", err)
1104
-
return
1105
-
}
1106
-
1107
-
patch := r.FormValue("patch")
1108
-
if patch == "" {
1109
-
s.pages.Notice(w, "patch-error", "Patch is required.")
1110
-
return
1111
-
}
1112
-
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.")
1115
-
return
1116
-
}
1117
-
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.")
1120
-
} else {
1121
-
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
1122
-
}
1123
-
}
1124
-
1125
-
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1126
-
user := s.oauth.GetUser(r)
1127
-
f, err := s.repoResolver.Resolve(r)
1128
-
if err != nil {
1129
-
log.Println("failed to get repo and knot", err)
1130
-
return
1131
-
}
1132
-
1133
-
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1134
-
RepoInfo: f.RepoInfo(user),
1135
-
})
1136
-
}
1137
-
1138
-
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1139
-
user := s.oauth.GetUser(r)
1140
-
f, err := s.repoResolver.Resolve(r)
1141
-
if err != nil {
1142
-
log.Println("failed to get repo and knot", err)
1143
-
return
1144
-
}
1145
-
1146
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1147
-
if err != nil {
1148
-
log.Printf("failed to create unsigned client for %s", f.Knot)
1149
-
s.pages.Error503(w)
1150
-
return
1151
-
}
1152
-
1153
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1154
-
if err != nil {
1155
-
log.Println("failed to reach knotserver", err)
1156
-
return
1157
-
}
1158
-
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)
1162
-
})
1163
-
1164
-
withoutDefault := []types.Branch{}
1165
-
for _, b := range branches {
1166
-
if b.IsDefault {
1167
-
continue
1168
-
}
1169
-
withoutDefault = append(withoutDefault, b)
1170
-
}
1171
-
1172
-
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1173
-
RepoInfo: f.RepoInfo(user),
1174
-
Branches: withoutDefault,
1175
-
})
1176
-
}
1177
-
1178
-
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1179
-
user := s.oauth.GetUser(r)
1180
-
f, err := s.repoResolver.Resolve(r)
1181
-
if err != nil {
1182
-
log.Println("failed to get repo and knot", err)
1183
-
return
1184
-
}
1185
-
1186
-
forks, err := db.GetForksByDid(s.db, user.Did)
1187
-
if err != nil {
1188
-
log.Println("failed to get forks", err)
1189
-
return
1190
-
}
1191
-
1192
-
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1193
-
RepoInfo: f.RepoInfo(user),
1194
-
Forks: forks,
1195
-
Selected: r.URL.Query().Get("fork"),
1196
-
})
1197
-
}
1198
-
1199
-
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1200
-
user := s.oauth.GetUser(r)
1201
-
1202
-
f, err := s.repoResolver.Resolve(r)
1203
-
if err != nil {
1204
-
log.Println("failed to get repo and knot", err)
1205
-
return
1206
-
}
1207
-
1208
-
forkVal := r.URL.Query().Get("fork")
1209
-
1210
-
// fork repo
1211
-
repo, err := db.GetRepo(s.db, user.Did, forkVal)
1212
-
if err != nil {
1213
-
log.Println("failed to get repo", user.Did, forkVal)
1214
-
return
1215
-
}
1216
-
1217
-
sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1218
-
if err != nil {
1219
-
log.Printf("failed to create unsigned client for %s", repo.Knot)
1220
-
s.pages.Error503(w)
1221
-
return
1222
-
}
1223
-
1224
-
sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name)
1225
-
if err != nil {
1226
-
log.Println("failed to reach knotserver for source branches", err)
1227
-
return
1228
-
}
1229
-
1230
-
targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1231
-
if err != nil {
1232
-
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1233
-
s.pages.Error503(w)
1234
-
return
1235
-
}
1236
-
1237
-
targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
1238
-
if err != nil {
1239
-
log.Println("failed to reach knotserver for target branches", err)
1240
-
return
1241
-
}
1242
-
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)
1246
-
})
1247
-
1248
-
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1249
-
RepoInfo: f.RepoInfo(user),
1250
-
SourceBranches: sourceBranches,
1251
-
TargetBranches: targetResult.Branches,
1252
-
})
1253
-
}
1254
-
1255
-
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1256
-
user := s.oauth.GetUser(r)
1257
-
f, err := s.repoResolver.Resolve(r)
1258
-
if err != nil {
1259
-
log.Println("failed to get repo and knot", err)
1260
-
return
1261
-
}
1262
-
1263
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1264
-
if !ok {
1265
-
log.Println("failed to get pull")
1266
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1267
-
return
1268
-
}
1269
-
1270
-
switch r.Method {
1271
-
case http.MethodGet:
1272
-
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1273
-
RepoInfo: f.RepoInfo(user),
1274
-
Pull: pull,
1275
-
})
1276
-
return
1277
-
case http.MethodPost:
1278
-
if pull.IsPatchBased() {
1279
-
s.resubmitPatch(w, r)
1280
-
return
1281
-
} else if pull.IsBranchBased() {
1282
-
s.resubmitBranch(w, r)
1283
-
return
1284
-
} else if pull.IsForkBased() {
1285
-
s.resubmitFork(w, r)
1286
-
return
1287
-
}
1288
-
}
1289
-
}
1290
-
1291
-
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1292
-
user := s.oauth.GetUser(r)
1293
-
1294
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1295
-
if !ok {
1296
-
log.Println("failed to get pull")
1297
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1298
-
return
1299
-
}
1300
-
1301
-
f, err := s.repoResolver.Resolve(r)
1302
-
if err != nil {
1303
-
log.Println("failed to get repo and knot", err)
1304
-
return
1305
-
}
1306
-
1307
-
if user.Did != pull.OwnerDid {
1308
-
log.Println("unauthorized user")
1309
-
w.WriteHeader(http.StatusUnauthorized)
1310
-
return
1311
-
}
1312
-
1313
-
patch := r.FormValue("patch")
1314
-
1315
-
s.resubmitPullHelper(w, r, f, user, pull, patch, "")
1316
-
}
1317
-
1318
-
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1319
-
user := s.oauth.GetUser(r)
1320
-
1321
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1322
-
if !ok {
1323
-
log.Println("failed to get pull")
1324
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1325
-
return
1326
-
}
1327
-
1328
-
f, err := s.repoResolver.Resolve(r)
1329
-
if err != nil {
1330
-
log.Println("failed to get repo and knot", err)
1331
-
return
1332
-
}
1333
-
1334
-
if user.Did != pull.OwnerDid {
1335
-
log.Println("unauthorized user")
1336
-
w.WriteHeader(http.StatusUnauthorized)
1337
-
return
1338
-
}
1339
-
1340
-
if !f.RepoInfo(user).Roles.IsPushAllowed() {
1341
-
log.Println("unauthorized user")
1342
-
w.WriteHeader(http.StatusUnauthorized)
1343
-
return
1344
-
}
1345
-
1346
-
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1347
-
if err != nil {
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.")
1350
-
return
1351
-
}
1352
-
1353
-
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
1354
-
if err != nil {
1355
-
log.Printf("compare request failed: %s", err)
1356
-
s.pages.Notice(w, "resubmit-error", err.Error())
1357
-
return
1358
-
}
1359
-
1360
-
sourceRev := comparison.Rev2
1361
-
patch := comparison.Patch
1362
-
1363
-
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1364
-
}
1365
-
1366
-
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
1367
-
user := s.oauth.GetUser(r)
1368
-
1369
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1370
-
if !ok {
1371
-
log.Println("failed to get pull")
1372
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
1373
-
return
1374
-
}
1375
-
1376
-
f, err := s.repoResolver.Resolve(r)
1377
-
if err != nil {
1378
-
log.Println("failed to get repo and knot", err)
1379
-
return
1380
-
}
1381
-
1382
-
if user.Did != pull.OwnerDid {
1383
-
log.Println("unauthorized user")
1384
-
w.WriteHeader(http.StatusUnauthorized)
1385
-
return
1386
-
}
1387
-
1388
-
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
1389
-
if err != nil {
1390
-
log.Println("failed to get source repo", err)
1391
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1392
-
return
1393
-
}
1394
-
1395
-
// extract patch by performing compare
1396
-
ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
1397
-
if err != nil {
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.")
1400
-
return
1401
-
}
1402
-
1403
-
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
1404
-
if err != nil {
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.")
1407
-
return
1408
-
}
1409
-
1410
-
// update the hidden tracking branch to latest
1411
-
signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
1412
-
if err != nil {
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.")
1415
-
return
1416
-
}
1417
-
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.")
1422
-
return
1423
-
}
1424
-
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)
1427
-
if err != nil {
1428
-
log.Printf("failed to compare branches: %s", err)
1429
-
s.pages.Notice(w, "resubmit-error", err.Error())
1430
-
return
1431
-
}
1432
-
1433
-
sourceRev := comparison.Rev2
1434
-
patch := comparison.Patch
1435
-
1436
-
s.resubmitPullHelper(w, r, f, user, pull, patch, sourceRev)
1437
-
}
1438
-
1439
-
// validate a resubmission against a pull request
1440
-
func validateResubmittedPatch(pull *db.Pull, patch string) error {
1441
-
if patch == "" {
1442
-
return fmt.Errorf("Patch is empty.")
1443
-
}
1444
-
1445
-
if patch == pull.LatestPatch() {
1446
-
return fmt.Errorf("Patch is identical to previous submission.")
1447
-
}
1448
-
1449
-
if !patchutil.IsPatchValid(patch) {
1450
-
return fmt.Errorf("Invalid patch format. Please provide a valid diff.")
1451
-
}
1452
-
1453
-
return nil
1454
-
}
1455
-
1456
-
func (s *State) resubmitPullHelper(
1457
-
w http.ResponseWriter,
1458
-
r *http.Request,
1459
-
f *reporesolver.ResolvedRepo,
1460
-
user *oauth.User,
1461
-
pull *db.Pull,
1462
-
patch string,
1463
-
sourceRev string,
1464
-
) {
1465
-
if pull.IsStacked() {
1466
-
log.Println("resubmitting stacked PR")
1467
-
s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1468
-
return
1469
-
}
1470
-
1471
-
if err := validateResubmittedPatch(pull, patch); err != nil {
1472
-
s.pages.Notice(w, "resubmit-error", err.Error())
1473
-
return
1474
-
}
1475
-
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.")
1480
-
return
1481
-
}
1482
-
}
1483
-
1484
-
tx, err := s.db.BeginTx(r.Context(), nil)
1485
-
if err != nil {
1486
-
log.Println("failed to start tx")
1487
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1488
-
return
1489
-
}
1490
-
defer tx.Rollback()
1491
-
1492
-
err = db.ResubmitPull(tx, pull, patch, sourceRev)
1493
-
if err != nil {
1494
-
log.Println("failed to create pull request", err)
1495
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1496
-
return
1497
-
}
1498
-
client, err := s.oauth.AuthorizedClient(r)
1499
-
if err != nil {
1500
-
log.Println("failed to authorize client")
1501
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1502
-
return
1503
-
}
1504
-
1505
-
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1506
-
if err != nil {
1507
-
// failed to get record
1508
-
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1509
-
return
1510
-
}
1511
-
1512
-
var recordPullSource *tangled.RepoPull_Source
1513
-
if pull.IsBranchBased() {
1514
-
recordPullSource = &tangled.RepoPull_Source{
1515
-
Branch: pull.PullSource.Branch,
1516
-
}
1517
-
}
1518
-
if pull.IsForkBased() {
1519
-
repoAt := pull.PullSource.RepoAt.String()
1520
-
recordPullSource = &tangled.RepoPull_Source{
1521
-
Branch: pull.PullSource.Branch,
1522
-
Repo: &repoAt,
1523
-
}
1524
-
}
1525
-
1526
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1527
-
Collection: tangled.RepoPullNSID,
1528
-
Repo: user.Did,
1529
-
Rkey: pull.Rkey,
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,
1539
-
},
1540
-
},
1541
-
})
1542
-
if err != nil {
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.")
1545
-
return
1546
-
}
1547
-
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.")
1551
-
return
1552
-
}
1553
-
1554
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1555
-
return
1556
-
}
1557
-
1558
-
func (s *State) resubmitStackedPullHelper(
1559
-
w http.ResponseWriter,
1560
-
r *http.Request,
1561
-
f *reporesolver.ResolvedRepo,
1562
-
user *oauth.User,
1563
-
pull *db.Pull,
1564
-
patch string,
1565
-
stackId string,
1566
-
) {
1567
-
targetBranch := pull.TargetBranch
1568
-
1569
-
origStack, _ := r.Context().Value("stack").(db.Stack)
1570
-
newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
1571
-
if err != nil {
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.")
1574
-
return
1575
-
}
1576
-
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
1582
-
}
1583
-
for _, p := range newStack {
1584
-
newById[p.ChangeId] = p
1585
-
}
1586
-
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
1590
-
//
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{})
1596
-
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
1601
-
}
1602
-
}
1603
-
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
1608
-
}
1609
-
}
1610
-
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()))
1619
-
1620
-
origHeader, _ := gitdiff.ParsePatchHeader(origHeaderStr)
1621
-
newHeader, _ := gitdiff.ParsePatchHeader(newHeaderStr)
1622
-
1623
-
patchutil.SortPatch(newFiles)
1624
-
patchutil.SortPatch(origFiles)
1625
-
1626
-
// text content of patch may be identical, but a jj rebase might have forwarded it
1627
-
//
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{}{}
1633
-
} else {
1634
-
updated[op.ChangeId] = struct{}{}
1635
-
}
1636
-
}
1637
-
}
1638
-
1639
-
tx, err := s.db.Begin()
1640
-
if err != nil {
1641
-
log.Println("failed to start transaction", err)
1642
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1643
-
return
1644
-
}
1645
-
defer tx.Rollback()
1646
-
1647
-
// pds updates to make
1648
-
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1649
-
1650
-
// deleted pulls are marked as deleted in the DB
1651
-
for _, p := range deletions {
1652
-
err := db.DeletePull(tx, p.RepoAt, p.PullId)
1653
-
if err != nil {
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.")
1656
-
return
1657
-
}
1658
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1659
-
RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
1660
-
Collection: tangled.RepoPullNSID,
1661
-
Rkey: p.Rkey,
1662
-
},
1663
-
})
1664
-
}
1665
-
1666
-
// new pulls are created
1667
-
for _, p := range additions {
1668
-
err := db.NewPull(tx, p)
1669
-
if err != nil {
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.")
1672
-
return
1673
-
}
1674
-
1675
-
record := p.AsRecord()
1676
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1677
-
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1678
-
Collection: tangled.RepoPullNSID,
1679
-
Rkey: &p.Rkey,
1680
-
Value: &lexutil.LexiconTypeDecoder{
1681
-
Val: &record,
1682
-
},
1683
-
},
1684
-
})
1685
-
}
1686
-
1687
-
// updated pulls are, well, updated; to start a new round
1688
-
for id := range updated {
1689
-
op, _ := origById[id]
1690
-
np, _ := newById[id]
1691
-
1692
-
submission := np.Submissions[np.LastRoundNumber()]
1693
-
1694
-
// resubmit the old pull
1695
-
err := db.ResubmitPull(tx, op, submission.Patch, submission.SourceRev)
1696
-
1697
-
if err != nil {
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.")
1700
-
return
1701
-
}
1702
-
1703
-
record := op.AsRecord()
1704
-
record.Patch = submission.Patch
1705
-
1706
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1707
-
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1708
-
Collection: tangled.RepoPullNSID,
1709
-
Rkey: op.Rkey,
1710
-
Value: &lexutil.LexiconTypeDecoder{
1711
-
Val: &record,
1712
-
},
1713
-
},
1714
-
})
1715
-
}
1716
-
1717
-
// unchanged pulls are edited without starting a new round
1718
-
//
1719
-
// update source-revs & patches without advancing rounds
1720
-
for changeId := range unchanged {
1721
-
op, _ := origById[changeId]
1722
-
np, _ := newById[changeId]
1723
-
1724
-
origSubmission := op.Submissions[op.LastRoundNumber()]
1725
-
newSubmission := np.Submissions[np.LastRoundNumber()]
1726
-
1727
-
log.Println("moving unchanged change id : ", changeId)
1728
-
1729
-
err := db.UpdatePull(
1730
-
tx,
1731
-
newSubmission.Patch,
1732
-
newSubmission.SourceRev,
1733
-
db.FilterEq("id", origSubmission.ID),
1734
-
)
1735
-
1736
-
if err != nil {
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.")
1739
-
return
1740
-
}
1741
-
1742
-
record := op.AsRecord()
1743
-
record.Patch = newSubmission.Patch
1744
-
1745
-
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1746
-
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
1747
-
Collection: tangled.RepoPullNSID,
1748
-
Rkey: op.Rkey,
1749
-
Value: &lexutil.LexiconTypeDecoder{
1750
-
Val: &record,
1751
-
},
1752
-
},
1753
-
})
1754
-
}
1755
-
1756
-
// update parent-change-id relations for the entire stack
1757
-
for _, p := range newStack {
1758
-
err := db.SetPullParentChangeId(
1759
-
tx,
1760
-
p.ParentChangeId,
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),
1765
-
)
1766
-
1767
-
if err != nil {
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.")
1770
-
return
1771
-
}
1772
-
}
1773
-
1774
-
err = tx.Commit()
1775
-
if err != nil {
1776
-
log.Println("failed to resubmit pull", err)
1777
-
s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.")
1778
-
return
1779
-
}
1780
-
1781
-
client, err := s.oauth.AuthorizedClient(r)
1782
-
if err != nil {
1783
-
log.Println("failed to authorize client")
1784
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1785
-
return
1786
-
}
1787
-
1788
-
_, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{
1789
-
Repo: user.Did,
1790
-
Writes: writes,
1791
-
})
1792
-
if err != nil {
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.")
1795
-
return
1796
-
}
1797
-
1798
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1799
-
return
1800
-
}
1801
-
1802
-
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
1803
-
f, err := s.repoResolver.Resolve(r)
1804
-
if err != nil {
1805
-
log.Println("failed to resolve repo:", err)
1806
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1807
-
return
1808
-
}
1809
-
1810
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1811
-
if !ok {
1812
-
log.Println("failed to get pull")
1813
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1814
-
return
1815
-
}
1816
-
1817
-
var pullsToMerge db.Stack
1818
-
pullsToMerge = append(pullsToMerge, pull)
1819
-
if pull.IsStacked() {
1820
-
stack, ok := r.Context().Value("stack").(db.Stack)
1821
-
if !ok {
1822
-
log.Println("failed to get stack")
1823
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge patch. Try again later.")
1824
-
return
1825
-
}
1826
-
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...)
1833
-
}
1834
-
1835
-
patch := pullsToMerge.CombinedPatch()
1836
-
1837
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
1838
-
if err != nil {
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.")
1841
-
return
1842
-
}
1843
-
1844
-
ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid)
1845
-
if err != nil {
1846
-
log.Printf("resolving identity: %s", err)
1847
-
w.WriteHeader(http.StatusNotFound)
1848
-
return
1849
-
}
1850
-
1851
-
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
1852
-
if err != nil {
1853
-
log.Printf("failed to get primary email: %s", err)
1854
-
}
1855
-
1856
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
1857
-
if err != nil {
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.")
1860
-
return
1861
-
}
1862
-
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)
1865
-
if err != nil {
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.")
1868
-
return
1869
-
}
1870
-
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.")
1874
-
return
1875
-
}
1876
-
1877
-
tx, err := s.db.Begin()
1878
-
if err != nil {
1879
-
log.Println("failed to start transcation", err)
1880
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1881
-
return
1882
-
}
1883
-
defer tx.Rollback()
1884
-
1885
-
for _, p := range pullsToMerge {
1886
-
err := db.MergePull(tx, f.RepoAt, p.PullId)
1887
-
if err != nil {
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.")
1890
-
return
1891
-
}
1892
-
}
1893
-
1894
-
err = tx.Commit()
1895
-
if err != nil {
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.")
1899
-
return
1900
-
}
1901
-
1902
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
1903
-
}
1904
-
1905
-
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1906
-
user := s.oauth.GetUser(r)
1907
-
1908
-
f, err := s.repoResolver.Resolve(r)
1909
-
if err != nil {
1910
-
log.Println("malformed middleware")
1911
-
return
1912
-
}
1913
-
1914
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1915
-
if !ok {
1916
-
log.Println("failed to get pull")
1917
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1918
-
return
1919
-
}
1920
-
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.")
1929
-
return
1930
-
}
1931
-
1932
-
// Start a transaction
1933
-
tx, err := s.db.BeginTx(r.Context(), nil)
1934
-
if err != nil {
1935
-
log.Println("failed to start transaction", err)
1936
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1937
-
return
1938
-
}
1939
-
defer tx.Rollback()
1940
-
1941
-
var pullsToClose []*db.Pull
1942
-
pullsToClose = append(pullsToClose, pull)
1943
-
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...)
1949
-
}
1950
-
1951
-
for _, p := range pullsToClose {
1952
-
// Close the pull in the database
1953
-
err = db.ClosePull(tx, f.RepoAt, p.PullId)
1954
-
if err != nil {
1955
-
log.Println("failed to close pull", err)
1956
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
1957
-
return
1958
-
}
1959
-
}
1960
-
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.")
1965
-
return
1966
-
}
1967
-
1968
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1969
-
return
1970
-
}
1971
-
1972
-
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1973
-
user := s.oauth.GetUser(r)
1974
-
1975
-
f, err := s.repoResolver.Resolve(r)
1976
-
if err != nil {
1977
-
log.Println("failed to resolve repo", err)
1978
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
1979
-
return
1980
-
}
1981
-
1982
-
pull, ok := r.Context().Value("pull").(*db.Pull)
1983
-
if !ok {
1984
-
log.Println("failed to get pull")
1985
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
1986
-
return
1987
-
}
1988
-
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.")
1997
-
return
1998
-
}
1999
-
2000
-
// Start a transaction
2001
-
tx, err := s.db.BeginTx(r.Context(), nil)
2002
-
if err != nil {
2003
-
log.Println("failed to start transaction", err)
2004
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
2005
-
return
2006
-
}
2007
-
defer tx.Rollback()
2008
-
2009
-
var pullsToReopen []*db.Pull
2010
-
pullsToReopen = append(pullsToReopen, pull)
2011
-
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...)
2017
-
}
2018
-
2019
-
for _, p := range pullsToReopen {
2020
-
// Close the pull in the database
2021
-
err = db.ReopenPull(tx, f.RepoAt, p.PullId)
2022
-
if err != nil {
2023
-
log.Println("failed to close pull", err)
2024
-
s.pages.Notice(w, "pull-close", "Failed to close pull.")
2025
-
return
2026
-
}
2027
-
}
2028
-
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.")
2033
-
return
2034
-
}
2035
-
2036
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2037
-
return
2038
-
}
2039
-
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)
2042
-
if err != nil {
2043
-
return nil, fmt.Errorf("Failed to extract patches: %v", err)
2044
-
}
2045
-
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.")
2049
-
}
2050
-
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()
2057
-
if err != nil {
2058
-
return nil, fmt.Errorf("Stacking is only supported if all patches contain a change-id commit header.")
2059
-
}
2060
-
2061
-
title := fp.Title
2062
-
body := fp.Body
2063
-
rkey := appview.TID()
2064
-
2065
-
initialSubmission := db.PullSubmission{
2066
-
Patch: fp.Raw,
2067
-
SourceRev: fp.SHA,
2068
-
}
2069
-
pull := db.Pull{
2070
-
Title: title,
2071
-
Body: body,
2072
-
TargetBranch: targetBranch,
2073
-
OwnerDid: user.Did,
2074
-
RepoAt: f.RepoAt,
2075
-
Rkey: rkey,
2076
-
Submissions: []*db.PullSubmission{
2077
-
&initialSubmission,
2078
-
},
2079
-
PullSource: pullSource,
2080
-
Created: time.Now(),
2081
-
2082
-
StackId: stackId,
2083
-
ChangeId: changeId,
2084
-
ParentChangeId: parentChangeId,
2085
-
}
2086
-
2087
-
stack = append(stack, &pull)
2088
-
2089
-
parentChangeId = changeId
2090
-
}
2091
-
2092
-
return stack, nil
2093
-
}
+8
-47
appview/state/router.go
+8
-47
appview/state/router.go
···
8
8
"github.com/gorilla/sessions"
9
9
"tangled.sh/tangled.sh/core/appview/middleware"
10
10
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
11
+
"tangled.sh/tangled.sh/core/appview/pulls"
11
12
"tangled.sh/tangled.sh/core/appview/settings"
12
13
"tangled.sh/tangled.sh/core/appview/state/userutil"
13
14
)
···
138
139
r.Get("/*", s.RepoCompare)
139
140
})
140
141
141
-
r.Route("/pulls", func(r chi.Router) {
142
-
r.Get("/", s.RepoPulls)
143
-
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
144
-
r.Get("/", s.NewPull)
145
-
r.Get("/patch-upload", s.PatchUploadFragment)
146
-
r.Post("/validate-patch", s.ValidatePatch)
147
-
r.Get("/compare-branches", s.CompareBranchesFragment)
148
-
r.Get("/compare-forks", s.CompareForksFragment)
149
-
r.Get("/fork-branches", s.CompareForksBranchesFragment)
150
-
r.Post("/", s.NewPull)
151
-
})
152
-
153
-
r.Route("/{pull}", func(r chi.Router) {
154
-
r.Use(mw.ResolvePull())
155
-
r.Get("/", s.RepoSinglePull)
156
-
157
-
r.Route("/round/{round}", func(r chi.Router) {
158
-
r.Get("/", s.RepoPullPatch)
159
-
r.Get("/interdiff", s.RepoPullInterdiff)
160
-
r.Get("/actions", s.PullActions)
161
-
r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
162
-
r.Get("/", s.PullComment)
163
-
r.Post("/", s.PullComment)
164
-
})
165
-
})
166
-
167
-
r.Route("/round/{round}.patch", func(r chi.Router) {
168
-
r.Get("/", s.RepoPullPatchRaw)
169
-
})
170
-
171
-
r.Group(func(r chi.Router) {
172
-
r.Use(middleware.AuthMiddleware(s.oauth))
173
-
r.Route("/resubmit", func(r chi.Router) {
174
-
r.Get("/", s.ResubmitPull)
175
-
r.Post("/", s.ResubmitPull)
176
-
})
177
-
r.Post("/close", s.ClosePull)
178
-
r.Post("/reopen", s.ReopenPull)
179
-
// collaborators only
180
-
r.Group(func(r chi.Router) {
181
-
r.Use(mw.RepoPermissionMiddleware("repo:push"))
182
-
r.Post("/merge", s.MergePull)
183
-
// maybe lock, etc.
184
-
})
185
-
})
186
-
})
187
-
})
142
+
r.Mount("/pulls", s.PullsRouter(mw))
188
143
189
144
// These routes get proxied to the knot
190
145
r.Get("/info/refs", s.InfoRefs)
···
272
227
273
228
r.Mount("/settings", s.SettingsRouter())
274
229
r.Mount("/", s.OAuthRouter())
230
+
275
231
r.Get("/keys/{user}", s.Keys)
276
232
277
233
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
···
305
261
306
262
return settings.Router()
307
263
}
264
+
265
+
func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {
266
+
pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.resolver, s.db, s.config)
267
+
return pulls.Router(mw)
268
+
}