this repo has no description

appview,knotmirror: use knotmirror to read the repository

Underlying types except the interface hasn't changed much.
Removed `xrpcclient.HandleXrpcErr()` call as appview always expect
knotmirror with compatible API.

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me 0f77faad 1b788e87

verified
+1031 -215
+1
appview/config/config.go
··· 129 Pds PdsConfig `env:",prefix=TANGLED_PDS_"` 130 Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"` 131 Label LabelConfig `env:",prefix=TANGLED_LABEL_"` 132 } 133 134 func LoadConfig(ctx context.Context) (*Config, error) {
··· 129 Pds PdsConfig `env:",prefix=TANGLED_PDS_"` 130 Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"` 131 Label LabelConfig `env:",prefix=TANGLED_LABEL_"` 132 + KnotMirror string `env:"TANGLED_KNOTMIRROR"` 133 } 134 135 func LoadConfig(ctx context.Context) (*Config, error) {
+10 -50
appview/pulls/pulls.go
··· 410 return nil 411 } 412 413 - scheme := "http" 414 - if !s.config.Core.Dev { 415 - scheme = "https" 416 - } 417 - host := fmt.Sprintf("%s://%s", scheme, repo.Knot) 418 - xrpcc := &indigoxrpc.Client{ 419 - Host: host, 420 - } 421 - 422 - resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name)) 423 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 424 return nil 425 } ··· 435 return pages.Unknown 436 } 437 438 - var knot, ownerDid, repoName string 439 - 440 if pull.PullSource.RepoAt != nil { 441 // fork-based pulls 442 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 443 - if err != nil { 444 - log.Println("failed to get source repo", err) 445 - return pages.Unknown 446 - } 447 - 448 - knot = sourceRepo.Knot 449 - ownerDid = sourceRepo.Did 450 - repoName = sourceRepo.Name 451 } else { 452 // pulls within the same repo 453 - knot = repo.Knot 454 - ownerDid = repo.Did 455 - repoName = repo.Name 456 } 457 458 - scheme := "http" 459 - if !s.config.Core.Dev { 460 - scheme = "https" 461 - } 462 - host := fmt.Sprintf("%s://%s", scheme, knot) 463 - xrpcc := &indigoxrpc.Client{ 464 - Host: host, 465 - } 466 - 467 - didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName) 468 - branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName) 469 if err != nil { 470 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 471 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1534 return 1535 } 1536 1537 - scheme := "http" 1538 - if !s.config.Core.Dev { 1539 - scheme = "https" 1540 - } 1541 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1542 - xrpcc := &indigoxrpc.Client{ 1543 - Host: host, 1544 - } 1545 1546 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1547 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1548 if err != nil { 1549 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1550 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1551 - s.pages.Error503(w) 1552 - return 1553 - } 1554 log.Println("failed to fetch branches", err) 1555 return 1556 } 1557
··· 410 return nil 411 } 412 413 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror} 414 + resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoAt().String()) 415 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 416 return nil 417 } ··· 427 return pages.Unknown 428 } 429 430 + var sourceRepo syntax.ATURI 431 if pull.PullSource.RepoAt != nil { 432 // fork-based pulls 433 + sourceRepo = *pull.PullSource.RepoAt 434 } else { 435 // pulls within the same repo 436 + sourceRepo = repo.RepoAt() 437 } 438 439 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror} 440 + branchResp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.String()) 441 if err != nil { 442 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 443 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1506 return 1507 } 1508 1509 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror} 1510 1511 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 1512 if err != nil { 1513 log.Println("failed to fetch branches", err) 1514 + s.pages.Error503(w) 1515 return 1516 } 1517
+9 -19
appview/repo/archive.go
··· 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 ) 12 13 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 20 l.Error("failed to get repo and knot", "err", err) 21 return 22 } 23 - scheme := "http" 24 - if !rp.config.Core.Dev { 25 - scheme = "https" 26 - } 27 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 - didSlashRepo := f.DidSlashRepo() 29 30 // build the xrpc url 31 - u, err := url.Parse(host) 32 - if err != nil { 33 - l.Error("failed to parse host URL", "err", err) 34 - rp.pages.Error503(w) 35 - return 36 - } 37 - 38 - u.Path = "/xrpc/sh.tangled.repo.archive" 39 query := url.Values{} 40 query.Set("format", "tar.gz") 41 query.Set("prefix", r.URL.Query().Get("prefix")) 42 - query.Set("ref", ref) 43 - query.Set("repo", didSlashRepo) 44 - u.RawQuery = query.Encode() 45 - 46 - xrpcURL := u.String() 47 48 // make the get request 49 resp, err := http.Get(xrpcURL)
··· 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/api/tangled" 12 ) 13 14 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 21 l.Error("failed to get repo and knot", "err", err) 22 return 23 } 24 25 // build the xrpc url 26 query := url.Values{} 27 + query.Set("repo", f.RepoAt().String()) 28 + query.Set("ref", ref) 29 query.Set("format", "tar.gz") 30 query.Set("prefix", r.URL.Query().Get("prefix")) 31 + xrpcURL := fmt.Sprintf( 32 + "%s/xrpc/%s?%s", 33 + rp.config.KnotMirror, 34 + tangled.GitTempGetArchiveNSID, 35 + query.Encode(), 36 + ) 37 38 // make the get request 39 resp, err := http.Get(xrpcURL)
+2 -10
appview/repo/artifact.go
··· 313 return nil, err 314 } 315 316 - scheme := "http" 317 - if !rp.config.Core.Dev { 318 - scheme = "https" 319 - } 320 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 321 - xrpcc := &indigoxrpc.Client{ 322 - Host: host, 323 - } 324 325 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 326 - xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 327 if err != nil { 328 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 329 l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
··· 313 return nil, err 314 } 315 316 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 317 318 + xrpcBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, f.RepoAt().String()) 319 if err != nil { 320 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 321 l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+5 -12
appview/repo/branches.go
··· 21 l.Error("failed to get repo and knot", "err", err) 22 return 23 } 24 - scheme := "http" 25 - if !rp.config.Core.Dev { 26 - scheme = "https" 27 - } 28 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 29 - xrpcc := &indigoxrpc.Client{ 30 - Host: host, 31 - } 32 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 33 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 34 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 35 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 36 rp.pages.Error503(w) 37 return 38 }
··· 21 l.Error("failed to get repo and knot", "err", err) 22 return 23 } 24 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 25 + 26 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 27 + if err != nil { 28 + l.Error("failed to call XRPC repo.branches", "err", err) 29 rp.pages.Error503(w) 30 return 31 }
+3 -11
appview/repo/compare.go
··· 27 return 28 } 29 30 - scheme := "http" 31 - if !rp.config.Core.Dev { 32 - scheme = "https" 33 - } 34 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 35 - xrpcc := &indigoxrpc.Client{ 36 - Host: host, 37 - } 38 39 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 40 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 41 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 rp.pages.Error503(w) ··· 74 head = queryHead 75 } 76 77 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 78 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 rp.pages.Error503(w)
··· 27 return 28 } 29 30 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 31 32 + branchBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 33 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 34 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 35 rp.pages.Error503(w) ··· 66 head = queryHead 67 } 68 69 + tagBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 70 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 71 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 72 rp.pages.Error503(w)
+27 -52
appview/repo/index.go
··· 22 "tangled.org/core/appview/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/pages" 25 - "tangled.org/core/appview/xrpcclient" 26 "tangled.org/core/orm" 27 "tangled.org/core/types" 28 ··· 40 if err != nil { 41 l.Error("failed to fully resolve repo", "err", err) 42 return 43 - } 44 - 45 - scheme := "http" 46 - if !rp.config.Core.Dev { 47 - scheme = "https" 48 - } 49 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 50 - xrpcc := &indigoxrpc.Client{ 51 - Host: host, 52 } 53 54 user := rp.oauth.GetMultiAccountUser(r) 55 56 // Build index response from multiple XRPC calls 57 - result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 58 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 59 - if errors.Is(xrpcerr, xrpcclient.ErrXrpcUnsupported) { 60 - l.Error("failed to call XRPC repo.index", "err", err) 61 - rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 62 - LoggedInUser: user, 63 - NeedsKnotUpgrade: true, 64 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 65 - }) 66 - return 67 - } else { 68 - l.Error("failed to build index response", "err", err) 69 - rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 70 - LoggedInUser: user, 71 - KnotUnreachable: true, 72 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 73 - }) 74 - return 75 - } 76 } 77 78 tagMap := make(map[string][]string) ··· 133 } 134 135 // TODO: a bit dirty 136 - languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, xrpcc, result.Ref, ref == "") 137 if err != nil { 138 l.Warn("failed to compute language percentages", "err", err) 139 // non-fatal ··· 169 ctx context.Context, 170 l *slog.Logger, 171 repo *models.Repo, 172 - xrpcc *indigoxrpc.Client, 173 currentRef string, 174 isDefaultRef bool, 175 ) ([]types.RepoLanguageDetails, error) { ··· 182 183 if err != nil || langs == nil { 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) 187 if err != nil { 188 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 189 - l.Error("failed to call XRPC repo.languages", "err", xrpcerr) 190 - return nil, xrpcerr 191 - } 192 - return nil, err 193 } 194 195 if ls == nil || ls.Languages == nil { ··· 258 } 259 260 // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 261 - 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) 263 264 // first get branches to determine the ref if not specified 265 - branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo) 266 if err != nil { 267 - return nil, fmt.Errorf("failed to call repoBranches: %w", err) 268 } 269 270 var branchesResp types.RepoBranchesResponse ··· 296 297 var ( 298 tagsResp types.RepoTagsResponse 299 - treeResp *tangled.RepoTree_Output 300 logResp types.RepoLogResponse 301 readmeContent string 302 readmeFileName string ··· 304 305 // tags 306 wg.Go(func() { 307 - tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo) 308 if err != nil { 309 - errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 310 return 311 } 312 313 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 314 - errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err)) 315 } 316 }) 317 318 // tree/files 319 wg.Go(func() { 320 - resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo) 321 if err != nil { 322 - errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 323 return 324 } 325 treeResp = resp ··· 327 328 // commits 329 wg.Go(func() { 330 - logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo) 331 if err != nil { 332 - errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 333 return 334 } 335 336 if err := json.Unmarshal(logBytes, &logResp); err != nil { 337 - errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err)) 338 } 339 }) 340 ··· 376 Readme: readmeContent, 377 ReadmeFileName: readmeFileName, 378 Commits: logResp.Commits, 379 - Description: logResp.Description, 380 Files: files, 381 Branches: branchesResp.Branches, 382 Tags: tagsResp.Tags,
··· 22 "tangled.org/core/appview/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/orm" 26 "tangled.org/core/types" 27 ··· 39 if err != nil { 40 l.Error("failed to fully resolve repo", "err", err) 41 return 42 } 43 44 user := rp.oauth.GetMultiAccountUser(r) 45 46 // Build index response from multiple XRPC calls 47 + result, err := rp.buildIndexResponse(r.Context(), f, ref) 48 + if err != nil { 49 + l.Error("failed to build index response", "err", err) 50 + rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 51 + LoggedInUser: user, 52 + KnotUnreachable: true, 53 + RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 54 + }) 55 + return 56 } 57 58 tagMap := make(map[string][]string) ··· 113 } 114 115 // TODO: a bit dirty 116 + languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, result.Ref, ref == "") 117 if err != nil { 118 l.Warn("failed to compute language percentages", "err", err) 119 // non-fatal ··· 149 ctx context.Context, 150 l *slog.Logger, 151 repo *models.Repo, 152 currentRef string, 153 isDefaultRef bool, 154 ) ([]types.RepoLanguageDetails, error) { ··· 161 162 if err != nil || langs == nil { 163 // non-fatal, fetch langs from ks via XRPC 164 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 165 + ls, err := tangled.GitTempListLanguages(ctx, xrpcc, currentRef, repo.RepoAt().String()) 166 if err != nil { 167 + return nil, fmt.Errorf("calling knotmirror: %w", err) 168 } 169 170 if ls == nil || ls.Languages == nil { ··· 233 } 234 235 // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 236 + func (rp *Repo) buildIndexResponse(ctx context.Context, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 237 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 238 239 // first get branches to determine the ref if not specified 240 + branchesBytes, err := tangled.GitTempListBranches(ctx, xrpcc, "", 0, repo.RepoAt().String()) 241 if err != nil { 242 + return nil, fmt.Errorf("calling knotmirror git.listBranches: %w", err) 243 } 244 245 var branchesResp types.RepoBranchesResponse ··· 271 272 var ( 273 tagsResp types.RepoTagsResponse 274 + treeResp *tangled.GitTempGetTree_Output 275 logResp types.RepoLogResponse 276 readmeContent string 277 readmeFileName string ··· 279 280 // tags 281 wg.Go(func() { 282 + tagsBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, repo.RepoAt().String()) 283 if err != nil { 284 + errs = errors.Join(errs, fmt.Errorf("failed to call git.ListTags: %w", err)) 285 return 286 } 287 288 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 289 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListTags: %w", err)) 290 } 291 }) 292 293 // tree/files 294 wg.Go(func() { 295 + resp, err := tangled.GitTempGetTree(ctx, xrpcc, "", ref, repo.RepoAt().String()) 296 if err != nil { 297 + errs = errors.Join(errs, fmt.Errorf("failed to call git.GetTree: %w", err)) 298 return 299 } 300 treeResp = resp ··· 302 303 // commits 304 wg.Go(func() { 305 + logBytes, err := tangled.GitTempListCommits(ctx, xrpcc, "", 50, ref, repo.RepoAt().String()) 306 if err != nil { 307 + errs = errors.Join(errs, fmt.Errorf("failed to call git.ListCommits: %w", err)) 308 return 309 } 310 311 if err := json.Unmarshal(logBytes, &logResp); err != nil { 312 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListCommits: %w", err)) 313 } 314 }) 315 ··· 351 Readme: readmeContent, 352 ReadmeFileName: readmeFileName, 353 Commits: logResp.Commits, 354 + Description: "", 355 Files: files, 356 Branches: branchesResp.Branches, 357 Tags: tagsResp.Tags,
+10 -18
appview/repo/log.go
··· 40 ref := chi.URLParam(r, "ref") 41 ref, _ = url.PathUnescape(ref) 42 43 - scheme := "http" 44 - if !rp.config.Core.Dev { 45 - scheme = "https" 46 - } 47 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 48 - xrpcc := &indigoxrpc.Client{ 49 - Host: host, 50 - } 51 52 limit := int64(60) 53 cursor := "" ··· 57 cursor = strconv.Itoa(offset) 58 } 59 60 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 61 - xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 62 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 63 - l.Error("failed to call XRPC repo.log", "err", xrpcerr) 64 rp.pages.Error503(w) 65 return 66 } ··· 72 return 73 } 74 75 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 76 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 77 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 78 rp.pages.Error503(w) 79 return 80 } ··· 93 } 94 } 95 96 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 97 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 98 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 99 rp.pages.Error503(w) 100 return 101 }
··· 40 ref := chi.URLParam(r, "ref") 41 ref, _ = url.PathUnescape(ref) 42 43 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 44 45 limit := int64(60) 46 cursor := "" ··· 50 cursor = strconv.Itoa(offset) 51 } 52 53 + xrpcBytes, err := tangled.GitTempListCommits(r.Context(), xrpcc, cursor, limit, ref, f.RepoAt().String()) 54 + if err != nil { 55 + l.Error("failed to call XRPC repo.log", "err", err) 56 rp.pages.Error503(w) 57 return 58 } ··· 64 return 65 } 66 67 + tagBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 68 + if err != nil { 69 + l.Error("failed to call XRPC repo.tags", "err", err) 70 rp.pages.Error503(w) 71 return 72 } ··· 85 } 86 } 87 88 + branchBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 89 + if err != nil { 90 + l.Error("failed to call XRPC repo.branches", "err", err) 91 rp.pages.Error503(w) 92 return 93 }
+2 -10
appview/repo/settings.go
··· 182 f, err := rp.repoResolver.Resolve(r) 183 user := rp.oauth.GetMultiAccountUser(r) 184 185 - scheme := "http" 186 - if !rp.config.Core.Dev { 187 - scheme = "https" 188 - } 189 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 190 - xrpcc := &indigoxrpc.Client{ 191 - Host: host, 192 - } 193 194 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 195 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 196 var result types.RepoBranchesResponse 197 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 198 l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
··· 182 f, err := rp.repoResolver.Resolve(r) 183 user := rp.oauth.GetMultiAccountUser(r) 184 185 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 186 187 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 188 var result types.RepoBranchesResponse 189 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 190 l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+8 -23
appview/repo/tags.go
··· 27 l.Error("failed to get repo and knot", "err", err) 28 return 29 } 30 - scheme := "http" 31 - if !rp.config.Core.Dev { 32 - scheme = "https" 33 - } 34 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 35 - xrpcc := &indigoxrpc.Client{ 36 - Host: host, 37 - } 38 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 39 - xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 40 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 41 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 42 rp.pages.Error503(w) 43 return 44 } ··· 90 l.Error("failed to get repo and knot", "err", err) 91 return 92 } 93 - scheme := "http" 94 - if !rp.config.Core.Dev { 95 - scheme = "https" 96 - } 97 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 98 - xrpcc := &indigoxrpc.Client{ 99 - Host: host, 100 - } 101 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 102 tag := chi.URLParam(r, "tag") 103 104 - xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, repo, tag) 105 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 106 // if we don't match an existing tag, and the tag we're trying 107 // to match is "latest", resolve to the most recent tag 108 if tag == "latest" { 109 - tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, repo) 110 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 111 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 112 rp.pages.Error503(w)
··· 27 l.Error("failed to get repo and knot", "err", err) 28 return 29 } 30 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 31 + xrpcBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 32 + if err != nil { 33 + l.Error("failed to call XRPC repo.tags", "err", err) 34 rp.pages.Error503(w) 35 return 36 } ··· 82 l.Error("failed to get repo and knot", "err", err) 83 return 84 } 85 tag := chi.URLParam(r, "tag") 86 87 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 88 + 89 + xrpcBytes, err := tangled.GitTempGetTag(r.Context(), xrpcc, f.RepoAt().String(), tag) 90 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 91 // if we don't match an existing tag, and the tag we're trying 92 // to match is "latest", resolve to the most recent tag 93 if tag == "latest" { 94 + tagsBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 1, f.RepoAt().String()) 95 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 96 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 97 rp.pages.Error503(w)
+3 -10
appview/repo/tree.go
··· 33 treePath := chi.URLParam(r, "*") 34 treePath, _ = url.PathUnescape(treePath) 35 treePath = strings.TrimSuffix(treePath, "/") 36 - scheme := "http" 37 - if !rp.config.Core.Dev { 38 - scheme = "https" 39 - } 40 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 41 - xrpcc := &indigoxrpc.Client{ 42 - Host: host, 43 - } 44 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 45 - xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 46 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 47 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 48 rp.pages.Error503(w)
··· 33 treePath := chi.URLParam(r, "*") 34 treePath, _ = url.PathUnescape(treePath) 35 treePath = strings.TrimSuffix(treePath, "/") 36 + 37 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror} 38 + xrpcResp, err := tangled.GitTempGetTree(r.Context(), xrpcc, treePath, ref, f.RepoAt().String()) 39 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 40 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 41 rp.pages.Error503(w)
+11
knotmirror/config/config.go
··· 8 ) 9 10 type Config struct { 11 TapUrl string `env:"MIRROR_TAP_URL, default=http://localhost:2480"` 12 DbPath string `env:"MIRROR_DB_PATH, default=mirror.db"` 13 KnotUseSSL bool `env:"MIRROR_KNOT_USE_SSL, default=false"` // use SSL for Knot when not scheme is not specified ··· 16 GitRepoFetchTimeout time.Duration `env:"MIRROR_GIT_FETCH_TIMEOUT, default=600s"` 17 ResyncParallelism int `env:"MIRROR_RESYNC_PARALLELISM, default=5"` 18 Slurper SlurperConfig `env:",prefix=MIRROR_SLURPER_"` 19 MetricsListen string `env:"MIRROR_METRICS_LISTEN, default=:7100"` 20 AdminListen string `env:"MIRROR_ADMIN_LISTEN, default=:7200"` 21 } 22 23 type SlurperConfig struct {
··· 8 ) 9 10 type Config struct { 11 + PlcUrl string `env:"MIRROR_PLC_URL, default=https://plc.directory"` 12 TapUrl string `env:"MIRROR_TAP_URL, default=http://localhost:2480"` 13 DbPath string `env:"MIRROR_DB_PATH, default=mirror.db"` 14 KnotUseSSL bool `env:"MIRROR_KNOT_USE_SSL, default=false"` // use SSL for Knot when not scheme is not specified ··· 17 GitRepoFetchTimeout time.Duration `env:"MIRROR_GIT_FETCH_TIMEOUT, default=600s"` 18 ResyncParallelism int `env:"MIRROR_RESYNC_PARALLELISM, default=5"` 19 Slurper SlurperConfig `env:",prefix=MIRROR_SLURPER_"` 20 + UseSSL bool `env:"MIRROR_USE_SSL, default=false"` 21 + Hostname string `env:"MIRROR_HOSTNAME, required"` 22 + Listen string `env:"MIRROR_LISTEN, default=:7000"` 23 MetricsListen string `env:"MIRROR_METRICS_LISTEN, default=:7100"` 24 AdminListen string `env:"MIRROR_ADMIN_LISTEN, default=:7200"` 25 + } 26 + 27 + func (c *Config) BaseUrl() string { 28 + if c.UseSSL { 29 + return "https://" + c.Hostname 30 + } 31 + return "http://" + c.Hostname 32 } 33 34 type SlurperConfig struct {
+13
knotmirror/knotmirror.go
··· 8 "time" 9 10 "github.com/prometheus/client_golang/prometheus/promhttp" 11 "tangled.org/core/knotmirror/config" 12 "tangled.org/core/knotmirror/db" 13 "tangled.org/core/knotmirror/knotstream" 14 "tangled.org/core/knotmirror/models" 15 "tangled.org/core/log" 16 ) 17 ··· 32 if err != nil { 33 return fmt.Errorf("initializing db: %w", err) 34 } 35 36 res, err := db.ExecContext(ctx, 37 `update repos set state = $1 where state = $2`, ··· 47 } 48 logger.Info(fmt.Sprintf("clearing resyning states: %d records updated", rows)) 49 50 knotstream := knotstream.NewKnotStream(logger, db, cfg) 51 crawler := NewCrawler(logger, db) 52 resyncer := NewResyncer(logger, db, cfg) ··· 55 // maintain repository list with tap 56 // NOTE: this can be removed once we introduce did-for-repo because then we can just listen to KnotStream for #identity events. 57 tap := NewTapClient(logger, cfg, db, knotstream) 58 59 // start metrics endpoint 60 go func() {
··· 8 "time" 9 10 "github.com/prometheus/client_golang/prometheus/promhttp" 11 + "tangled.org/core/idresolver" 12 "tangled.org/core/knotmirror/config" 13 "tangled.org/core/knotmirror/db" 14 "tangled.org/core/knotmirror/knotstream" 15 "tangled.org/core/knotmirror/models" 16 + "tangled.org/core/knotmirror/xrpc" 17 "tangled.org/core/log" 18 ) 19 ··· 34 if err != nil { 35 return fmt.Errorf("initializing db: %w", err) 36 } 37 + 38 + resolver := idresolver.DefaultResolver(cfg.PlcUrl) 39 40 res, err := db.ExecContext(ctx, 41 `update repos set state = $1 where state = $2`, ··· 51 } 52 logger.Info(fmt.Sprintf("clearing resyning states: %d records updated", rows)) 53 54 + xrpc := xrpc.New(logger, cfg, db, resolver) 55 knotstream := knotstream.NewKnotStream(logger, db, cfg) 56 crawler := NewCrawler(logger, db) 57 resyncer := NewResyncer(logger, db, cfg) ··· 60 // maintain repository list with tap 61 // NOTE: this can be removed once we introduce did-for-repo because then we can just listen to KnotStream for #identity events. 62 tap := NewTapClient(logger, cfg, db, knotstream) 63 + 64 + // start xrpc server 65 + go func() { 66 + logger.Info("starting xrpc server", "addr", cfg.Listen) 67 + if err := http.ListenAndServe(cfg.Listen, xrpc.Router()); err != nil { 68 + logger.Error("xrpc server failed", "error", err) 69 + } 70 + }() 71 72 // start metrics endpoint 73 go func() {
+103
knotmirror/xrpc/git_getArchive.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "compress/gzip" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "strings" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/go-git/go-git/v5/plumbing" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/knotmirror/db" 15 + "tangled.org/core/knotserver/git" 16 + ) 17 + 18 + func (x *Xrpc) GetArchive(w http.ResponseWriter, r *http.Request) { 19 + var ( 20 + repoQuery = r.URL.Query().Get("repo") 21 + ref = r.URL.Query().Get("ref") 22 + format = r.URL.Query().Get("format") 23 + prefix = r.URL.Query().Get("prefix") 24 + ) 25 + 26 + repo, err := syntax.ParseATURI(repoQuery) 27 + if err != nil || repo.RecordKey() == "" { 28 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 29 + return 30 + } 31 + 32 + if format != "tar.gz" { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "only tar.gz format is supported"}) 34 + return 35 + } 36 + if format == "" { 37 + format = "tar.gz" 38 + } 39 + 40 + l := x.logger.With("repo", repo, "ref", ref, "format", format, "prefix", prefix) 41 + ctx := r.Context() 42 + 43 + repoPath, err := x.makeRepoPath(ctx, repo) 44 + if err != nil { 45 + l.Error("failed to resolve repo at-uri", "err", err) 46 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to resolve repo"}) 47 + return 48 + } 49 + 50 + gr, err := git.Open(repoPath, ref) 51 + if err != nil { 52 + l.Error("failed to open git repo", "err", err) 53 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to open git repo"}) 54 + return 55 + } 56 + 57 + repoName, err := func() (string, error) { 58 + r, err := db.GetRepoByAtUri(ctx, x.db, repo) 59 + if err != nil { 60 + return "", err 61 + } 62 + return r.Name, nil 63 + }() 64 + if err != nil { 65 + l.Error("failed to get repo name", "err", err) 66 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to retrieve repo name"}) 67 + return 68 + } 69 + 70 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 71 + immutableLink := func() string { 72 + params := url.Values{} 73 + params.Set("repo", repo.String()) 74 + params.Set("ref", gr.Hash().String()) 75 + params.Set("format", format) 76 + params.Set("prefix", prefix) 77 + return fmt.Sprintf("%s/xrpc/%s?%s", x.cfg.BaseUrl(), tangled.GitTempGetArchiveNSID, params.Encode()) 78 + }() 79 + 80 + filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 81 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 82 + w.Header().Set("Content-Type", "application/gzip") 83 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 84 + 85 + gw := gzip.NewWriter(w) 86 + defer gw.Close() 87 + 88 + if err := gr.WriteTar(gw, prefix); err != nil { 89 + // once we start writing to the body we can't report error anymore 90 + // so we are only left with logging the error 91 + l.Error("writing tar file", "err", err.Error()) 92 + w.WriteHeader(http.StatusInternalServerError) 93 + return 94 + } 95 + 96 + if err := gw.Flush(); err != nil { 97 + // once we start writing to the body we can't report error anymore 98 + // so we are only left with logging the error 99 + l.Error("flushing", "err", err.Error()) 100 + w.WriteHeader(http.StatusInternalServerError) 101 + return 102 + } 103 + }
+85
knotmirror/xrpc/git_getBlob.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "slices" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/go-git/go-git/v5/plumbing/object" 13 + "tangled.org/core/knotserver/git" 14 + ) 15 + 16 + func (x *Xrpc) GetBlob(w http.ResponseWriter, r *http.Request) { 17 + var ( 18 + repoQuery = r.URL.Query().Get("repo") 19 + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 20 + path = r.URL.Query().Get("path") 21 + ) 22 + 23 + repo, err := syntax.ParseATURI(repoQuery) 24 + if err != nil || repo.RecordKey() == "" { 25 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 26 + return 27 + } 28 + 29 + l := x.logger.With("repo", repo, "ref", ref, "path", path) 30 + 31 + if path == "" { 32 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) 33 + return 34 + } 35 + 36 + file, err := x.getFile(r.Context(), repo, ref, path) 37 + if err != nil { 38 + // TODO: better error return 39 + l.Error("failed to get blob", "err", err) 40 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 41 + return 42 + } 43 + 44 + reader, err := file.Reader() 45 + if err != nil { 46 + l.Error("failed to read blob", "err", err) 47 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 48 + } 49 + defer reader.Close() 50 + 51 + w.Header().Set("Content-Type", "application/octet-stream") 52 + if _, err := io.Copy(w, reader); err != nil { 53 + l.Error("failed to serve the blob", "err", err) 54 + } 55 + } 56 + 57 + func (x *Xrpc) getFile(ctx context.Context, repo syntax.ATURI, ref, path string) (*object.File, error) { 58 + repoPath, err := x.makeRepoPath(ctx, repo) 59 + if err != nil { 60 + return nil, fmt.Errorf("resolving repo at-uri: %w", err) 61 + } 62 + 63 + gr, err := git.Open(repoPath, ref) 64 + if err != nil { 65 + return nil, fmt.Errorf("opening git repo: %w", err) 66 + } 67 + 68 + return gr.File(path) 69 + } 70 + 71 + var textualMimeTypes = []string{ 72 + "application/json", 73 + "application/xml", 74 + "application/yaml", 75 + "application/x-yaml", 76 + "application/toml", 77 + "application/javascript", 78 + "application/ecmascript", 79 + } 80 + 81 + // isTextualMimeType returns true if the MIME type represents textual content 82 + // that should be served as text/plain for security reasons 83 + func isTextualMimeType(mimeType string) bool { 84 + return slices.Contains(textualMimeTypes, mimeType) 85 + }
+84
knotmirror/xrpc/git_getBranch.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "tangled.org/core/api/tangled" 13 + "tangled.org/core/knotserver/git" 14 + ) 15 + 16 + // TODO: maybe rename to `sh.tangled.repo.temp.getCommit`? 17 + func (x *Xrpc) GetBranch(w http.ResponseWriter, r *http.Request) { 18 + var ( 19 + repoQuery = r.URL.Query().Get("repo") 20 + nameQuery = r.URL.Query().Get("name") 21 + ) 22 + 23 + repo, err := syntax.ParseATURI(repoQuery) 24 + if err != nil || repo.RecordKey() == "" { 25 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 26 + return 27 + } 28 + 29 + if nameQuery == "" { 30 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing name parameter"}) 31 + return 32 + } 33 + branchName, _ := url.PathUnescape(nameQuery) 34 + 35 + l := x.logger.With("repo", repo, "branch", branchName) 36 + 37 + out, err := x.getBranch(r.Context(), repo, branchName) 38 + if err != nil { 39 + // TODO: better error return 40 + l.Error("failed to get branch", "err", err) 41 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get branch"}) 42 + return 43 + } 44 + writeJson(w, http.StatusOK, out) 45 + } 46 + 47 + func (x *Xrpc) getBranch(ctx context.Context, repo syntax.ATURI, branchName string) (*tangled.GitTempGetBranch_Output, error) { 48 + repoPath, err := x.makeRepoPath(ctx, repo) 49 + if err != nil { 50 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 51 + } 52 + 53 + gr, err := git.PlainOpen(repoPath) 54 + if err != nil { 55 + return nil, fmt.Errorf("failed to open git repo: %w", err) 56 + } 57 + 58 + ref, err := gr.Branch(branchName) 59 + if err != nil { 60 + return nil, fmt.Errorf("getting branch '%s': %w", branchName, err) 61 + } 62 + 63 + commit, err := gr.Commit(ref.Hash()) 64 + if err != nil { 65 + return nil, fmt.Errorf("getting commit '%s': %w", ref.Hash(), err) 66 + } 67 + 68 + out := tangled.GitTempGetBranch_Output{ 69 + Name: ref.Name().Short(), 70 + Hash: ref.Hash().String(), 71 + When: commit.Author.When.Format(time.RFC3339), 72 + Author: &tangled.GitTempDefs_Signature{ 73 + Name: commit.Author.Name, 74 + Email: commit.Author.Email, 75 + When: commit.Author.When.Format(time.RFC3339), 76 + }, 77 + } 78 + 79 + if commit.Message != "" { 80 + out.Message = &commit.Message 81 + } 82 + 83 + return &out, nil 84 + }
+92
knotmirror/xrpc/git_getTag.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/atproto/atclient" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/go-git/go-git/v5/plumbing" 11 + "github.com/go-git/go-git/v5/plumbing/object" 12 + "tangled.org/core/knotserver/git" 13 + "tangled.org/core/types" 14 + ) 15 + 16 + func (x *Xrpc) GetTag(w http.ResponseWriter, r *http.Request) { 17 + var ( 18 + repoQuery = r.URL.Query().Get("repo") 19 + tagName = r.URL.Query().Get("tag") 20 + ) 21 + 22 + repo, err := syntax.ParseATURI(repoQuery) 23 + if err != nil || repo.RecordKey() == "" { 24 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 25 + return 26 + } 27 + 28 + if tagName == "" { 29 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing 'tag' parameter"}) 30 + return 31 + } 32 + 33 + l := x.logger.With("repo", repo, "tag", tagName) 34 + 35 + out, err := x.getTag(r.Context(), repo, tagName) 36 + if err != nil { 37 + // TODO: better error return 38 + l.Error("failed to get tag", "err", err) 39 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tag"}) 40 + return 41 + } 42 + writeJson(w, http.StatusOK, out) 43 + } 44 + 45 + func (x *Xrpc) getTag(ctx context.Context, repo syntax.ATURI, tagName string) (*types.RepoTagResponse, error) { 46 + repoPath, err := x.makeRepoPath(ctx, repo) 47 + if err != nil { 48 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 49 + } 50 + 51 + gr, err := git.PlainOpen(repoPath) 52 + if err != nil { 53 + return nil, fmt.Errorf("failed to open git repo: %w", err) 54 + } 55 + 56 + // if this is not already formatted as refs/tags/v0.1.0, then format it 57 + if !plumbing.ReferenceName(tagName).IsTag() { 58 + tagName = plumbing.NewTagReferenceName(tagName).String() 59 + } 60 + 61 + tag, err := func() (object.Tag, error) { 62 + tags, err := gr.Tags(&git.TagsOptions{ 63 + Pattern: tagName, 64 + }) 65 + if err != nil { 66 + return object.Tag{}, err 67 + } 68 + if len(tags) != 1 { 69 + return object.Tag{}, fmt.Errorf("expected 1 tag to be returned, got %d tags", len(tags)) 70 + } 71 + return tags[0], nil 72 + }() 73 + if err != nil { 74 + return nil, fmt.Errorf("getting tag: %w", err) 75 + } 76 + 77 + var target *object.Tag 78 + if tag.Target != plumbing.ZeroHash { 79 + target = &tag 80 + } 81 + 82 + return &types.RepoTagResponse{ 83 + Tag: &types.TagReference{ 84 + Tag: target, 85 + Reference: types.Reference{ 86 + Name: tag.Name, 87 + Hash: tag.Hash.String(), 88 + }, 89 + Message: tag.Message, 90 + }, 91 + }, nil 92 + }
+118
knotmirror/xrpc/git_getTree.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "path/filepath" 8 + "time" 9 + "unicode/utf8" 10 + 11 + "github.com/bluesky-social/indigo/atproto/atclient" 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/pages/markup" 15 + "tangled.org/core/knotserver/git" 16 + ) 17 + 18 + func (x *Xrpc) GetTree(w http.ResponseWriter, r *http.Request) { 19 + var ( 20 + repoQuery = r.URL.Query().Get("repo") 21 + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 22 + path = r.URL.Query().Get("path") // path can be empty (defaults to root) 23 + ) 24 + 25 + repo, err := syntax.ParseATURI(repoQuery) 26 + if err != nil || repo.RecordKey() == "" { 27 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 28 + return 29 + } 30 + 31 + l := x.logger.With("repo", repo, "ref", ref, "path", path) 32 + 33 + out, err := x.getTree(r.Context(), repo, ref, path) 34 + if err != nil { 35 + // TODO: better error return 36 + l.Error("failed to get tree", "err", err) 37 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tree"}) 38 + return 39 + } 40 + writeJson(w, http.StatusOK, out) 41 + } 42 + 43 + func (x *Xrpc) getTree(ctx context.Context, repo syntax.ATURI, ref, path string) (*tangled.GitTempGetTree_Output, error) { 44 + repoPath, err := x.makeRepoPath(ctx, repo) 45 + if err != nil { 46 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 47 + } 48 + 49 + gr, err := git.Open(repoPath, ref) 50 + if err != nil { 51 + return nil, fmt.Errorf("opening git repo: %w", err) 52 + } 53 + 54 + files, err := gr.FileTree(ctx, path) 55 + if err != nil { 56 + return nil, fmt.Errorf("reading file tree: %w", err) 57 + } 58 + 59 + // if any of these files are a readme candidate, pass along its blob contents too 60 + var readmeFileName string 61 + var readmeContents string 62 + for _, file := range files { 63 + if markup.IsReadmeFile(file.Name) { 64 + contents, err := gr.RawContent(filepath.Join(path, file.Name)) 65 + if err != nil { 66 + x.logger.Error("failed to read contents of file", "path", path, "file", file.Name) 67 + } 68 + 69 + if utf8.Valid(contents) { 70 + readmeFileName = file.Name 71 + readmeContents = string(contents) 72 + break 73 + } 74 + } 75 + } 76 + 77 + // convert NiceTree -> tangled.RepoTempGetTree_TreeEntry 78 + treeEntries := make([]*tangled.GitTempGetTree_TreeEntry, len(files)) 79 + for i, file := range files { 80 + entry := &tangled.GitTempGetTree_TreeEntry{ 81 + Name: file.Name, 82 + Mode: file.Mode, 83 + Size: file.Size, 84 + } 85 + if file.LastCommit != nil { 86 + entry.Last_commit = &tangled.GitTempGetTree_LastCommit{ 87 + Hash: file.LastCommit.Hash.String(), 88 + Message: file.LastCommit.Message, 89 + When: file.LastCommit.When.Format(time.RFC3339), 90 + } 91 + } 92 + treeEntries[i] = entry 93 + } 94 + 95 + var parentPtr *string 96 + if path != "" { 97 + parentPtr = &path 98 + } 99 + 100 + var dotdotPtr *string 101 + if path != "" { 102 + dotdot := filepath.Dir(path) 103 + if dotdot != "." { 104 + dotdotPtr = &dotdot 105 + } 106 + } 107 + 108 + return &tangled.GitTempGetTree_Output{ 109 + Ref: ref, 110 + Parent: parentPtr, 111 + Dotdot: dotdotPtr, 112 + Files: treeEntries, 113 + Readme: &tangled.GitTempGetTree_Readme{ 114 + Filename: readmeFileName, 115 + Contents: readmeContents, 116 + }, 117 + }, nil 118 + }
+95
knotmirror/xrpc/git_listBranches.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "path/filepath" 8 + "strconv" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "tangled.org/core/knotserver/git" 13 + "tangled.org/core/types" 14 + ) 15 + 16 + func (x *Xrpc) ListBranches(w http.ResponseWriter, r *http.Request) { 17 + var ( 18 + repoQuery = r.URL.Query().Get("repo") 19 + limitQuery = r.URL.Query().Get("limit") 20 + cursorQuery = r.URL.Query().Get("cursor") 21 + ) 22 + 23 + repo, err := syntax.ParseATURI(repoQuery) 24 + if err != nil || repo.RecordKey() == "" { 25 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 26 + return 27 + } 28 + 29 + limit := 50 30 + if limitQuery != "" { 31 + limit, err = strconv.Atoi(limitQuery) 32 + if err != nil || limit < 1 || limit > 1000 { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) 34 + return 35 + } 36 + } 37 + 38 + var cursor int64 39 + if cursorQuery != "" { 40 + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) 41 + if err != nil || cursor < 0 { 42 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) 43 + return 44 + } 45 + } 46 + 47 + l := x.logger.With("repo", repoQuery, "limit", limit, "cursor", cursor) 48 + 49 + out, err := x.listBranches(r.Context(), repo, limit, cursor) 50 + if err != nil { 51 + // TODO: better error return 52 + l.Error("failed to list branches", "err", err) 53 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list branches"}) 54 + return 55 + } 56 + writeJson(w, http.StatusOK, out) 57 + } 58 + 59 + func (x *Xrpc) listBranches(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoBranchesResponse, error) { 60 + repoPath, err := x.makeRepoPath(ctx, repo) 61 + if err != nil { 62 + return nil, fmt.Errorf("resolving repo at-uri: %w", err) 63 + } 64 + 65 + gr, err := git.PlainOpen(repoPath) 66 + if err != nil { 67 + return nil, fmt.Errorf("opening git repo: %w", err) 68 + } 69 + 70 + branches, err := gr.Branches(&git.BranchesOptions{ 71 + Limit: limit, 72 + Offset: int(cursor), 73 + }) 74 + if err != nil { 75 + return nil, fmt.Errorf("listing git branches: %w", err) 76 + } 77 + 78 + return &types.RepoBranchesResponse{ 79 + // TODO: include default branch and cursor 80 + Branches: branches, 81 + }, nil 82 + } 83 + 84 + func (x *Xrpc) makeRepoPath(ctx context.Context, repo syntax.ATURI) (string, error) { 85 + id, err := x.resolver.ResolveIdent(ctx, repo.Authority().String()) 86 + if err != nil { 87 + return "", err 88 + } 89 + 90 + return filepath.Join( 91 + x.cfg.GitRepoBasePath, 92 + id.DID.String(), 93 + repo.RecordKey().String(), 94 + ), nil 95 + }
+95
knotmirror/xrpc/git_listCommits.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "strconv" 8 + 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.org/core/knotserver/git" 12 + "tangled.org/core/types" 13 + ) 14 + 15 + func (x *Xrpc) ListCommits(w http.ResponseWriter, r *http.Request) { 16 + var ( 17 + repoQuery = r.URL.Query().Get("repo") 18 + ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 19 + limitQuery = r.URL.Query().Get("limit") 20 + cursorQuery = r.URL.Query().Get("cursor") 21 + ) 22 + 23 + repo, err := syntax.ParseATURI(repoQuery) 24 + if err != nil || repo.RecordKey() == "" { 25 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 26 + return 27 + } 28 + 29 + limit := 50 30 + if limitQuery != "" { 31 + limit, err = strconv.Atoi(limitQuery) 32 + if err != nil || limit < 1 || limit > 1000 { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) 34 + return 35 + } 36 + } 37 + 38 + var cursor int64 39 + if cursorQuery != "" { 40 + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) 41 + if err != nil || cursor < 0 { 42 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) 43 + return 44 + } 45 + } 46 + 47 + l := x.logger.With("repo", repo, "ref", ref) 48 + 49 + out, err := x.listCommits(r.Context(), repo, ref, limit, cursor) 50 + if err != nil { 51 + // TODO: better error return 52 + l.Error("failed to list commits", "err", err) 53 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list commits"}) 54 + return 55 + } 56 + writeJson(w, http.StatusOK, out) 57 + } 58 + 59 + func (x *Xrpc) listCommits(ctx context.Context, repo syntax.ATURI, ref string, limit int, cursor int64) (*types.RepoLogResponse, error) { 60 + repoPath, err := x.makeRepoPath(ctx, repo) 61 + if err != nil { 62 + return nil, fmt.Errorf("resolving repo at-uri: %w", err) 63 + } 64 + 65 + gr, err := git.Open(repoPath, ref) 66 + if err != nil { 67 + return nil, fmt.Errorf("opening git repo: %w", err) 68 + } 69 + 70 + offset := int(cursor) 71 + 72 + commits, err := gr.Commits(offset, limit) 73 + if err != nil { 74 + return nil, fmt.Errorf("listing git commits: %w", err) 75 + } 76 + 77 + tcommits := make([]types.Commit, len(commits)) 78 + for i, c := range commits { 79 + tcommits[i].FromGoGitCommit(c) 80 + } 81 + 82 + total, err := gr.TotalCommits() 83 + if err != nil { 84 + return nil, fmt.Errorf("counting total commits: %w", err) 85 + } 86 + 87 + return &types.RepoLogResponse{ 88 + Commits: tcommits, 89 + Ref: ref, 90 + Page: (offset / limit) + 1, 91 + PerPage: limit, 92 + Total: total, 93 + Log: true, 94 + }, nil 95 + }
+87
knotmirror/xrpc/git_listLanguages.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "math" 7 + "net/http" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "tangled.org/core/api/tangled" 13 + "tangled.org/core/knotserver/git" 14 + ) 15 + 16 + func (x *Xrpc) ListLanguages(w http.ResponseWriter, r *http.Request) { 17 + var ( 18 + repoQuery = r.URL.Query().Get("repo") 19 + ref = r.URL.Query().Get("ref") 20 + ) 21 + l := x.logger.With("repo", repoQuery, "ref", ref) 22 + 23 + repo, err := syntax.ParseATURI(repoQuery) 24 + if err != nil || repo.RecordKey() == "" { 25 + l.Error("invalid repo at-uri", "err", err) 26 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 27 + return 28 + } 29 + 30 + out, err := x.listLanguages(r.Context(), repo, ref) 31 + if err != nil { 32 + // TODO: better error return 33 + l.Error("failed to list languages", "err", err) 34 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list languages"}) 35 + return 36 + } 37 + 38 + writeJson(w, http.StatusOK, out) 39 + } 40 + 41 + func (x *Xrpc) listLanguages(ctx context.Context, repo syntax.ATURI, ref string) (*tangled.GitTempListLanguages_Output, error) { 42 + repoPath, err := x.makeRepoPath(ctx, repo) 43 + if err != nil { 44 + return nil, fmt.Errorf("resolving repo at-uri: %w", err) 45 + } 46 + 47 + gr, err := git.Open(repoPath, ref) 48 + if err != nil { 49 + return nil, fmt.Errorf("opening git repo: %w", err) 50 + } 51 + 52 + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) 53 + defer cancel() 54 + 55 + sizes, err := gr.AnalyzeLanguages(ctx) 56 + if err != nil { 57 + return nil, fmt.Errorf("analyzing languages: %w", err) 58 + } 59 + 60 + return &tangled.GitTempListLanguages_Output{ 61 + Ref: ref, 62 + Languages: sizesToLanguages(sizes), 63 + }, nil 64 + } 65 + 66 + func sizesToLanguages(sizes git.LangBreakdown) []*tangled.GitTempListLanguages_Language { 67 + var apiLanguages []*tangled.GitTempListLanguages_Language 68 + var totalSize int64 69 + for _, size := range sizes { 70 + totalSize += size 71 + } 72 + 73 + for name, size := range sizes { 74 + percentagef64 := float64(size) / float64(totalSize) * 100 75 + percentage := math.Round(percentagef64) 76 + 77 + lang := &tangled.GitTempListLanguages_Language{ 78 + Name: name, 79 + Size: size, 80 + Percentage: int64(percentage), 81 + } 82 + 83 + apiLanguages = append(apiLanguages, lang) 84 + } 85 + 86 + return apiLanguages 87 + }
+94
knotmirror/xrpc/git_listTags.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "strconv" 8 + 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/go-git/go-git/v5/plumbing" 12 + "github.com/go-git/go-git/v5/plumbing/object" 13 + "tangled.org/core/knotserver/git" 14 + "tangled.org/core/types" 15 + ) 16 + 17 + func (x *Xrpc) ListTags(w http.ResponseWriter, r *http.Request) { 18 + var ( 19 + repoQuery = r.URL.Query().Get("repo") 20 + limitQuery = r.URL.Query().Get("limit") 21 + cursorQuery = r.URL.Query().Get("cursor") 22 + ) 23 + 24 + repo, err := syntax.ParseATURI(repoQuery) 25 + if err != nil || repo.RecordKey() == "" { 26 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 27 + } 28 + 29 + limit := 50 30 + if limitQuery != "" { 31 + limit, err = strconv.Atoi(limitQuery) 32 + if err != nil || limit < 1 || limit > 1000 { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) 34 + } 35 + } 36 + 37 + var cursor int64 38 + if cursorQuery != "" { 39 + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) 40 + if err != nil || cursor < 0 { 41 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) 42 + } 43 + } 44 + 45 + l := x.logger.With("repo", repo, "limit", limit, "cursor", cursor) 46 + 47 + out, err := x.listTags(r.Context(), repo, limit, cursor) 48 + if err != nil { 49 + // TODO: better error return 50 + l.Error("failed to list tags", "err", err) 51 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list tags"}) 52 + } 53 + writeJson(w, http.StatusOK, out) 54 + } 55 + 56 + func (x *Xrpc) listTags(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoTagsResponse, error) { 57 + repoPath, err := x.makeRepoPath(ctx, repo) 58 + if err != nil { 59 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 60 + } 61 + 62 + gr, err := git.PlainOpen(repoPath) 63 + if err != nil { 64 + return nil, fmt.Errorf("failed to open git repo: %w", err) 65 + } 66 + 67 + tags, err := gr.Tags(&git.TagsOptions{ 68 + Limit: limit, 69 + Offset: int(cursor), 70 + }) 71 + if err != nil { 72 + return nil, fmt.Errorf("failed to get git tags: %w", err) 73 + } 74 + 75 + rtags := make([]*types.TagReference, len(tags)) 76 + for i, tag := range tags { 77 + var target *object.Tag 78 + if tag.Target != plumbing.ZeroHash { 79 + target = &tag 80 + } 81 + rtags[i] = &types.TagReference{ 82 + Reference: types.Reference{ 83 + Name: tag.Name, 84 + Hash: tag.Hash.String(), 85 + }, 86 + Tag: target, 87 + Message: tag.Message, 88 + } 89 + } 90 + 91 + return &types.RepoTagsResponse{ 92 + Tags: rtags, 93 + }, nil 94 + }
+60
knotmirror/xrpc/xrpc.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "log/slog" 7 + "net/http" 8 + 9 + "github.com/go-chi/chi/v5" 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/idresolver" 12 + "tangled.org/core/knotmirror/config" 13 + ) 14 + 15 + type Xrpc struct { 16 + cfg *config.Config 17 + db *sql.DB 18 + resolver *idresolver.Resolver 19 + logger *slog.Logger 20 + } 21 + 22 + func New(logger *slog.Logger, cfg *config.Config, db *sql.DB, resolver *idresolver.Resolver) *Xrpc { 23 + return &Xrpc{ 24 + cfg, 25 + db, 26 + resolver, 27 + logger, 28 + } 29 + } 30 + 31 + func (x *Xrpc) Router() http.Handler { 32 + r := chi.NewRouter() 33 + 34 + r.Route("/xrpc", func(r chi.Router) { 35 + r.Get("/"+tangled.GitTempGetArchiveNSID, x.GetArchive) 36 + r.Get("/"+tangled.GitTempGetBlobNSID, x.GetBlob) 37 + r.Get("/"+tangled.GitTempGetBranchNSID, x.GetBranch) 38 + // r.Get("/"+tangled.GitTempGetCommitNSID, x.GetCommit) // todo 39 + // r.Get("/"+tangled.GitTempGetDiffNSID, x.GetDiff) // todo 40 + // r.Get("/"+tangled.GitTempGetEntityNSID, x.GetEntity) // todo 41 + // r.Get("/"+tangled.GitTempGetHeadNSID, x.GetHead) // todo 42 + r.Get("/"+tangled.GitTempGetTagNSID, x.GetTag) // using types.Response 43 + r.Get("/"+tangled.GitTempGetTreeNSID, x.GetTree) 44 + r.Get("/"+tangled.GitTempListBranchesNSID, x.ListBranches) // wip, unknown output 45 + r.Get("/"+tangled.GitTempListCommitsNSID, x.ListCommits) 46 + r.Get("/"+tangled.GitTempListLanguagesNSID, x.ListLanguages) 47 + r.Get("/"+tangled.GitTempListTagsNSID, x.ListTags) 48 + }) 49 + 50 + return r 51 + } 52 + 53 + func writeJson(w http.ResponseWriter, status int, response any) error { 54 + w.Header().Set("Content-Type", "application/json") 55 + w.WriteHeader(status) 56 + if err := json.NewEncoder(w).Encode(response); err != nil { 57 + return err 58 + } 59 + return nil 60 + }
+14
knotserver/git/git.go
··· 199 return io.ReadAll(reader) 200 } 201 202 // read and parse .gitmodules 203 func (g *GitRepo) Submodules() (*config.Modules, error) { 204 c, err := g.r.CommitObject(g.h)
··· 199 return io.ReadAll(reader) 200 } 201 202 + func (g *GitRepo) File(path string) (*object.File, error) { 203 + c, err := g.r.CommitObject(g.h) 204 + if err != nil { 205 + return nil, fmt.Errorf("commit object: %w", err) 206 + } 207 + 208 + tree, err := c.Tree() 209 + if err != nil { 210 + return nil, fmt.Errorf("file tree: %w", err) 211 + } 212 + 213 + return tree.File(path) 214 + } 215 + 216 // read and parse .gitmodules 217 func (g *GitRepo) Submodules() (*config.Modules, error) { 218 c, err := g.r.CommitObject(g.h)