Monorepo for Tangled tangled.org

appview: update state, ingester, middleware, and resolver for repo DID #1140

open opened by oyster.cafe targeting master from oyster.cafe/tangled-core: master
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mgprvt2eon22
+494 -303
Diff #10
+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
··· 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
··· 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))
+30 -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) ··· 1347 1335 Rkey: rkey, 1348 1336 Record: &lexutil.LexiconTypeDecoder{ 1349 1337 Val: &tangled.RepoPull{ 1350 - Title: title, 1351 - Target: &tangled.RepoPull_Target{ 1352 - Repo: string(repo.RepoAt()), 1353 - Branch: targetBranch, 1354 - }, 1338 + Title: title, 1339 + Target: repoPullTarget(repo, targetBranch), 1355 1340 PatchBlob: blob.Blob, 1356 1341 Source: recordPullSource, 1357 1342 CreatedAt: time.Now().Format(time.RFC3339), ··· 1544 1529 Host: host, 1545 1530 } 1546 1531 1547 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1548 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1532 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 1549 1533 if err != nil { 1550 1534 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1551 1535 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1631 1615 Host: sourceHost, 1632 1616 } 1633 1617 1634 - sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name) 1635 - sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo) 1618 + sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, repo.RepoIdentifier()) 1636 1619 if err != nil { 1637 1620 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1638 1621 log.Println("failed to call XRPC repo.branches for source", xrpcerr) ··· 1660 1643 Host: targetHost, 1661 1644 } 1662 1645 1663 - targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1664 - targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo) 1646 + targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, f.RepoIdentifier()) 1665 1647 if err != nil { 1666 1648 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1667 1649 log.Println("failed to call XRPC repo.branches for target", xrpcerr) ··· 1771 1753 return 1772 1754 } 1773 1755 1774 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1756 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 1775 1757 if !roles.IsPushAllowed() { 1776 1758 log.Println("unauthorized user") 1777 1759 w.WriteHeader(http.StatusUnauthorized) ··· 1787 1769 Host: host, 1788 1770 } 1789 1771 1790 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1791 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch) 1772 + xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 1792 1773 if err != nil { 1793 1774 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1794 1775 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1881 1862 forkScheme = "https" 1882 1863 } 1883 1864 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) 1865 + forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 1886 1866 if err != nil { 1887 1867 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1888 1868 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 2360 2340 } 2361 2341 2362 2342 // auth filter: only owner or collaborators can close 2363 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2343 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2364 2344 isOwner := roles.IsOwner() 2365 2345 isCollaborator := roles.IsCollaborator() 2366 2346 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2434 2414 } 2435 2415 2436 2416 // auth filter: only owner or collaborators can close 2437 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2417 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2438 2418 isOwner := roles.IsOwner() 2439 2419 isCollaborator := roles.IsCollaborator() 2440 2420 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2559 2539 } 2560 2540 2561 2541 func ptrPullState(s models.PullState) *models.PullState { return &s } 2542 + 2543 + func repoPullTarget(repo *models.Repo, branch string) *tangled.RepoPull_Target { 2544 + s := string(repo.RepoAt()) 2545 + return &tangled.RepoPull_Target{ 2546 + Branch: branch, 2547 + Repo: &s, 2548 + } 2549 + }
+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 }
+18 -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 + if f.RepoDid != "" { 363 + rec.RepoDid = &f.RepoDid 364 + } else { 365 + s := f.RepoAt().String() 366 + rec.Repo = &s 367 + } 368 + return rec 369 + }
+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
··· 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
··· 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
··· 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
··· 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)
+117 -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 + return rec 1286 + }
+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 }
+3 -5
appview/repo/tags.go
··· 35 35 xrpcc := &indigoxrpc.Client{ 36 36 Host: host, 37 37 } 38 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 39 - xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 38 + xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 40 39 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 41 40 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 42 41 rp.pages.Error503(w) ··· 98 97 xrpcc := &indigoxrpc.Client{ 99 98 Host: host, 100 99 } 101 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 102 100 tag := chi.URLParam(r, "tag") 103 101 104 - xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, repo, tag) 102 + xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, f.RepoIdentifier(), tag) 105 103 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 106 104 // if we don't match an existing tag, and the tag we're trying 107 105 // to match is "latest", resolve to the most recent tag 108 106 if tag == "latest" { 109 - tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, repo) 107 + tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, f.RepoIdentifier()) 110 108 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 111 109 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 112 110 rp.pages.Error503(w)
+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
··· 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
··· 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
··· 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
··· 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 }
+15 -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 + starRecord := &tangled.FeedStar{ 46 + CreatedAt: createdAt, 47 + } 48 + repo, err := db.GetRepo(s.db, orm.FilterEq("at_uri", subjectUri.String())) 49 + repoHasDid := err == nil && repo.RepoDid != "" 50 + if repoHasDid { 51 + starRecord.SubjectDid = &repo.RepoDid 52 + } else { 53 + s := subjectUri.String() 54 + starRecord.Subject = &s 55 + } 56 + 43 57 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 58 Collection: tangled.FeedStarNSID, 45 59 Repo: currentUser.Active.Did, 46 60 Rkey: rkey, 47 - Record: &lexutil.LexiconTypeDecoder{ 48 - Val: &tangled.FeedStar{ 49 - Subject: subjectUri.String(), 50 - CreatedAt: createdAt, 51 - }}, 61 + Record: &lexutil.LexiconTypeDecoder{Val: starRecord}, 52 62 }) 53 63 if err != nil { 54 64 log.Println("failed to create atproto record", err)
+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
··· 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

14 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
no conflicts, ready to merge
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 2 comments

appview/state/state.go:631 won't this eventually redirect to /{owner}/{reponame}?

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

1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments