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