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
+301 -147
Diff #7
+78 -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 + star.SubjectDid = *record.SubjectDid 129 + repo, repoErr := db.GetRepo(i.Db, orm.FilterEq("repo_did", *record.SubjectDid)) 130 + if repoErr == nil { 131 + subjectUri = repo.RepoAt() 132 + star.RepoAt = subjectUri 133 + } 134 + case record.Subject != nil: 135 + subjectUri, err = syntax.ParseATURI(*record.Subject) 136 + if err != nil { 137 + l.Error("invalid record", "err", err) 138 + return err 139 + } 140 + star.RepoAt = subjectUri 141 + repo, repoErr := db.GetRepoByAtUri(i.Db, subjectUri.String()) 142 + if repoErr == nil && repo.RepoDid != "" { 143 + star.SubjectDid = repo.RepoDid 144 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, *record.Subject); enqErr != nil { 145 + l.Warn("failed to enqueue PDS rewrite for star", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 146 + } 147 + } 148 + default: 149 + l.Error("star record has neither subject nor subjectDid") 150 + return fmt.Errorf("star record has neither subject nor subjectDid") 151 + } 152 + err = db.AddStar(i.Db, star) 129 153 case jmodels.CommitOperationDelete: 130 154 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 131 155 } ··· 220 244 return err 221 245 } 222 246 223 - repoAt, err := syntax.ParseATURI(record.Repo) 224 - if err != nil { 225 - return err 247 + var repo *models.Repo 248 + if record.RepoDid != nil && *record.RepoDid != "" { 249 + repo, err = db.GetRepoByDid(i.Db, *record.RepoDid) 250 + if err != nil && !errors.Is(err, sql.ErrNoRows) { 251 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, err) 252 + } 226 253 } 227 - 228 - repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 229 - if err != nil { 230 - return err 254 + if repo == nil && record.Repo != nil { 255 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 256 + if parseErr != nil { 257 + return parseErr 258 + } 259 + repo, err = db.GetRepoByAtUri(i.Db, repoAt.String()) 260 + if err != nil { 261 + return err 262 + } 263 + } 264 + if repo == nil { 265 + return fmt.Errorf("artifact record has neither valid repoDid nor repo field") 231 266 } 232 267 233 - ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 268 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.RepoIdentifier(), "repo:push") 234 269 if err != nil || !ok { 235 270 return err 236 271 } 237 272 273 + repoDid := repo.RepoDid 274 + if repoDid == "" && record.RepoDid != nil { 275 + repoDid = *record.RepoDid 276 + } 277 + if repoDid != "" && (record.RepoDid == nil || *record.RepoDid == "") && record.Repo != nil { 278 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repoDid, tangled.RepoArtifactNSID, e.Commit.RKey, *record.Repo); enqErr != nil { 279 + l.Warn("failed to enqueue PDS rewrite for artifact", "err", enqErr, "did", did, "repoDid", repoDid) 280 + } 281 + } 282 + 238 283 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 239 284 if err != nil { 240 285 createdAt = time.Now() ··· 243 288 artifact := models.Artifact{ 244 289 Did: did, 245 290 Rkey: e.Commit.RKey, 246 - RepoAt: repoAt, 291 + RepoAt: repo.RepoAt(), 292 + RepoDid: repoDid, 247 293 Tag: plumbing.Hash(record.Tag), 248 294 CreatedAt: createdAt, 249 295 BlobCid: cid.Cid(record.Artifact.Ref), ··· 822 868 823 869 issue := models.IssueFromRecord(did, rkey, record) 824 870 871 + if issue.RepoDid == "" && issue.RepoAt == "" { 872 + return fmt.Errorf("issue record has neither repo nor repoDid") 873 + } 874 + 825 875 if err := i.Validator.ValidateIssue(&issue); err != nil { 826 876 return fmt.Errorf("failed to validate issue: %w", err) 827 877 } 828 878 879 + if issue.RepoDid == "" && record.Repo != nil { 880 + repo, repoErr := db.GetRepoByAtUri(i.Db, *record.Repo) 881 + if repoErr == nil && repo.RepoDid != "" { 882 + issue.RepoDid = repo.RepoDid 883 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, *record.Repo); enqErr != nil { 884 + l.Warn("failed to enqueue PDS rewrite for issue", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 885 + } 886 + } 887 + } 888 + 829 889 tx, err := ddb.BeginTx(ctx, nil) 830 890 if err != nil { 831 891 l.Error("failed to begin transaction", "err", err)
+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))
+14 -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, repo.RepoDid, 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 ··· 114 121 // this is basically a models.Repo 115 122 OwnerDid: ownerId.DID.String(), 116 123 OwnerHandle: ownerId.Handle.String(), 124 + RepoDid: repo.RepoDid, 117 125 Name: repo.Name, 118 126 Rkey: repo.Rkey, 119 127 Description: repo.Description,
+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)
+52 -48
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" ··· 88 89 return err 89 90 } 90 91 92 + if record.RepoDid == nil || *record.RepoDid == "" { 93 + logger.Error("gitRefUpdate missing repoDid, skipping", "repo_name", record.RepoName) 94 + return fmt.Errorf("gitRefUpdate missing repoDid") 95 + } 96 + 91 97 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) 92 98 if err != nil { 93 99 return err ··· 97 103 } 98 104 99 105 logger.Info("processing gitRefUpdate event", 100 - "repo_did", record.RepoDid, 101 - "repo_name", record.RepoName, 106 + "repo_did", *record.RepoDid, 102 107 "ref", record.Ref, 103 108 "old_sha", record.OldSha, 104 109 "new_sha", record.NewSha) 105 110 106 - // trigger webhook notifications first (before other ops that might fail) 107 111 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 { 112 + 113 + repo, lookupErr := db.GetRepoByDid(d, *record.RepoDid) 114 + if lookupErr != nil && !errors.Is(lookupErr, sql.ErrNoRows) { 115 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, lookupErr) 116 + } 117 + 118 + var repos []models.Repo 119 + if lookupErr == nil { 120 + repos = []models.Repo{*repo} 121 + } 122 + 123 + if errWebhook == nil && len(repos) == 1 { 117 124 notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid) 118 125 } 119 126 ··· 147 154 } 148 155 pushedBranch := ref.Short() 149 156 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 { 157 + if record.RepoDid == nil || *record.RepoDid == "" { 158 + return 159 + } 160 + 161 + repo, err := db.GetRepoByDid(d, *record.RepoDid) 162 + if err != nil { 157 163 return 158 164 } 159 - repo := repos[0] 160 165 161 166 siteConfig, err := db.GetRepoSiteConfig(d, repo.RepoAt().String()) 162 167 if err != nil || siteConfig == nil { ··· 180 185 Trigger: models.SiteDeployTriggerPush, 181 186 } 182 187 183 - deployErr := sites.Deploy(ctx, cfClient, knotHost, record.RepoDid, record.RepoName, siteConfig.Branch, siteConfig.Dir) 188 + deployErr := sites.Deploy(ctx, cfClient, knotHost, *record.RepoDid, record.RepoName, siteConfig.Branch, siteConfig.Dir) 184 189 if deployErr != nil { 185 - logger.Error("sites: R2 sync failed on push", "repo", record.RepoDid+"/"+record.RepoName, "err", deployErr) 190 + logger.Error("sites: R2 sync failed on push", "repo", repo.RepoIdentifier(), "err", deployErr) 186 191 deploy.Status = models.SiteDeployStatusFailure 187 192 deploy.Error = deployErr.Error() 188 193 } else { ··· 190 195 } 191 196 192 197 if err := db.AddSiteDeploy(d, deploy); err != nil { 193 - logger.Error("sites: failed to record deploy", "repo", record.RepoDid+"/"+record.RepoName, "err", err) 198 + logger.Error("sites: failed to record deploy", "repo", repo.RepoIdentifier(), "err", err) 194 199 } 195 200 196 201 if deployErr == nil { 197 - logger.Info("site deployed to r2", "repo", record.RepoDid+"/"+record.RepoName) 202 + logger.Info("site deployed to r2", "repo", repo.RepoIdentifier()) 198 203 } 199 204 } 200 205 ··· 236 241 237 242 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 238 243 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) 244 + return fmt.Errorf("empty language data for repo: %v/%s", record.OwnerDid, record.RepoName) 240 245 } 241 246 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) 247 + if record.RepoDid == nil || *record.RepoDid == "" { 248 + return fmt.Errorf("gitRefUpdate missing repoDid for language update") 250 249 } 251 - if len(repos) != 1 { 252 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 250 + 251 + r, lookupErr := db.GetRepoByDid(d, *record.RepoDid) 252 + if lookupErr != nil { 253 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, lookupErr) 253 254 } 254 - repo := repos[0] 255 + repo := *r 255 256 256 257 ref := plumbing.ReferenceName(record.Ref) 257 258 if !ref.IsBranch() { ··· 266 267 267 268 langs = append(langs, models.RepoLanguage{ 268 269 RepoAt: repo.RepoAt(), 270 + RepoDid: repo.RepoDid, 269 271 Ref: ref.Short(), 270 272 IsDefaultRef: record.Meta.IsDefaultRef, 271 273 Language: l.Lang, ··· 304 306 return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 305 307 } 306 308 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) 309 + if record.TriggerMetadata.Repo.RepoDid == nil || *record.TriggerMetadata.Repo.RepoDid == "" { 310 + return fmt.Errorf("pipeline missing repoDid: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 316 311 } 317 - if len(repos) != 1 { 318 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 312 + 313 + repo, lookupErr := db.GetRepoByDid(d, *record.TriggerMetadata.Repo.RepoDid) 314 + if lookupErr != nil { 315 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.TriggerMetadata.Repo.RepoDid, lookupErr) 319 316 } 317 + repos := []models.Repo{*repo} 320 318 if repos[0].Spindle == "" { 321 319 return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 322 320 } ··· 349 347 return fmt.Errorf("failed to add trigger entry: %w", err) 350 348 } 351 349 350 + repoName := "" 351 + if record.TriggerMetadata.Repo.Repo != nil { 352 + repoName = *record.TriggerMetadata.Repo.Repo 353 + } 354 + 352 355 pipeline := models.Pipeline{ 353 356 Rkey: msg.Rkey, 354 357 Knot: source.Key(), 355 358 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 356 - RepoName: record.TriggerMetadata.Repo.Repo, 359 + RepoName: repoName, 360 + RepoDid: repos[0].RepoDid, 357 361 TriggerId: int(triggerId), 358 362 Sha: sha, 359 363 }
+28 -2
appview/state/router.go
··· 1 1 package state 2 2 3 3 import ( 4 + "context" 5 + "database/sql" 6 + "errors" 4 7 "net/http" 5 8 "strings" 6 9 7 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/db" 8 12 "tangled.org/core/appview/issues" 9 13 "tangled.org/core/appview/knots" 10 14 "tangled.org/core/appview/labels" ··· 46 50 if len(pathParts) > 0 { 47 51 firstPart := pathParts[0] 48 52 49 - // if using a DID or handle, just continue as per usual 50 - if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) { 53 + if userutil.IsDid(firstPart) { 54 + repo, err := db.GetRepoByDid(s.db, firstPart) 55 + switch { 56 + case err == nil: 57 + remaining := "" 58 + if len(pathParts) > 1 { 59 + remaining = "/" + pathParts[1] 60 + } 61 + rewritten := "/" + repo.Did + "/" + repo.Name + remaining 62 + r2 := r.Clone(r.Context()) 63 + r2.URL.Path = rewritten 64 + r2.URL.RawPath = rewritten 65 + ctx := context.WithValue(r2.Context(), "repoDidCanonical", true) 66 + userRouter.ServeHTTP(w, r2.WithContext(ctx)) 67 + case errors.Is(err, sql.ErrNoRows): 68 + userRouter.ServeHTTP(w, r) 69 + default: 70 + s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err) 71 + http.Error(w, "internal server error", http.StatusInternalServerError) 72 + } 73 + return 74 + } 75 + 76 + if userutil.IsHandle(firstPart) { 51 77 userRouter.ServeHTTP(w, r) 52 78 return 53 79 }
+20 -7
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) ··· 61 71 RepoAt: subjectUri, 62 72 Rkey: rkey, 63 73 } 74 + if repoHasDid { 75 + star.SubjectDid = repo.RepoDid 76 + } 64 77 65 78 err = db.AddStar(s.db, star) 66 79 if err != nil { ··· 68 81 return 69 82 } 70 83 71 - starCount, err := db.GetStarCount(s.db, subjectUri) 84 + starCount, err := db.GetStarCount(s.db, "", subjectUri) 72 85 if err != nil { 73 86 log.Println("failed to get star count for ", subjectUri) 74 87 } ··· 107 120 // this is not an issue, the firehose event might have already done this 108 121 } 109 122 110 - starCount, err := db.GetStarCount(s.db, subjectUri) 123 + starCount, err := db.GetStarCount(s.db, "", subjectUri) 111 124 if err != nil { 112 125 log.Println("failed to get star count for ", subjectUri) 113 126 return
+92 -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 + if rd := strings.TrimSpace(r.FormValue("repo_did")); rd != "" { 479 + input.RepoDid = &rd 480 + } 481 + 482 + createResp, xe := tangled.RepoCreate( 483 + r.Context(), 484 + client, 485 + input, 486 + ) 487 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 488 + l.Error("xrpc error", "xe", xe) 489 + s.pages.Notice(w, "repo", err.Error()) 490 + return 491 + } 492 + 493 + var repoDid string 494 + if createResp != nil && createResp.RepoDid != nil { 495 + repoDid = *createResp.RepoDid 496 + } 497 + if repoDid == "" { 498 + l.Error("knot returned empty repo DID") 499 + s.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.") 500 + return 501 + } 502 + 461 503 repo := &models.Repo{ 462 504 Did: user.Active.Did, 463 505 Name: repoName, ··· 466 508 Description: description, 467 509 Created: time.Now(), 468 510 Labels: s.config.Label.DefaultLabelDefs, 511 + RepoDid: repoDid, 469 512 } 470 513 record := repo.AsRecord() 471 514 515 + cleanupKnot := func() { 516 + go func() { 517 + delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} 518 + for attempt, delay := range delays { 519 + time.Sleep(delay) 520 + deleteClient, dErr := s.oauth.ServiceClient( 521 + r, 522 + oauth.WithService(domain), 523 + oauth.WithLxm(tangled.RepoDeleteNSID), 524 + oauth.WithDev(s.config.Core.Dev), 525 + ) 526 + if dErr != nil { 527 + l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr) 528 + continue 529 + } 530 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 531 + if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ 532 + Did: user.Active.Did, 533 + Name: repoName, 534 + Rkey: rkey, 535 + }); dErr != nil { 536 + cancel() 537 + l.Error("failed to clean up repo on knot after rollback", "attempt", attempt+1, "err", dErr) 538 + continue 539 + } 540 + cancel() 541 + l.Info("successfully cleaned up repo on knot after rollback", "attempt", attempt+1) 542 + return 543 + } 544 + l.Error("exhausted retries for knot cleanup, repo may be orphaned", 545 + "did", user.Active.Did, "repo", repoName, "knot", domain) 546 + }() 547 + } 548 + 472 549 atpClient, err := s.oauth.AuthorizedClient(r) 473 550 if err != nil { 474 551 l.Info("PDS write failed", "err", err) 552 + cleanupKnot() 475 553 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 476 554 return 477 555 } ··· 486 564 }) 487 565 if err != nil { 488 566 l.Info("PDS write failed", "err", err) 567 + cleanupKnot() 489 568 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 490 569 return 491 570 } ··· 501 580 return 502 581 } 503 582 504 - // The rollback function reverts a few things on failure: 505 - // - the pending txn 506 - // - the ACLs 507 - // - the atproto record created 508 583 rollback := func() { 509 584 err1 := tx.Rollback() 510 585 err2 := s.enforcer.E.LoadPolicy() 511 586 err3 := rollbackRecord(context.Background(), aturi, atpClient) 512 587 513 - // ignore txn complete errors, this is okay 514 588 if errors.Is(err1, sql.ErrTxDone) { 515 589 err1 = nil 516 590 } 517 591 518 592 if errs := errors.Join(err1, err2, err3); errs != nil { 519 593 l.Error("failed to rollback changes", "errs", errs) 520 - return 521 594 } 522 - } 523 - defer rollback() 524 - 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 595 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 596 + if aturi != "" { 597 + cleanupKnot() 598 + } 548 599 } 600 + defer rollback() 549 601 550 602 err = db.AddRepo(tx, repo) 551 603 if err != nil { ··· 554 606 return 555 607 } 556 608 557 - // acls 558 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 559 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 609 + rbacPath := repo.RepoIdentifier() 610 + err = s.enforcer.AddRepo(user.Active.Did, domain, rbacPath) 560 611 if err != nil { 561 612 l.Error("acl setup failed", "err", err) 562 613 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 577 628 return 578 629 } 579 630 580 - // reset the ATURI because the transaction completed successfully 581 631 aturi = "" 582 632 583 633 s.notifier.NewRepo(r.Context(), repo) 584 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 634 + if repoDid != "" { 635 + s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 636 + } else { 637 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 638 + } 585 639 } 586 640 } 587 641
+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