Signed-off-by: Lewis lewis@tangled.org
+506
-304
Diff
round #11
+74
-18
appview/ingester.go
+74
-18
appview/ingester.go
···
2
2
3
3
import (
4
4
"context"
5
+
"database/sql"
5
6
"encoding/json"
7
+
"errors"
6
8
"fmt"
7
9
"log/slog"
8
10
"maps"
···
116
118
return err
117
119
}
118
120
119
-
subjectUri, err = syntax.ParseATURI(record.Subject)
120
-
if err != nil {
121
-
l.Error("invalid record", "err", err)
122
-
return err
121
+
star := &models.Star{
122
+
Did: did,
123
+
Rkey: e.Commit.RKey,
123
124
}
124
-
err = db.AddStar(i.Db, &models.Star{
125
-
Did: did,
126
-
RepoAt: subjectUri,
127
-
Rkey: e.Commit.RKey,
128
-
})
125
+
126
+
switch {
127
+
case record.SubjectDid != nil:
128
+
repo, repoErr := db.GetRepo(i.Db, orm.FilterEq("repo_did", *record.SubjectDid))
129
+
if repoErr == nil {
130
+
subjectUri = repo.RepoAt()
131
+
star.RepoAt = subjectUri
132
+
}
133
+
case record.Subject != nil:
134
+
subjectUri, err = syntax.ParseATURI(*record.Subject)
135
+
if err != nil {
136
+
l.Error("invalid record", "err", err)
137
+
return err
138
+
}
139
+
star.RepoAt = subjectUri
140
+
repo, repoErr := db.GetRepoByAtUri(i.Db, subjectUri.String())
141
+
if repoErr == nil && repo.RepoDid != "" {
142
+
if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, *record.Subject); enqErr != nil {
143
+
l.Warn("failed to enqueue PDS rewrite for star", "err", enqErr, "did", did, "repoDid", repo.RepoDid)
144
+
}
145
+
}
146
+
default:
147
+
l.Error("star record has neither subject nor subjectDid")
148
+
return fmt.Errorf("star record has neither subject nor subjectDid")
149
+
}
150
+
err = db.AddStar(i.Db, star)
129
151
case jmodels.CommitOperationDelete:
130
152
err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey)
131
153
}
···
220
242
return err
221
243
}
222
244
223
-
repoAt, err := syntax.ParseATURI(record.Repo)
224
-
if err != nil {
225
-
return err
245
+
var repo *models.Repo
246
+
if record.RepoDid != nil && *record.RepoDid != "" {
247
+
repo, err = db.GetRepoByDid(i.Db, *record.RepoDid)
248
+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
249
+
return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, err)
250
+
}
226
251
}
227
-
228
-
repo, err := db.GetRepoByAtUri(i.Db, repoAt.String())
229
-
if err != nil {
230
-
return err
252
+
if repo == nil && record.Repo != nil {
253
+
repoAt, parseErr := syntax.ParseATURI(*record.Repo)
254
+
if parseErr != nil {
255
+
return parseErr
256
+
}
257
+
repo, err = db.GetRepoByAtUri(i.Db, repoAt.String())
258
+
if err != nil {
259
+
return err
260
+
}
261
+
}
262
+
if repo == nil {
263
+
return fmt.Errorf("artifact record has neither valid repoDid nor repo field")
231
264
}
232
265
233
-
ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push")
266
+
ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.RepoIdentifier(), "repo:push")
234
267
if err != nil || !ok {
235
268
return err
236
269
}
237
270
271
+
repoDid := repo.RepoDid
272
+
if repoDid == "" && record.RepoDid != nil {
273
+
repoDid = *record.RepoDid
274
+
}
275
+
if repoDid != "" && (record.RepoDid == nil || *record.RepoDid == "") && record.Repo != nil {
276
+
if enqErr := db.EnqueuePdsRewrite(i.Db, did, repoDid, tangled.RepoArtifactNSID, e.Commit.RKey, *record.Repo); enqErr != nil {
277
+
l.Warn("failed to enqueue PDS rewrite for artifact", "err", enqErr, "did", did, "repoDid", repoDid)
278
+
}
279
+
}
280
+
238
281
createdAt, err := time.Parse(time.RFC3339, record.CreatedAt)
239
282
if err != nil {
240
283
createdAt = time.Now()
···
243
286
artifact := models.Artifact{
244
287
Did: did,
245
288
Rkey: e.Commit.RKey,
246
-
RepoAt: repoAt,
289
+
RepoAt: repo.RepoAt(),
247
290
Tag: plumbing.Hash(record.Tag),
248
291
CreatedAt: createdAt,
249
292
BlobCid: cid.Cid(record.Artifact.Ref),
···
822
865
823
866
issue := models.IssueFromRecord(did, rkey, record)
824
867
868
+
if issue.RepoAt == "" {
869
+
return fmt.Errorf("issue record has no repo field")
870
+
}
871
+
825
872
if err := i.Validator.ValidateIssue(&issue); err != nil {
826
873
return fmt.Errorf("failed to validate issue: %w", err)
827
874
}
828
875
876
+
if record.Repo != nil {
877
+
repo, repoErr := db.GetRepoByAtUri(i.Db, *record.Repo)
878
+
if repoErr == nil && repo.RepoDid != "" {
879
+
if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, *record.Repo); enqErr != nil {
880
+
l.Warn("failed to enqueue PDS rewrite for issue", "err", enqErr, "did", did, "repoDid", repo.RepoDid)
881
+
}
882
+
}
883
+
}
884
+
829
885
tx, err := ddb.BeginTx(ctx, nil)
830
886
if err != nil {
831
887
l.Error("failed to begin transaction", "err", err)
+2
-2
appview/issues/issues.go
+2
-2
appview/issues/issues.go
···
306
306
return
307
307
}
308
308
309
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
309
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
310
310
isRepoOwner := roles.IsOwner()
311
311
isCollaborator := roles.IsCollaborator()
312
312
isIssueOwner := user.Active.Did == issue.Did
···
354
354
return
355
355
}
356
356
357
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
357
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
358
358
isRepoOwner := roles.IsOwner()
359
359
isCollaborator := roles.IsCollaborator()
360
360
isIssueOwner := user.Active.Did == issue.Did
+9
-5
appview/middleware/middleware.go
+9
-5
appview/middleware/middleware.go
···
17
17
"tangled.org/core/appview/pages"
18
18
"tangled.org/core/appview/pagination"
19
19
"tangled.org/core/appview/reporesolver"
20
+
"tangled.org/core/appview/state/userutil"
20
21
"tangled.org/core/idresolver"
21
22
"tangled.org/core/orm"
22
23
"tangled.org/core/rbac"
···
161
162
return
162
163
}
163
164
164
-
ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
165
+
ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.RepoIdentifier(), requiredPerm)
165
166
if err != nil || !ok {
166
-
log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo())
167
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.RepoIdentifier())
167
168
http.Error(w, "Forbiden", http.StatusUnauthorized)
168
169
return
169
170
}
···
188
189
189
190
id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
190
191
if err != nil {
191
-
// invalid did or handle
192
192
log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err)
193
193
mw.pages.Error404(w)
194
194
return
···
334
334
335
335
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
336
336
if r.URL.Query().Get("go-get") == "1" {
337
+
modulePath := userutil.FlattenDid(fullName)
338
+
if strings.Contains(modulePath, ":") {
339
+
modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name
340
+
}
337
341
html := fmt.Sprintf(
338
342
`<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>
339
343
<meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`,
340
-
fullName, fullName,
341
-
fullName, fullName,
344
+
modulePath, fullName,
345
+
modulePath, fullName,
342
346
)
343
347
w.Header().Set("Content-Type", "text/html")
344
348
w.Write([]byte(html))
+5
-1
appview/models/issue.go
+5
-1
appview/models/issue.go
···
45
45
references[i] = string(uri)
46
46
}
47
47
repoAtStr := i.RepoAt.String()
48
-
return tangled.RepoIssue{
48
+
rec := tangled.RepoIssue{
49
49
Repo: &repoAtStr,
50
50
Title: i.Title,
51
51
Body: &i.Body,
···
53
53
References: references,
54
54
CreatedAt: i.Created.Format(time.RFC3339),
55
55
}
56
+
if i.Repo != nil && i.Repo.RepoDid != "" {
57
+
rec.RepoDid = &i.Repo.RepoDid
58
+
}
59
+
return rec
56
60
}
57
61
58
62
func (i *Issue) State() string {
+37
-42
appview/pulls/pulls.go
+37
-42
appview/pulls/pulls.go
···
406
406
}
407
407
408
408
// user can only delete branch if they are a collaborator in the repo that the branch belongs to
409
-
perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo())
409
+
perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier())
410
410
if !slices.Contains(perms, "repo:push") {
411
411
return nil
412
412
}
···
420
420
Host: host,
421
421
}
422
422
423
-
resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name))
423
+
resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, repo.RepoIdentifier())
424
424
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
425
425
return nil
426
426
}
···
436
436
return pages.Unknown
437
437
}
438
438
439
-
var knot, ownerDid, repoName string
440
-
439
+
var sourceRepo *models.Repo
441
440
if pull.PullSource.RepoAt != nil {
442
-
// fork-based pulls
443
-
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
441
+
var err error
442
+
sourceRepo, err = db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
444
443
if err != nil {
445
444
log.Println("failed to get source repo", err)
446
445
return pages.Unknown
447
446
}
448
-
449
-
knot = sourceRepo.Knot
450
-
ownerDid = sourceRepo.Did
451
-
repoName = sourceRepo.Name
452
447
} else {
453
-
// pulls within the same repo
454
-
knot = repo.Knot
455
-
ownerDid = repo.Did
456
-
repoName = repo.Name
448
+
sourceRepo = repo
457
449
}
458
450
459
451
scheme := "http"
460
452
if !s.config.Core.Dev {
461
453
scheme = "https"
462
454
}
463
-
host := fmt.Sprintf("%s://%s", scheme, knot)
455
+
host := fmt.Sprintf("%s://%s", scheme, sourceRepo.Knot)
464
456
xrpcc := &indigoxrpc.Client{
465
457
Host: host,
466
458
}
467
459
468
-
didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName)
469
-
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName)
460
+
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.RepoIdentifier())
470
461
if err != nil {
471
462
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
472
463
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
913
904
Host: host,
914
905
}
915
906
916
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
917
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
907
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier())
918
908
if err != nil {
919
909
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
920
910
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
963
953
}
964
954
965
955
// Determine PR type based on input parameters
966
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
956
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
967
957
isPushAllowed := roles.IsPushAllowed()
968
958
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
969
959
isForkBased := fromFork != "" && sourceBranch != ""
···
1079
1069
Host: host,
1080
1070
}
1081
1071
1082
-
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
1083
-
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, didSlashRepo, targetBranch, sourceBranch)
1072
+
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch)
1084
1073
if err != nil {
1085
1074
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1086
1075
log.Println("failed to call XRPC repo.compare", xrpcerr)
···
1189
1178
Host: forkHost,
1190
1179
}
1191
1180
1192
-
forkRepoId := fmt.Sprintf("%s/%s", fork.Did, fork.Name)
1193
-
forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, forkRepoId, hiddenRef, sourceBranch)
1181
+
forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch)
1194
1182
if err != nil {
1195
1183
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1196
1184
log.Println("failed to call XRPC repo.compare for fork", xrpcerr)
···
1231
1219
Repo: &forkAtUriStr,
1232
1220
Sha: sourceRev,
1233
1221
}
1222
+
if fork.RepoDid != "" {
1223
+
recordPullSource.RepoDid = &fork.RepoDid
1224
+
}
1234
1225
1235
1226
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1236
1227
}
···
1347
1338
Rkey: rkey,
1348
1339
Record: &lexutil.LexiconTypeDecoder{
1349
1340
Val: &tangled.RepoPull{
1350
-
Title: title,
1351
-
Target: &tangled.RepoPull_Target{
1352
-
Repo: string(repo.RepoAt()),
1353
-
Branch: targetBranch,
1354
-
},
1341
+
Title: title,
1342
+
Target: repoPullTarget(repo, targetBranch),
1355
1343
PatchBlob: blob.Blob,
1356
1344
Source: recordPullSource,
1357
1345
CreatedAt: time.Now().Format(time.RFC3339),
···
1544
1532
Host: host,
1545
1533
}
1546
1534
1547
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1548
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1535
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier())
1549
1536
if err != nil {
1550
1537
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1551
1538
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
1631
1618
Host: sourceHost,
1632
1619
}
1633
1620
1634
-
sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name)
1635
-
sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo)
1621
+
sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, repo.RepoIdentifier())
1636
1622
if err != nil {
1637
1623
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1638
1624
log.Println("failed to call XRPC repo.branches for source", xrpcerr)
···
1660
1646
Host: targetHost,
1661
1647
}
1662
1648
1663
-
targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1664
-
targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo)
1649
+
targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, f.RepoIdentifier())
1665
1650
if err != nil {
1666
1651
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1667
1652
log.Println("failed to call XRPC repo.branches for target", xrpcerr)
···
1771
1756
return
1772
1757
}
1773
1758
1774
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
1759
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
1775
1760
if !roles.IsPushAllowed() {
1776
1761
log.Println("unauthorized user")
1777
1762
w.WriteHeader(http.StatusUnauthorized)
···
1787
1772
Host: host,
1788
1773
}
1789
1774
1790
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1791
-
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch)
1775
+
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch)
1792
1776
if err != nil {
1793
1777
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1794
1778
log.Println("failed to call XRPC repo.compare", xrpcerr)
···
1881
1865
forkScheme = "https"
1882
1866
}
1883
1867
forkHost := fmt.Sprintf("%s://%s", forkScheme, forkRepo.Knot)
1884
-
forkRepoId := fmt.Sprintf("%s/%s", forkRepo.Did, forkRepo.Name)
1885
-
forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepoId, hiddenRef, pull.PullSource.Branch)
1868
+
forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch)
1886
1869
if err != nil {
1887
1870
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1888
1871
log.Println("failed to call XRPC repo.compare for fork", xrpcerr)
···
2360
2343
}
2361
2344
2362
2345
// auth filter: only owner or collaborators can close
2363
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2346
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
2364
2347
isOwner := roles.IsOwner()
2365
2348
isCollaborator := roles.IsCollaborator()
2366
2349
isPullAuthor := user.Active.Did == pull.OwnerDid
···
2434
2417
}
2435
2418
2436
2419
// auth filter: only owner or collaborators can close
2437
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2420
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())}
2438
2421
isOwner := roles.IsOwner()
2439
2422
isCollaborator := roles.IsCollaborator()
2440
2423
isPullAuthor := user.Active.Did == pull.OwnerDid
···
2559
2542
}
2560
2543
2561
2544
func ptrPullState(s models.PullState) *models.PullState { return &s }
2545
+
2546
+
func repoPullTarget(repo *models.Repo, branch string) *tangled.RepoPull_Target {
2547
+
s := string(repo.RepoAt())
2548
+
t := &tangled.RepoPull_Target{
2549
+
Branch: branch,
2550
+
Repo: &s,
2551
+
}
2552
+
if repo.RepoDid != "" {
2553
+
t.RepoDid = &repo.RepoDid
2554
+
}
2555
+
return t
2556
+
}
+2
-2
appview/repo/archive.go
+2
-2
appview/repo/archive.go
···
25
25
scheme = "https"
26
26
}
27
27
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
28
-
didSlashRepo := f.DidSlashRepo()
28
+
didSlashRepo := f.RepoIdentifier()
29
29
30
30
// build the xrpc url
31
31
u, err := url.Parse(host)
···
70
70
if link := resp.Header.Get("Link"); link != "" {
71
71
if resolvedRef, err := extractImmutableLink(link); err == nil {
72
72
newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"",
73
-
rp.config.Core.BaseUrl(), f.DidSlashRepo(), resolvedRef)
73
+
rp.config.Core.BaseUrl(), f.RepoIdentifier(), resolvedRef)
74
74
w.Header().Set("Link", newLink)
75
75
}
76
76
}
+17
-9
appview/repo/artifact.go
+17
-9
appview/repo/artifact.go
···
80
80
Repo: user.Active.Did,
81
81
Rkey: rkey,
82
82
Record: &lexutil.LexiconTypeDecoder{
83
-
Val: &tangled.RepoArtifact{
84
-
Artifact: uploadBlobResp.Blob,
85
-
CreatedAt: createdAt.Format(time.RFC3339),
86
-
Name: header.Filename,
87
-
Repo: f.RepoAt().String(),
88
-
Tag: tag.Tag.Hash[:],
89
-
},
83
+
Val: repoArtifactRecord(f, uploadBlobResp.Blob, createdAt, header.Filename, tag.Tag.Hash[:]),
90
84
},
91
85
})
92
86
if err != nil {
···
322
316
Host: host,
323
317
}
324
318
325
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
326
-
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
319
+
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, f.RepoIdentifier())
327
320
if err != nil {
328
321
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
329
322
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
···
358
351
359
352
return tag, nil
360
353
}
354
+
355
+
func repoArtifactRecord(f *models.Repo, blob *lexutil.LexBlob, createdAt time.Time, name string, tag []byte) *tangled.RepoArtifact {
356
+
rec := &tangled.RepoArtifact{
357
+
Artifact: blob,
358
+
CreatedAt: createdAt.Format(time.RFC3339),
359
+
Name: name,
360
+
Tag: tag,
361
+
}
362
+
s := f.RepoAt().String()
363
+
rec.Repo = &s
364
+
if f.RepoDid != "" {
365
+
rec.RepoDid = &f.RepoDid
366
+
}
367
+
return rec
368
+
}
+3
-4
appview/repo/blob.go
+3
-4
appview/repo/blob.go
···
58
58
xrpcc := &indigoxrpc.Client{
59
59
Host: host,
60
60
}
61
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
62
-
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
61
+
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, f.RepoIdentifier())
63
62
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
64
63
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
65
64
rp.pages.Error503(w)
···
139
138
if !rp.config.Core.Dev {
140
139
scheme = "https"
141
140
}
142
-
repo := f.DidSlashRepo()
141
+
repo := f.RepoIdentifier()
143
142
baseURL := &url.URL{
144
143
Scheme: scheme,
145
144
Host: f.Knot,
···
290
289
scheme = "https"
291
290
}
292
291
293
-
repoName := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
292
+
repoName := repo.RepoIdentifier()
294
293
baseURL := &url.URL{
295
294
Scheme: scheme,
296
295
Host: repo.Knot,
+1
-2
appview/repo/branches.go
+1
-2
appview/repo/branches.go
···
29
29
xrpcc := &indigoxrpc.Client{
30
30
Host: host,
31
31
}
32
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
33
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
32
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier())
34
33
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
35
34
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
36
35
rp.pages.Error503(w)
+7
-7
appview/repo/compare.go
+7
-7
appview/repo/compare.go
···
36
36
Host: host,
37
37
}
38
38
39
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
40
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
39
+
repoId := f.RepoIdentifier()
40
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId)
41
41
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42
42
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
43
43
rp.pages.Error503(w)
···
74
74
head = queryHead
75
75
}
76
76
77
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
77
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId)
78
78
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
79
79
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
80
80
rp.pages.Error503(w)
···
149
149
Host: host,
150
150
}
151
151
152
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
152
+
repoId := f.RepoIdentifier()
153
153
154
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
154
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId)
155
155
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
156
156
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
157
157
rp.pages.Error503(w)
···
165
165
return
166
166
}
167
167
168
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
168
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId)
169
169
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
170
170
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
171
171
rp.pages.Error503(w)
···
179
179
return
180
180
}
181
181
182
-
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
182
+
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repoId, base, head)
183
183
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
184
184
l.Error("failed to call XRPC repo.compare", "err", xrpcerr)
185
185
rp.pages.Error503(w)
+6
-8
appview/repo/index.go
+6
-8
appview/repo/index.go
···
182
182
183
183
if err != nil || langs == nil {
184
184
// non-fatal, fetch langs from ks via XRPC
185
-
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
186
-
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, didSlashRepo)
185
+
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo.RepoIdentifier())
187
186
if err != nil {
188
187
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
189
188
l.Error("failed to call XRPC repo.languages", "err", xrpcerr)
···
259
258
260
259
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
261
260
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) {
262
-
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
261
+
repoId := repo.RepoIdentifier()
263
262
264
-
// first get branches to determine the ref if not specified
265
-
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo)
263
+
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repoId)
266
264
if err != nil {
267
265
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
268
266
}
···
304
302
305
303
// tags
306
304
wg.Go(func() {
307
-
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo)
305
+
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repoId)
308
306
if err != nil {
309
307
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
310
308
return
···
317
315
318
316
// tree/files
319
317
wg.Go(func() {
320
-
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo)
318
+
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repoId)
321
319
if err != nil {
322
320
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
323
321
return
···
327
325
328
326
// commits
329
327
wg.Go(func() {
330
-
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo)
328
+
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repoId)
331
329
if err != nil {
332
330
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
333
331
return
+5
-6
appview/repo/log.go
+5
-6
appview/repo/log.go
···
57
57
cursor = strconv.Itoa(offset)
58
58
}
59
59
60
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
61
-
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
60
+
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, f.RepoIdentifier())
62
61
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
63
62
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
64
63
rp.pages.Error503(w)
···
72
71
return
73
72
}
74
73
75
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
74
+
repoId := f.RepoIdentifier()
75
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId)
76
76
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
77
77
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
78
78
rp.pages.Error503(w)
···
93
93
}
94
94
}
95
95
96
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
96
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId)
97
97
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
98
98
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
99
99
rp.pages.Error503(w)
···
172
172
Host: host,
173
173
}
174
174
175
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
176
-
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
175
+
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, f.RepoIdentifier())
177
176
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
178
177
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
179
178
rp.pages.Error503(w)
+120
-55
appview/repo/repo.go
+120
-55
appview/repo/repo.go
···
35
35
atpclient "github.com/bluesky-social/indigo/atproto/client"
36
36
"github.com/bluesky-social/indigo/atproto/syntax"
37
37
lexutil "github.com/bluesky-social/indigo/lex/util"
38
-
securejoin "github.com/cyphar/filepath-securejoin"
38
+
39
39
"github.com/go-chi/chi/v5"
40
40
)
41
41
···
315
315
return
316
316
}
317
317
318
-
err = db.SubscribeLabel(tx, &models.RepoLabel{
318
+
if err = db.SubscribeLabel(tx, &models.RepoLabel{
319
319
RepoAt: f.RepoAt(),
320
320
LabelAt: label.AtUri(),
321
-
})
321
+
}); err != nil {
322
+
fail("Failed to subscribe to label.", err)
323
+
return
324
+
}
322
325
323
326
err = tx.Commit()
324
327
if err != nil {
···
752
755
Repo: currentUser.Active.Did,
753
756
Rkey: rkey,
754
757
Record: &lexutil.LexiconTypeDecoder{
755
-
Val: &tangled.RepoCollaborator{
756
-
Subject: collaboratorIdent.DID.String(),
757
-
Repo: string(f.RepoAt()),
758
-
CreatedAt: createdAt.Format(time.RFC3339),
759
-
}},
758
+
Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt),
759
+
},
760
760
})
761
761
// invalid record
762
762
if err != nil {
···
791
791
}
792
792
defer rollback()
793
793
794
-
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
794
+
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.RepoIdentifier())
795
795
if err != nil {
796
796
fail("Failed to add collaborator permissions.", err)
797
797
return
···
897
897
}()
898
898
899
899
// remove collaborator RBAC
900
-
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
900
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.RepoIdentifier(), f.Knot)
901
901
if err != nil {
902
902
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
903
903
return
904
904
}
905
905
for _, c := range repoCollaborators {
906
906
did := c[0]
907
-
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
907
+
rp.enforcer.RemoveCollaborator(did, f.Knot, f.RepoIdentifier())
908
908
}
909
909
l.Info("removed collaborators")
910
910
911
911
// remove repo RBAC
912
-
err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.DidSlashRepo())
912
+
err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.RepoIdentifier())
913
913
if err != nil {
914
914
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
915
915
return
···
1064
1064
uri = "http"
1065
1065
}
1066
1066
1067
-
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name)
1067
+
forkSourceUrl := fmt.Sprintf("%s://%s/%s", uri, f.Knot, f.RepoIdentifier())
1068
1068
l = l.With("cloneUrl", forkSourceUrl)
1069
1069
1070
-
sourceAt := f.RepoAt().String()
1071
-
1072
-
// create an atproto record for this fork
1073
1070
rkey := tid.TID()
1071
+
1072
+
// TODO: this could coordinate better with the knot to recieve a clone status
1073
+
client, err := rp.oauth.ServiceClient(
1074
+
r,
1075
+
oauth.WithService(targetKnot),
1076
+
oauth.WithLxm(tangled.RepoCreateNSID),
1077
+
oauth.WithDev(rp.config.Core.Dev),
1078
+
oauth.WithTimeout(time.Second*20),
1079
+
)
1080
+
if err != nil {
1081
+
l.Error("could not create service client", "err", err)
1082
+
rp.pages.Notice(w, "repo", "Failed to connect to knot server.")
1083
+
return
1084
+
}
1085
+
1086
+
forkInput := &tangled.RepoCreate_Input{
1087
+
Rkey: rkey,
1088
+
Name: forkName,
1089
+
Source: &forkSourceUrl,
1090
+
}
1091
+
createResp, createErr := tangled.RepoCreate(
1092
+
r.Context(),
1093
+
client,
1094
+
forkInput,
1095
+
)
1096
+
if err := xrpcclient.HandleXrpcErr(createErr); err != nil {
1097
+
rp.pages.Notice(w, "repo", err.Error())
1098
+
return
1099
+
}
1100
+
1101
+
var repoDid string
1102
+
if createResp != nil && createResp.RepoDid != nil {
1103
+
repoDid = *createResp.RepoDid
1104
+
}
1105
+
if repoDid == "" {
1106
+
l.Error("knot returned empty repo DID for fork")
1107
+
rp.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.")
1108
+
return
1109
+
}
1110
+
1111
+
forkSource := f.RepoAt().String()
1112
+
if f.RepoDid != "" {
1113
+
forkSource = f.RepoDid
1114
+
}
1115
+
1074
1116
repo := &models.Repo{
1075
1117
Did: user.Active.Did,
1076
1118
Name: forkName,
1077
1119
Knot: targetKnot,
1078
1120
Rkey: rkey,
1079
-
Source: sourceAt,
1121
+
Source: forkSource,
1080
1122
Description: f.Description,
1081
1123
Created: time.Now(),
1082
1124
Labels: rp.config.Label.DefaultLabelDefs,
1125
+
RepoDid: repoDid,
1083
1126
}
1084
1127
record := repo.AsRecord()
1085
1128
1129
+
cleanupKnot := func() {
1130
+
go func() {
1131
+
delays := []time.Duration{0, 2 * time.Second, 5 * time.Second}
1132
+
for attempt, delay := range delays {
1133
+
time.Sleep(delay)
1134
+
deleteClient, dErr := rp.oauth.ServiceClient(
1135
+
r,
1136
+
oauth.WithService(targetKnot),
1137
+
oauth.WithLxm(tangled.RepoDeleteNSID),
1138
+
oauth.WithDev(rp.config.Core.Dev),
1139
+
)
1140
+
if dErr != nil {
1141
+
l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr)
1142
+
continue
1143
+
}
1144
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
1145
+
if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{
1146
+
Did: user.Active.Did,
1147
+
Name: forkName,
1148
+
Rkey: rkey,
1149
+
}); dErr != nil {
1150
+
cancel()
1151
+
l.Error("failed to clean up fork on knot after rollback", "attempt", attempt+1, "err", dErr)
1152
+
continue
1153
+
}
1154
+
cancel()
1155
+
l.Info("successfully cleaned up fork on knot after rollback", "attempt", attempt+1)
1156
+
return
1157
+
}
1158
+
l.Error("exhausted retries for knot cleanup, fork may be orphaned",
1159
+
"did", user.Active.Did, "fork", forkName, "knot", targetKnot)
1160
+
}()
1161
+
}
1162
+
1086
1163
atpClient, err := rp.oauth.AuthorizedClient(r)
1087
1164
if err != nil {
1088
1165
l.Error("failed to create xrpcclient", "err", err)
1166
+
cleanupKnot()
1089
1167
rp.pages.Notice(w, "repo", "Failed to fork repository.")
1090
1168
return
1091
1169
}
···
1100
1178
})
1101
1179
if err != nil {
1102
1180
l.Error("failed to write to PDS", "err", err)
1181
+
cleanupKnot()
1103
1182
rp.pages.Notice(w, "repo", "Failed to announce repository creation.")
1104
1183
return
1105
1184
}
···
1115
1194
return
1116
1195
}
1117
1196
1118
-
// The rollback function reverts a few things on failure:
1119
-
// - the pending txn
1120
-
// - the ACLs
1121
-
// - the atproto record created
1122
1197
rollback := func() {
1123
1198
err1 := tx.Rollback()
1124
1199
err2 := rp.enforcer.E.LoadPolicy()
1125
1200
err3 := rollbackRecord(context.Background(), aturi, atpClient)
1126
1201
1127
-
// ignore txn complete errors, this is okay
1128
1202
if errors.Is(err1, sql.ErrTxDone) {
1129
1203
err1 = nil
1130
1204
}
1131
1205
1132
1206
if errs := errors.Join(err1, err2, err3); errs != nil {
1133
1207
l.Error("failed to rollback changes", "errs", errs)
1134
-
return
1135
1208
}
1136
-
}
1137
-
defer rollback()
1138
1209
1139
-
// TODO: this could coordinate better with the knot to recieve a clone status
1140
-
client, err := rp.oauth.ServiceClient(
1141
-
r,
1142
-
oauth.WithService(targetKnot),
1143
-
oauth.WithLxm(tangled.RepoCreateNSID),
1144
-
oauth.WithDev(rp.config.Core.Dev),
1145
-
oauth.WithTimeout(time.Second*20), // big repos take time to clone
1146
-
)
1147
-
if err != nil {
1148
-
l.Error("could not create service client", "err", err)
1149
-
rp.pages.Notice(w, "repo", "Failed to connect to knot server.")
1150
-
return
1151
-
}
1152
-
1153
-
err = tangled.RepoCreate(
1154
-
r.Context(),
1155
-
client,
1156
-
&tangled.RepoCreate_Input{
1157
-
Rkey: rkey,
1158
-
Source: &forkSourceUrl,
1159
-
},
1160
-
)
1161
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
1162
-
rp.pages.Notice(w, "repo", err.Error())
1163
-
return
1210
+
if aturi != "" {
1211
+
cleanupKnot()
1212
+
}
1164
1213
}
1214
+
defer rollback()
1165
1215
1166
1216
err = db.AddRepo(tx, repo)
1167
1217
if err != nil {
···
1170
1220
return
1171
1221
}
1172
1222
1173
-
// acls
1174
-
p, _ := securejoin.SecureJoin(user.Active.Did, forkName)
1175
-
err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p)
1223
+
rbacPath := repo.RepoIdentifier()
1224
+
err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, rbacPath)
1176
1225
if err != nil {
1177
1226
l.Error("failed to add ACLs", "err", err)
1178
1227
rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
1193
1242
return
1194
1243
}
1195
1244
1196
-
// reset the ATURI because the transaction completed successfully
1197
1245
aturi = ""
1198
1246
1199
1247
rp.notifier.NewRepo(r.Context(), repo)
1200
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName))
1248
+
if repoDid != "" {
1249
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid))
1250
+
} else {
1251
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName))
1252
+
}
1201
1253
}
1202
1254
}
1203
1255
···
1222
1274
})
1223
1275
return err
1224
1276
}
1277
+
1278
+
func repoCollaboratorRecord(f *models.Repo, subject string, createdAt time.Time) *tangled.RepoCollaborator {
1279
+
rec := &tangled.RepoCollaborator{
1280
+
Subject: subject,
1281
+
CreatedAt: createdAt.Format(time.RFC3339),
1282
+
}
1283
+
s := string(f.RepoAt())
1284
+
rec.Repo = &s
1285
+
if f.RepoDid != "" {
1286
+
rec.RepoDid = &f.RepoDid
1287
+
}
1288
+
return rec
1289
+
}
+8
-9
appview/repo/settings.go
+8
-9
appview/repo/settings.go
···
293
293
// Skip entirely if there is no active domain claim — the site cannot be served anyway.
294
294
ownerClaim, _ := db.GetActiveDomainClaimForDid(rp.db, f.Did)
295
295
if ownerClaim == nil {
296
-
rp.logger.Info("skipping deploy: no active domain claim", "repo", f.DidSlashRepo())
296
+
rp.logger.Info("skipping deploy: no active domain claim", "repo", f.RepoIdentifier())
297
297
} else if rp.cfClient.Enabled() {
298
298
scheme := "http"
299
299
if !rp.config.Core.Dev {
···
313
313
314
314
deployErr := sites.Deploy(ctx, rp.cfClient, knotHost, f.Did, f.Name, branch, dir)
315
315
if deployErr != nil {
316
-
l.Error("sites: initial R2 sync failed", "repo", f.DidSlashRepo(), "err", deployErr)
316
+
l.Error("sites: initial R2 sync failed", "repo", f.RepoIdentifier(), "err", deployErr)
317
317
deploy.Status = models.SiteDeployStatusFailure
318
318
deploy.Error = deployErr.Error()
319
319
} else {
···
321
321
}
322
322
323
323
if err := db.AddSiteDeploy(rp.db, deploy); err != nil {
324
-
l.Error("sites: failed to record deploy", "repo", f.DidSlashRepo(), "err", err)
324
+
l.Error("sites: failed to record deploy", "repo", f.RepoIdentifier(), "err", err)
325
325
}
326
326
327
327
if deployErr == nil {
328
328
if err := sites.PutDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Did, f.Name, isIndex); err != nil {
329
329
l.Error("sites: KV write failed", "domain", ownerClaim.Domain, "err", err)
330
330
}
331
-
rp.logger.Info("site deployed to r2", "repo", f.DidSlashRepo(), "is_index", isIndex)
331
+
rp.logger.Info("site deployed to r2", "repo", f.RepoIdentifier(), "is_index", isIndex)
332
332
}
333
333
}()
334
334
} else {
335
-
rp.logger.Warn("cloudflare integration is disabled; site won't be deployed", "repo", f.DidSlashRepo())
335
+
rp.logger.Warn("cloudflare integration is disabled; site won't be deployed", "repo", f.RepoIdentifier())
336
336
}
337
337
338
338
rp.pages.HxRefresh(w)
···
367
367
go func() {
368
368
ctx := context.Background()
369
369
if err := sites.Delete(ctx, rp.cfClient, f.Did, f.Name); err != nil {
370
-
l.Error("sites: R2 delete failed", "repo", f.DidSlashRepo(), "err", err)
370
+
l.Error("sites: R2 delete failed", "repo", f.RepoIdentifier(), "err", err)
371
371
}
372
372
if ownerClaim != nil {
373
373
if err := sites.DeleteDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Name); err != nil {
···
395
395
Host: host,
396
396
}
397
397
398
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
399
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
398
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier())
400
399
var result types.RepoBranchesResponse
401
400
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
402
401
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
467
466
user := rp.oauth.GetMultiAccountUser(r)
468
467
469
468
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
470
-
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
469
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.RepoIdentifier(), repo.Knot)
471
470
if err != nil {
472
471
return nil, err
473
472
}
+1
-2
appview/repo/tree.go
+1
-2
appview/repo/tree.go
···
41
41
xrpcc := &indigoxrpc.Client{
42
42
Host: host,
43
43
}
44
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
45
-
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
44
+
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, f.RepoIdentifier())
46
45
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
47
46
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
48
47
rp.pages.Error503(w)
+13
-6
appview/reporesolver/resolver.go
+13
-6
appview/reporesolver/resolver.go
···
36
36
37
37
// NOTE: this... should not even be here. the entire package will be removed in future refactor
38
38
func GetBaseRepoPath(r *http.Request, repo *models.Repo) string {
39
+
if repo.RepoDid != "" {
40
+
return repo.RepoDid
41
+
}
39
42
var (
40
43
user = chi.URLParam(r, "user")
41
44
name = chi.URLParam(r, "repo")
42
45
)
43
46
if user == "" || name == "" {
44
-
return repo.DidSlashRepo()
47
+
return repo.RepoIdentifier()
45
48
}
46
49
return path.Join(user, name)
47
50
}
···
77
80
roles := repoinfo.RolesInRepo{}
78
81
if user != nil && user.Active != nil {
79
82
isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt)
80
-
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo())
83
+
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier())
81
84
}
82
85
83
86
stats := repo.RepoStats
84
87
if stats == nil {
85
-
starCount, err := db.GetStarCount(rr.execer, repoAt)
86
-
if err != nil {
88
+
starCount, starErr := db.GetStarCount(rr.execer, repoAt)
89
+
if starErr != nil {
87
90
log.Println("failed to get star count for ", repoAt)
88
91
}
89
92
issueCount, err := db.GetIssueCount(rr.execer, repoAt)
···
104
107
var sourceRepo *models.Repo
105
108
var err error
106
109
if repo.Source != "" {
107
-
sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source)
110
+
if strings.HasPrefix(repo.Source, "did:") {
111
+
sourceRepo, err = db.GetRepoByDid(rr.execer, repo.Source)
112
+
} else {
113
+
sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source)
114
+
}
108
115
if err != nil {
109
-
log.Println("failed to get repo by at uri", err)
116
+
log.Println("failed to get source repo", err)
110
117
}
111
118
}
112
119
+7
-22
appview/state/git_http.go
+7
-22
appview/state/git_http.go
···
37
37
}
38
38
39
39
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
40
-
user := r.Context().Value("resolvedId").(identity.Identity)
41
40
repo := r.Context().Value("repo").(*models.Repo)
42
41
43
42
scheme := "https"
···
45
44
scheme = "http"
46
45
}
47
46
48
-
// check for the 'service' url param
49
47
service := r.URL.Query().Get("service")
50
48
var contentType string
51
49
switch service {
52
50
case "git-receive-pack":
53
51
contentType = "application/x-git-receive-pack-advertisement"
54
52
default:
55
-
// git-upload-pack is the default service for git-clone / git-fetch.
56
53
contentType = "application/x-git-upload-pack-advertisement"
57
54
}
58
55
59
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
56
+
targetURL := fmt.Sprintf("%s://%s/%s/info/refs?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery)
60
57
s.proxyRequest(w, r, targetURL, contentType)
61
58
}
62
59
63
60
func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
64
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
65
-
if !ok {
66
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
67
-
return
68
-
}
69
61
repo := r.Context().Value("repo").(*models.Repo)
70
62
71
63
scheme := "https"
···
73
65
scheme = "http"
74
66
}
75
67
76
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
68
+
targetURL := fmt.Sprintf("%s://%s/%s/git-upload-archive?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery)
77
69
s.proxyRequest(w, r, targetURL, "application/x-git-upload-archive-result")
78
70
}
79
71
80
72
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
81
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
82
-
if !ok {
83
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
84
-
return
85
-
}
86
73
repo := r.Context().Value("repo").(*models.Repo)
87
74
88
75
scheme := "https"
···
90
77
scheme = "http"
91
78
}
92
79
93
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
80
+
targetURL := fmt.Sprintf("%s://%s/%s/git-upload-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery)
94
81
s.proxyRequest(w, r, targetURL, "application/x-git-upload-pack-result")
95
82
}
96
83
97
84
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
98
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
99
-
if !ok {
100
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
101
-
return
102
-
}
103
85
repo := r.Context().Value("repo").(*models.Repo)
104
86
105
87
scheme := "https"
···
107
89
scheme = "http"
108
90
}
109
91
110
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
92
+
targetURL := fmt.Sprintf("%s://%s/%s/git-receive-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery)
111
93
s.proxyRequest(w, r, targetURL, "application/x-git-receive-pack-result")
112
94
}
113
95
···
123
105
proxyReq.Header = r.Header.Clone()
124
106
125
107
repoOwnerHandle := chi.URLParam(r, "user")
108
+
if id, ok := r.Context().Value("resolvedId").(identity.Identity); ok && !id.Handle.IsInvalidHandle() {
109
+
repoOwnerHandle = id.Handle.String()
110
+
}
126
111
proxyReq.Header.Set("x-tangled-repo-owner-handle", repoOwnerHandle)
127
112
128
113
resp, err := client.Do(proxyReq)
+58
-53
appview/state/knotstream.go
+58
-53
appview/state/knotstream.go
···
2
2
3
3
import (
4
4
"context"
5
+
"database/sql"
5
6
"encoding/json"
6
7
"errors"
7
8
"fmt"
···
66
67
return ec.NewConsumer(cfg), nil
67
68
}
68
69
70
+
func resolveRepo(d *db.DB, repoDid *string, ownerDid, repoName string) (*models.Repo, error) {
71
+
if repoDid != nil && *repoDid != "" {
72
+
return db.GetRepoByDid(d, *repoDid)
73
+
}
74
+
repos, err := db.GetRepos(d, 1, orm.FilterEq("did", ownerDid), orm.FilterEq("name", repoName))
75
+
if err != nil {
76
+
return nil, err
77
+
}
78
+
if len(repos) == 0 {
79
+
return nil, sql.ErrNoRows
80
+
}
81
+
return &repos[0], nil
82
+
}
83
+
69
84
func knotIngester(d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client, notifier notify.Notifier, dev bool, c *config.Config, cfClient *cloudflare.Client) ec.ProcessFunc {
70
85
return func(ctx context.Context, source ec.Source, msg ec.Message) error {
71
86
switch msg.Nsid {
···
96
111
return fmt.Errorf("%s does not belong to %s, something is fishy", record.CommitterDid, source.Key())
97
112
}
98
113
114
+
ownerDid := ""
115
+
if record.OwnerDid != nil {
116
+
ownerDid = *record.OwnerDid
117
+
}
118
+
119
+
repo, lookupErr := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName)
120
+
if lookupErr != nil {
121
+
return fmt.Errorf("failed to look up repo: %w", lookupErr)
122
+
}
123
+
99
124
logger.Info("processing gitRefUpdate event",
100
-
"repo_did", record.RepoDid,
101
-
"repo_name", record.RepoName,
125
+
"repo", repo.RepoIdentifier(),
102
126
"ref", record.Ref,
103
127
"old_sha", record.OldSha,
104
128
"new_sha", record.NewSha)
105
129
106
-
// trigger webhook notifications first (before other ops that might fail)
107
-
var errWebhook error
108
-
repos, err := db.GetRepos(
109
-
d,
110
-
0,
111
-
orm.FilterEq("did", record.RepoDid),
112
-
orm.FilterEq("name", record.RepoName),
113
-
)
114
-
if err != nil {
115
-
errWebhook = fmt.Errorf("failed to lookup repo for webhooks: %w", err)
116
-
} else if len(repos) == 1 {
117
-
notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid)
118
-
}
130
+
notifier.Push(ctx, repo, record.Ref, record.OldSha, record.NewSha, record.CommitterDid)
119
131
120
132
errPunchcard := populatePunchcard(d, record)
121
133
errLanguages := updateRepoLanguages(d, record)
···
133
145
go triggerSitesDeployIfNeeded(ctx, d, cfClient, c, record, source)
134
146
}
135
147
136
-
return errors.Join(errWebhook, errPunchcard, errLanguages, errPosthog)
148
+
return errors.Join(errPunchcard, errLanguages, errPosthog)
137
149
}
138
150
139
151
// triggerSitesDeployIfNeeded checks whether the pushed ref matches the sites
···
147
159
}
148
160
pushedBranch := ref.Short()
149
161
150
-
repos, err := db.GetRepos(
151
-
d,
152
-
0,
153
-
orm.FilterEq("did", record.RepoDid),
154
-
orm.FilterEq("name", record.RepoName),
155
-
)
156
-
if err != nil || len(repos) != 1 {
162
+
ownerDid := ""
163
+
if record.OwnerDid != nil {
164
+
ownerDid = *record.OwnerDid
165
+
}
166
+
167
+
repo, err := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName)
168
+
if err != nil {
157
169
return
158
170
}
159
-
repo := repos[0]
160
171
161
172
siteConfig, err := db.GetRepoSiteConfig(d, repo.RepoAt().String())
162
173
if err != nil || siteConfig == nil {
···
180
191
Trigger: models.SiteDeployTriggerPush,
181
192
}
182
193
183
-
deployErr := sites.Deploy(ctx, cfClient, knotHost, record.RepoDid, record.RepoName, siteConfig.Branch, siteConfig.Dir)
194
+
deployErr := sites.Deploy(ctx, cfClient, knotHost, repo.RepoIdentifier(), record.RepoName, siteConfig.Branch, siteConfig.Dir)
184
195
if deployErr != nil {
185
-
logger.Error("sites: R2 sync failed on push", "repo", record.RepoDid+"/"+record.RepoName, "err", deployErr)
196
+
logger.Error("sites: R2 sync failed on push", "repo", repo.RepoIdentifier(), "err", deployErr)
186
197
deploy.Status = models.SiteDeployStatusFailure
187
198
deploy.Error = deployErr.Error()
188
199
} else {
···
190
201
}
191
202
192
203
if err := db.AddSiteDeploy(d, deploy); err != nil {
193
-
logger.Error("sites: failed to record deploy", "repo", record.RepoDid+"/"+record.RepoName, "err", err)
204
+
logger.Error("sites: failed to record deploy", "repo", repo.RepoIdentifier(), "err", err)
194
205
}
195
206
196
207
if deployErr == nil {
197
-
logger.Info("site deployed to r2", "repo", record.RepoDid+"/"+record.RepoName)
208
+
logger.Info("site deployed to r2", "repo", repo.RepoIdentifier())
198
209
}
199
210
}
200
211
···
236
247
237
248
func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error {
238
249
if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil {
239
-
return fmt.Errorf("empty language data for repo: %s/%s", record.RepoDid, record.RepoName)
250
+
return fmt.Errorf("empty language data for repo: %v/%s", record.OwnerDid, record.RepoName)
240
251
}
241
252
242
-
repos, err := db.GetRepos(
243
-
d,
244
-
0,
245
-
orm.FilterEq("did", record.RepoDid),
246
-
orm.FilterEq("name", record.RepoName),
247
-
)
248
-
if err != nil {
249
-
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
253
+
ownerDid := ""
254
+
if record.OwnerDid != nil {
255
+
ownerDid = *record.OwnerDid
250
256
}
251
-
if len(repos) != 1 {
252
-
return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos))
257
+
258
+
r, lookupErr := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName)
259
+
if lookupErr != nil {
260
+
return fmt.Errorf("failed to look up repo: %w", lookupErr)
253
261
}
254
-
repo := repos[0]
262
+
repo := *r
255
263
256
264
ref := plumbing.ReferenceName(record.Ref)
257
265
if !ref.IsBranch() {
···
304
312
return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey)
305
313
}
306
314
307
-
// does this repo have a spindle configured?
308
-
repos, err := db.GetRepos(
309
-
d,
310
-
0,
311
-
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
312
-
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
313
-
)
314
-
if err != nil {
315
-
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
315
+
repoName := ""
316
+
if record.TriggerMetadata.Repo.Repo != nil {
317
+
repoName = *record.TriggerMetadata.Repo.Repo
316
318
}
317
-
if len(repos) != 1 {
318
-
return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos))
319
+
320
+
repo, lookupErr := resolveRepo(d, record.TriggerMetadata.Repo.RepoDid, record.TriggerMetadata.Repo.Did, repoName)
321
+
if lookupErr != nil {
322
+
return fmt.Errorf("failed to look up repo: %w", lookupErr)
319
323
}
320
-
if repos[0].Spindle == "" {
324
+
if repo.Spindle == "" {
321
325
return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey)
322
326
}
323
327
···
353
357
Rkey: msg.Rkey,
354
358
Knot: source.Key(),
355
359
RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did),
356
-
RepoName: record.TriggerMetadata.Repo.Repo,
360
+
RepoName: repoName,
361
+
RepoDid: repo.RepoDid,
357
362
TriggerId: int(triggerId),
358
363
Sha: sha,
359
364
}
+26
-2
appview/state/router.go
+26
-2
appview/state/router.go
···
1
1
package state
2
2
3
3
import (
4
+
"database/sql"
5
+
"errors"
4
6
"net/http"
5
7
"strings"
6
8
7
9
"github.com/go-chi/chi/v5"
10
+
"tangled.org/core/appview/db"
8
11
"tangled.org/core/appview/issues"
9
12
"tangled.org/core/appview/knots"
10
13
"tangled.org/core/appview/labels"
···
46
49
if len(pathParts) > 0 {
47
50
firstPart := pathParts[0]
48
51
49
-
// if using a DID or handle, just continue as per usual
50
-
if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) {
52
+
if userutil.IsDid(firstPart) {
53
+
repo, err := db.GetRepoByDid(s.db, firstPart)
54
+
switch {
55
+
case err == nil:
56
+
remaining := ""
57
+
if len(pathParts) > 1 {
58
+
remaining = "/" + pathParts[1]
59
+
}
60
+
rewritten := "/" + repo.Did + "/" + repo.Name + remaining
61
+
r2 := r.Clone(r.Context())
62
+
r2.URL.Path = rewritten
63
+
r2.URL.RawPath = rewritten
64
+
userRouter.ServeHTTP(w, r2)
65
+
case errors.Is(err, sql.ErrNoRows):
66
+
userRouter.ServeHTTP(w, r)
67
+
default:
68
+
s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err)
69
+
http.Error(w, "internal server error", http.StatusInternalServerError)
70
+
}
71
+
return
72
+
}
73
+
74
+
if userutil.IsHandle(firstPart) {
51
75
userRouter.ServeHTTP(w, r)
52
76
return
53
77
}
+13
-5
appview/state/star.go
+13
-5
appview/state/star.go
···
12
12
"tangled.org/core/appview/db"
13
13
"tangled.org/core/appview/models"
14
14
"tangled.org/core/appview/pages"
15
+
"tangled.org/core/orm"
15
16
"tangled.org/core/tid"
16
17
)
17
18
···
40
41
case http.MethodPost:
41
42
createdAt := time.Now().Format(time.RFC3339)
42
43
rkey := tid.TID()
44
+
45
+
subjectStr := subjectUri.String()
46
+
starRecord := &tangled.FeedStar{
47
+
CreatedAt: createdAt,
48
+
Subject: &subjectStr,
49
+
}
50
+
repo, err := db.GetRepo(s.db, orm.FilterEq("at_uri", subjectUri.String()))
51
+
if err == nil && repo.RepoDid != "" {
52
+
starRecord.SubjectDid = &repo.RepoDid
53
+
}
54
+
43
55
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
44
56
Collection: tangled.FeedStarNSID,
45
57
Repo: currentUser.Active.Did,
46
58
Rkey: rkey,
47
-
Record: &lexutil.LexiconTypeDecoder{
48
-
Val: &tangled.FeedStar{
49
-
Subject: subjectUri.String(),
50
-
CreatedAt: createdAt,
51
-
}},
59
+
Record: &lexutil.LexiconTypeDecoder{Val: starRecord},
52
60
})
53
61
if err != nil {
54
62
log.Println("failed to create atproto record", err)
+88
-38
appview/state/state.go
+88
-38
appview/state/state.go
···
42
42
"github.com/bluesky-social/indigo/atproto/syntax"
43
43
lexutil "github.com/bluesky-social/indigo/lex/util"
44
44
"github.com/bluesky-social/indigo/xrpc"
45
-
securejoin "github.com/cyphar/filepath-securejoin"
45
+
46
46
"github.com/go-chi/chi/v5"
47
47
"github.com/posthog/posthog-go"
48
48
)
···
456
456
return
457
457
}
458
458
459
-
// create atproto record for this repo
460
459
rkey := tid.TID()
460
+
461
+
client, err := s.oauth.ServiceClient(
462
+
r,
463
+
oauth.WithService(domain),
464
+
oauth.WithLxm(tangled.RepoCreateNSID),
465
+
oauth.WithDev(s.config.Core.Dev),
466
+
)
467
+
if err != nil {
468
+
l.Error("service auth failed", "err", err)
469
+
s.pages.Notice(w, "repo", "Failed to reach knot server.")
470
+
return
471
+
}
472
+
473
+
input := &tangled.RepoCreate_Input{
474
+
Rkey: rkey,
475
+
Name: repoName,
476
+
DefaultBranch: &defaultBranch,
477
+
}
478
+
createResp, xe := tangled.RepoCreate(
479
+
r.Context(),
480
+
client,
481
+
input,
482
+
)
483
+
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
484
+
l.Error("xrpc error", "xe", xe)
485
+
s.pages.Notice(w, "repo", err.Error())
486
+
return
487
+
}
488
+
489
+
var repoDid string
490
+
if createResp != nil && createResp.RepoDid != nil {
491
+
repoDid = *createResp.RepoDid
492
+
}
493
+
if repoDid == "" {
494
+
l.Error("knot returned empty repo DID")
495
+
s.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.")
496
+
return
497
+
}
498
+
461
499
repo := &models.Repo{
462
500
Did: user.Active.Did,
463
501
Name: repoName,
···
466
504
Description: description,
467
505
Created: time.Now(),
468
506
Labels: s.config.Label.DefaultLabelDefs,
507
+
RepoDid: repoDid,
469
508
}
470
509
record := repo.AsRecord()
471
510
511
+
cleanupKnot := func() {
512
+
go func() {
513
+
delays := []time.Duration{0, 2 * time.Second, 5 * time.Second}
514
+
for attempt, delay := range delays {
515
+
time.Sleep(delay)
516
+
deleteClient, dErr := s.oauth.ServiceClient(
517
+
r,
518
+
oauth.WithService(domain),
519
+
oauth.WithLxm(tangled.RepoDeleteNSID),
520
+
oauth.WithDev(s.config.Core.Dev),
521
+
)
522
+
if dErr != nil {
523
+
l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr)
524
+
continue
525
+
}
526
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
527
+
if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{
528
+
Did: user.Active.Did,
529
+
Name: repoName,
530
+
Rkey: rkey,
531
+
}); dErr != nil {
532
+
cancel()
533
+
l.Error("failed to clean up repo on knot after rollback", "attempt", attempt+1, "err", dErr)
534
+
continue
535
+
}
536
+
cancel()
537
+
l.Info("successfully cleaned up repo on knot after rollback", "attempt", attempt+1)
538
+
return
539
+
}
540
+
l.Error("exhausted retries for knot cleanup, repo may be orphaned",
541
+
"did", user.Active.Did, "repo", repoName, "knot", domain)
542
+
}()
543
+
}
544
+
472
545
atpClient, err := s.oauth.AuthorizedClient(r)
473
546
if err != nil {
474
547
l.Info("PDS write failed", "err", err)
548
+
cleanupKnot()
475
549
s.pages.Notice(w, "repo", "Failed to write record to PDS.")
476
550
return
477
551
}
···
486
560
})
487
561
if err != nil {
488
562
l.Info("PDS write failed", "err", err)
563
+
cleanupKnot()
489
564
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
490
565
return
491
566
}
···
501
576
return
502
577
}
503
578
504
-
// The rollback function reverts a few things on failure:
505
-
// - the pending txn
506
-
// - the ACLs
507
-
// - the atproto record created
508
579
rollback := func() {
509
580
err1 := tx.Rollback()
510
581
err2 := s.enforcer.E.LoadPolicy()
511
582
err3 := rollbackRecord(context.Background(), aturi, atpClient)
512
583
513
-
// ignore txn complete errors, this is okay
514
584
if errors.Is(err1, sql.ErrTxDone) {
515
585
err1 = nil
516
586
}
517
587
518
588
if errs := errors.Join(err1, err2, err3); errs != nil {
519
589
l.Error("failed to rollback changes", "errs", errs)
520
-
return
521
590
}
522
-
}
523
-
defer rollback()
524
591
525
-
client, err := s.oauth.ServiceClient(
526
-
r,
527
-
oauth.WithService(domain),
528
-
oauth.WithLxm(tangled.RepoCreateNSID),
529
-
oauth.WithDev(s.config.Core.Dev),
530
-
)
531
-
if err != nil {
532
-
l.Error("service auth failed", "err", err)
533
-
s.pages.Notice(w, "repo", "Failed to reach PDS.")
534
-
return
535
-
}
536
-
537
-
xe := tangled.RepoCreate(
538
-
r.Context(),
539
-
client,
540
-
&tangled.RepoCreate_Input{
541
-
Rkey: rkey,
542
-
},
543
-
)
544
-
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
545
-
l.Error("xrpc error", "xe", xe)
546
-
s.pages.Notice(w, "repo", err.Error())
547
-
return
592
+
if aturi != "" {
593
+
cleanupKnot()
594
+
}
548
595
}
596
+
defer rollback()
549
597
550
598
err = db.AddRepo(tx, repo)
551
599
if err != nil {
···
554
602
return
555
603
}
556
604
557
-
// acls
558
-
p, _ := securejoin.SecureJoin(user.Active.Did, repoName)
559
-
err = s.enforcer.AddRepo(user.Active.Did, domain, p)
605
+
rbacPath := repo.RepoIdentifier()
606
+
err = s.enforcer.AddRepo(user.Active.Did, domain, rbacPath)
560
607
if err != nil {
561
608
l.Error("acl setup failed", "err", err)
562
609
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
577
624
return
578
625
}
579
626
580
-
// reset the ATURI because the transaction completed successfully
581
627
aturi = ""
582
628
583
629
s.notifier.NewRepo(r.Context(), repo)
584
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName))
630
+
if repoDid != "" {
631
+
s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid))
632
+
} else {
633
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName))
634
+
}
585
635
}
586
636
}
587
637
+1
-1
appview/validator/label.go
+1
-1
appview/validator/label.go
···
109
109
// validate permissions: only collaborators can apply labels currently
110
110
//
111
111
// TODO: introduce a repo:triage permission
112
-
ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.DidSlashRepo())
112
+
ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.RepoIdentifier())
113
113
if err != nil {
114
114
return fmt.Errorf("failed to enforce permissions: %w", err)
115
115
}
History
12 rounds
2 comments
oyster.cafe
submitted
#11
1 commit
expand
collapse
appview: DID-based routing, state/handler/middleware updates
Signed-off-by: Lewis <lewis@tangled.org>
merge conflicts detected
expand
collapse
expand
collapse
- go.mod:34
- go.sum:339
expand 2 comments
wdym by eventually? I was thinking to keep this in, so that we don't simply error out if someone does decide to be clever and link to their git repo by repoDID, we should render the page anyway to reward them for being clever instead of punishing heh
oyster.cafe
submitted
#10
1 commit
expand
collapse
appview: DID-based routing, state/handler/middleware updates
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#9
1 commit
expand
collapse
appview: DID-based routing, state/handler/middleware updates
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#8
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#7
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#6
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#5
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#4
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#3
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#2
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#1
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
expand 0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
appview: update state, ingester, middleware, and resolver for repo DID
Signed-off-by: Lewis <lewis@tangled.org>
appview/state/state.go:631won't this eventually redirect to/{owner}/{reponame}?