A vibe coded tangled fork which supports pijul.

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>

+1074 -263
+21 -16
appview/config/config.go
··· 46 46 PLCURL string `env:"URL, default=https://plc.directory"` 47 47 } 48 48 49 + type KnotMirrorConfig struct { 50 + Url string `env:"URL, required"` 51 + } 52 + 49 53 type JetstreamConfig struct { 50 54 Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"` 51 55 } ··· 150 154 } 151 155 152 156 type Config struct { 153 - Core CoreConfig `env:",prefix=TANGLED_"` 154 - Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"` 155 - Knotstream ConsumerConfig `env:",prefix=TANGLED_KNOTSTREAM_"` 156 - Spindlestream ConsumerConfig `env:",prefix=TANGLED_SPINDLESTREAM_"` 157 - Resend ResendConfig `env:",prefix=TANGLED_RESEND_"` 158 - Posthog PosthogConfig `env:",prefix=TANGLED_POSTHOG_"` 159 - Camo CamoConfig `env:",prefix=TANGLED_CAMO_"` 160 - Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"` 161 - OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"` 162 - Redis RedisConfig `env:",prefix=TANGLED_REDIS_"` 163 - Plc PlcConfig `env:",prefix=TANGLED_PLC_"` 164 - Pds PdsConfig `env:",prefix=TANGLED_PDS_"` 165 - Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"` 166 - Label LabelConfig `env:",prefix=TANGLED_LABEL_"` 167 - Bluesky BlueskyConfig `env:",prefix=TANGLED_BLUESKY_"` 168 - Sites SitesConfig `env:",prefix=TANGLED_SITES_"` 157 + Core CoreConfig `env:",prefix=TANGLED_"` 158 + Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"` 159 + Knotstream ConsumerConfig `env:",prefix=TANGLED_KNOTSTREAM_"` 160 + Spindlestream ConsumerConfig `env:",prefix=TANGLED_SPINDLESTREAM_"` 161 + Resend ResendConfig `env:",prefix=TANGLED_RESEND_"` 162 + Posthog PosthogConfig `env:",prefix=TANGLED_POSTHOG_"` 163 + Camo CamoConfig `env:",prefix=TANGLED_CAMO_"` 164 + Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"` 165 + OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"` 166 + Redis RedisConfig `env:",prefix=TANGLED_REDIS_"` 167 + Plc PlcConfig `env:",prefix=TANGLED_PLC_"` 168 + Pds PdsConfig `env:",prefix=TANGLED_PDS_"` 169 + Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"` 170 + Label LabelConfig `env:",prefix=TANGLED_LABEL_"` 171 + Bluesky BlueskyConfig `env:",prefix=TANGLED_BLUESKY_"` 172 + Sites SitesConfig `env:",prefix=TANGLED_SITES_"` 173 + KnotMirror KnotMirrorConfig `env:",prefix=TANGLED_KNOTMIRROR_"` 169 174 } 170 175 171 176 func LoadConfig(ctx context.Context) (*Config, error) {
+16 -82
appview/pulls/pulls.go
··· 411 411 return nil 412 412 } 413 413 414 - scheme := "http" 415 - if !s.config.Core.Dev { 416 - scheme = "https" 417 - } 418 - host := fmt.Sprintf("%s://%s", scheme, repo.Knot) 419 - xrpcc := &indigoxrpc.Client{ 420 - Host: host, 421 - } 422 - 423 - resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name)) 414 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 415 + resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoAt().String()) 424 416 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 425 417 return nil 426 418 } ··· 436 428 return pages.Unknown 437 429 } 438 430 439 - var knot, ownerDid, repoName string 440 - 431 + var sourceRepo syntax.ATURI 441 432 if pull.PullSource.RepoAt != nil { 442 433 // fork-based pulls 443 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 444 - if err != nil { 445 - log.Println("failed to get source repo", err) 446 - return pages.Unknown 447 - } 448 - 449 - knot = sourceRepo.Knot 450 - ownerDid = sourceRepo.Did 451 - repoName = sourceRepo.Name 434 + sourceRepo = *pull.PullSource.RepoAt 452 435 } else { 453 436 // pulls within the same repo 454 - knot = repo.Knot 455 - ownerDid = repo.Did 456 - repoName = repo.Name 437 + sourceRepo = repo.RepoAt() 457 438 } 458 439 459 - scheme := "http" 460 - if !s.config.Core.Dev { 461 - scheme = "https" 462 - } 463 - host := fmt.Sprintf("%s://%s", scheme, knot) 464 - xrpcc := &indigoxrpc.Client{ 465 - Host: host, 466 - } 467 - 468 - didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName) 469 - branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName) 440 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 441 + branchResp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.String()) 470 442 if err != nil { 471 443 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 472 444 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 904 876 905 877 switch r.Method { 906 878 case http.MethodGet: 907 - scheme := "http" 908 - if !s.config.Core.Dev { 909 - scheme = "https" 910 - } 911 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 912 - xrpcc := &indigoxrpc.Client{ 913 - Host: host, 914 - } 879 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 915 880 916 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 917 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 881 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 918 882 if err != nil { 919 883 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 920 884 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1535 1499 return 1536 1500 } 1537 1501 1538 - scheme := "http" 1539 - if !s.config.Core.Dev { 1540 - scheme = "https" 1541 - } 1542 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1543 - xrpcc := &indigoxrpc.Client{ 1544 - Host: host, 1545 - } 1502 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 1546 1503 1547 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1548 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1504 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 1549 1505 if err != nil { 1550 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1551 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1552 - s.pages.Error503(w) 1553 - return 1554 - } 1555 1506 log.Println("failed to fetch branches", err) 1507 + s.pages.Error503(w) 1556 1508 return 1557 1509 } 1558 1510 ··· 1607 1559 return 1608 1560 } 1609 1561 1562 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 1563 + 1610 1564 forkVal := r.URL.Query().Get("fork") 1611 1565 repoString := strings.SplitN(forkVal, "/", 2) 1612 1566 forkOwnerDid := repoString[0] ··· 1622 1576 return 1623 1577 } 1624 1578 1625 - sourceScheme := "http" 1626 - if !s.config.Core.Dev { 1627 - sourceScheme = "https" 1628 - } 1629 - sourceHost := fmt.Sprintf("%s://%s", sourceScheme, repo.Knot) 1630 - sourceXrpcc := &indigoxrpc.Client{ 1631 - Host: sourceHost, 1632 - } 1633 - 1634 - sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name) 1635 - sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo) 1579 + sourceXrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, repo.RepoAt().String()) 1636 1580 if err != nil { 1637 1581 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1638 1582 log.Println("failed to call XRPC repo.branches for source", xrpcerr) ··· 1651 1595 return 1652 1596 } 1653 1597 1654 - targetScheme := "http" 1655 - if !s.config.Core.Dev { 1656 - targetScheme = "https" 1657 - } 1658 - targetHost := fmt.Sprintf("%s://%s", targetScheme, f.Knot) 1659 - targetXrpcc := &indigoxrpc.Client{ 1660 - Host: targetHost, 1661 - } 1662 - 1663 - targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1664 - targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo) 1598 + targetXrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 1665 1599 if err != nil { 1666 1600 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1667 1601 log.Println("failed to call XRPC repo.branches for target", xrpcerr)
+9 -19
appview/repo/archive.go
··· 8 8 "strings" 9 9 10 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/api/tangled" 11 12 ) 12 13 13 14 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 20 21 l.Error("failed to get repo and knot", "err", err) 21 22 return 22 23 } 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 24 30 25 // 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 26 query := url.Values{} 27 + query.Set("repo", f.RepoAt().String()) 28 + query.Set("ref", ref) 40 29 query.Set("format", "tar.gz") 41 30 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() 31 + xrpcURL := fmt.Sprintf( 32 + "%s/xrpc/%s?%s", 33 + rp.config.KnotMirror.Url, 34 + tangled.GitTempGetArchiveNSID, 35 + query.Encode(), 36 + ) 47 37 48 38 // make the get request 49 39 resp, err := http.Get(xrpcURL)
+2 -10
appview/repo/artifact.go
··· 313 313 return nil, err 314 314 } 315 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 - } 316 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 324 317 325 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 326 - xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 318 + xrpcBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, f.RepoAt().String()) 327 319 if err != nil { 328 320 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 329 321 l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+5 -12
appview/repo/branches.go
··· 21 21 l.Error("failed to get repo and knot", "err", err) 22 22 return 23 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) 24 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 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) 36 29 rp.pages.Error503(w) 37 30 return 38 31 }
+3 -11
appview/repo/compare.go
··· 27 27 return 28 28 } 29 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 - } 30 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 38 31 39 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 40 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 32 + branchBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 41 33 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 34 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 35 rp.pages.Error503(w) ··· 74 66 head = queryHead 75 67 } 76 68 77 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 69 + tagBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 78 70 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 71 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 72 rp.pages.Error503(w)
+27 -52
appview/repo/index.go
··· 22 22 "tangled.org/core/appview/db" 23 23 "tangled.org/core/appview/models" 24 24 "tangled.org/core/appview/pages" 25 - "tangled.org/core/appview/xrpcclient" 26 25 "tangled.org/core/orm" 27 26 "tangled.org/core/types" 28 27 ··· 40 39 if err != nil { 41 40 l.Error("failed to fully resolve repo", "err", err) 42 41 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 42 } 53 43 54 44 user := rp.oauth.GetMultiAccountUser(r) 55 45 56 46 // 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 - } 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 76 56 } 77 57 78 58 tagMap := make(map[string][]string) ··· 133 113 } 134 114 135 115 // TODO: a bit dirty 136 - languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, xrpcc, result.Ref, ref == "") 116 + languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, result.Ref, ref == "") 137 117 if err != nil { 138 118 l.Warn("failed to compute language percentages", "err", err) 139 119 // non-fatal ··· 169 149 ctx context.Context, 170 150 l *slog.Logger, 171 151 repo *models.Repo, 172 - xrpcc *indigoxrpc.Client, 173 152 currentRef string, 174 153 isDefaultRef bool, 175 154 ) ([]types.RepoLanguageDetails, error) { ··· 182 161 183 162 if err != nil || langs == nil { 184 163 // 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) 164 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 165 + ls, err := tangled.GitTempListLanguages(ctx, xrpcc, currentRef, repo.RepoAt().String()) 187 166 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 167 + return nil, fmt.Errorf("calling knotmirror: %w", err) 193 168 } 194 169 195 170 if ls == nil || ls.Languages == nil { ··· 258 233 } 259 234 260 235 // 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) 236 + func (rp *Repo) buildIndexResponse(ctx context.Context, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 237 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 263 238 264 239 // first get branches to determine the ref if not specified 265 - branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo) 240 + branchesBytes, err := tangled.GitTempListBranches(ctx, xrpcc, "", 0, repo.RepoAt().String()) 266 241 if err != nil { 267 - return nil, fmt.Errorf("failed to call repoBranches: %w", err) 242 + return nil, fmt.Errorf("calling knotmirror git.listBranches: %w", err) 268 243 } 269 244 270 245 var branchesResp types.RepoBranchesResponse ··· 296 271 297 272 var ( 298 273 tagsResp types.RepoTagsResponse 299 - treeResp *tangled.RepoTree_Output 274 + treeResp *tangled.GitTempGetTree_Output 300 275 logResp types.RepoLogResponse 301 276 readmeContent string 302 277 readmeFileName string ··· 304 279 305 280 // tags 306 281 wg.Go(func() { 307 - tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo) 282 + tagsBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, repo.RepoAt().String()) 308 283 if err != nil { 309 - errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 284 + errs = errors.Join(errs, fmt.Errorf("failed to call git.ListTags: %w", err)) 310 285 return 311 286 } 312 287 313 288 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 314 - errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err)) 289 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListTags: %w", err)) 315 290 } 316 291 }) 317 292 318 293 // tree/files 319 294 wg.Go(func() { 320 - resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo) 295 + resp, err := tangled.GitTempGetTree(ctx, xrpcc, "", ref, repo.RepoAt().String()) 321 296 if err != nil { 322 - errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 297 + errs = errors.Join(errs, fmt.Errorf("failed to call git.GetTree: %w", err)) 323 298 return 324 299 } 325 300 treeResp = resp ··· 327 302 328 303 // commits 329 304 wg.Go(func() { 330 - logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo) 305 + logBytes, err := tangled.GitTempListCommits(ctx, xrpcc, "", 50, ref, repo.RepoAt().String()) 331 306 if err != nil { 332 - errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 307 + errs = errors.Join(errs, fmt.Errorf("failed to call git.ListCommits: %w", err)) 333 308 return 334 309 } 335 310 336 311 if err := json.Unmarshal(logBytes, &logResp); err != nil { 337 - errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err)) 312 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListCommits: %w", err)) 338 313 } 339 314 }) 340 315 ··· 376 351 Readme: readmeContent, 377 352 ReadmeFileName: readmeFileName, 378 353 Commits: logResp.Commits, 379 - Description: logResp.Description, 354 + Description: "", 380 355 Files: files, 381 356 Branches: branchesResp.Branches, 382 357 Tags: tagsResp.Tags,
+10 -18
appview/repo/log.go
··· 40 40 ref := chi.URLParam(r, "ref") 41 41 ref, _ = url.PathUnescape(ref) 42 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 - } 43 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 51 44 52 45 limit := int64(60) 53 46 cursor := "" ··· 57 50 cursor = strconv.Itoa(offset) 58 51 } 59 52 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) 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) 64 56 rp.pages.Error503(w) 65 57 return 66 58 } ··· 72 64 return 73 65 } 74 66 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) 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) 78 70 rp.pages.Error503(w) 79 71 return 80 72 } ··· 93 85 } 94 86 } 95 87 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) 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) 99 91 rp.pages.Error503(w) 100 92 return 101 93 }
+2 -10
appview/repo/settings.go
··· 386 386 f, err := rp.repoResolver.Resolve(r) 387 387 user := rp.oauth.GetMultiAccountUser(r) 388 388 389 - scheme := "http" 390 - if !rp.config.Core.Dev { 391 - scheme = "https" 392 - } 393 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 394 - xrpcc := &indigoxrpc.Client{ 395 - Host: host, 396 - } 389 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 397 390 398 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 399 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 391 + xrpcBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoAt().String()) 400 392 var result types.RepoBranchesResponse 401 393 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 402 394 l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+8 -23
appview/repo/tags.go
··· 27 27 l.Error("failed to get repo and knot", "err", err) 28 28 return 29 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) 30 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 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) 42 34 rp.pages.Error503(w) 43 35 return 44 36 } ··· 90 82 l.Error("failed to get repo and knot", "err", err) 91 83 return 92 84 } 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 85 tag := chi.URLParam(r, "tag") 103 86 104 - xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, repo, tag) 87 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 88 + 89 + xrpcBytes, err := tangled.GitTempGetTag(r.Context(), xrpcc, f.RepoAt().String(), tag) 105 90 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 106 91 // if we don't match an existing tag, and the tag we're trying 107 92 // to match is "latest", resolve to the most recent tag 108 93 if tag == "latest" { 109 - tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, repo) 94 + tagsBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 1, f.RepoAt().String()) 110 95 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 111 96 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 112 97 rp.pages.Error503(w)
+3 -10
appview/repo/tree.go
··· 33 33 treePath := chi.URLParam(r, "*") 34 34 treePath, _ = url.PathUnescape(treePath) 35 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) 36 + 37 + xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 38 + xrpcResp, err := tangled.GitTempGetTree(r.Context(), xrpcc, treePath, ref, f.RepoAt().String()) 46 39 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 47 40 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 48 41 rp.pages.Error503(w)
+11
knotmirror/config/config.go
··· 8 8 ) 9 9 10 10 type Config struct { 11 + PlcUrl string `env:"MIRROR_PLC_URL, default=https://plc.directory"` 11 12 TapUrl string `env:"MIRROR_TAP_URL, default=http://localhost:2480"` 12 13 DbUrl string `env:"MIRROR_DB_URL, required"` 13 14 KnotUseSSL bool `env:"MIRROR_KNOT_USE_SSL, default=false"` // use SSL for Knot when not scheme is not specified ··· 16 17 GitRepoFetchTimeout time.Duration `env:"MIRROR_GIT_FETCH_TIMEOUT, default=600s"` 17 18 ResyncParallelism int `env:"MIRROR_RESYNC_PARALLELISM, default=5"` 18 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"` 19 23 MetricsListen string `env:"MIRROR_METRICS_LISTEN, default=127.0.0.1:7100"` 20 24 AdminListen string `env:"MIRROR_ADMIN_LISTEN, default=127.0.0.1:7200"` 25 + } 26 + 27 + func (c *Config) BaseUrl() string { 28 + if c.UseSSL { 29 + return "https://" + c.Hostname 30 + } 31 + return "http://" + c.Hostname 21 32 } 22 33 23 34 type SlurperConfig struct {
+21
knotmirror/knotmirror.go
··· 7 7 _ "net/http/pprof" 8 8 "time" 9 9 10 + "github.com/go-chi/chi/v5" 10 11 "github.com/prometheus/client_golang/prometheus/promhttp" 12 + "tangled.org/core/idresolver" 11 13 "tangled.org/core/knotmirror/config" 12 14 "tangled.org/core/knotmirror/db" 13 15 "tangled.org/core/knotmirror/knotstream" 14 16 "tangled.org/core/knotmirror/models" 17 + "tangled.org/core/knotmirror/xrpc" 15 18 "tangled.org/core/log" 16 19 ) 17 20 ··· 33 36 return fmt.Errorf("initializing db: %w", err) 34 37 } 35 38 39 + resolver := idresolver.DefaultResolver(cfg.PlcUrl) 40 + 36 41 res, err := db.ExecContext(ctx, 37 42 `update repos set state = $1 where state = $2`, 38 43 models.RepoStateDesynchronized, ··· 47 52 } 48 53 logger.Info(fmt.Sprintf("clearing resyning states: %d records updated", rows)) 49 54 55 + xrpc := xrpc.New(logger, cfg, db, resolver) 50 56 knotstream := knotstream.NewKnotStream(logger, db, cfg) 51 57 crawler := NewCrawler(logger, db) 52 58 resyncer := NewResyncer(logger, db, cfg) ··· 55 61 // maintain repository list with tap 56 62 // NOTE: this can be removed once we introduce did-for-repo because then we can just listen to KnotStream for #identity events. 57 63 tap := NewTapClient(logger, cfg, db, knotstream) 64 + 65 + // start http server 66 + go func() { 67 + logger.Info("starting http server", "addr", cfg.Listen) 68 + 69 + mux := chi.NewRouter() 70 + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 71 + w.Write([]byte("Welcome to a knotmirror server.")) 72 + }) 73 + mux.Mount("/xrpc", xrpc.Router()) 74 + 75 + if err := http.ListenAndServe(cfg.Listen, mux); err != nil { 76 + logger.Error("xrpc server failed", "error", err) 77 + } 78 + }() 58 79 59 80 // start metrics endpoint 60 81 go func() {
+106
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 + if r == nil { 63 + return "", fmt.Errorf("repo not found: %s", repo) 64 + } 65 + return r.Name, nil 66 + }() 67 + if err != nil { 68 + l.Error("failed to get repo name", "err", err) 69 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to retrieve repo name"}) 70 + return 71 + } 72 + 73 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 74 + immutableLink := func() string { 75 + params := url.Values{} 76 + params.Set("repo", repo.String()) 77 + params.Set("ref", gr.Hash().String()) 78 + params.Set("format", format) 79 + params.Set("prefix", prefix) 80 + return fmt.Sprintf("%s/xrpc/%s?%s", x.cfg.BaseUrl(), tangled.GitTempGetArchiveNSID, params.Encode()) 81 + }() 82 + 83 + filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 84 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 85 + w.Header().Set("Content-Type", "application/gzip") 86 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 87 + 88 + gw := gzip.NewWriter(w) 89 + defer gw.Close() 90 + 91 + if err := gr.WriteTar(gw, prefix); err != nil { 92 + // once we start writing to the body we can't report error anymore 93 + // so we are only left with logging the error 94 + l.Error("writing tar file", "err", err.Error()) 95 + w.WriteHeader(http.StatusInternalServerError) 96 + return 97 + } 98 + 99 + if err := gw.Flush(); err != nil { 100 + // once we start writing to the body we can't report error anymore 101 + // so we are only left with logging the error 102 + l.Error("flushing", "err", err.Error()) 103 + w.WriteHeader(http.StatusInternalServerError) 104 + return 105 + } 106 + }
+86
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 + return 49 + } 50 + defer reader.Close() 51 + 52 + w.Header().Set("Content-Type", "application/octet-stream") 53 + if _, err := io.Copy(w, reader); err != nil { 54 + l.Error("failed to serve the blob", "err", err) 55 + } 56 + } 57 + 58 + func (x *Xrpc) getFile(ctx context.Context, repo syntax.ATURI, ref, path string) (*object.File, error) { 59 + repoPath, err := x.makeRepoPath(ctx, repo) 60 + if err != nil { 61 + return nil, fmt.Errorf("resolving repo at-uri: %w", err) 62 + } 63 + 64 + gr, err := git.Open(repoPath, ref) 65 + if err != nil { 66 + return nil, fmt.Errorf("opening git repo: %w", err) 67 + } 68 + 69 + return gr.File(path) 70 + } 71 + 72 + var textualMimeTypes = []string{ 73 + "application/json", 74 + "application/xml", 75 + "application/yaml", 76 + "application/x-yaml", 77 + "application/toml", 78 + "application/javascript", 79 + "application/ecmascript", 80 + } 81 + 82 + // isTextualMimeType returns true if the MIME type represents textual content 83 + // that should be served as text/plain for security reasons 84 + func isTextualMimeType(mimeType string) bool { 85 + return slices.Contains(textualMimeTypes, mimeType) 86 + }
+85
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 + // then, we should ensure the given `ref` is valid 18 + func (x *Xrpc) GetBranch(w http.ResponseWriter, r *http.Request) { 19 + var ( 20 + repoQuery = r.URL.Query().Get("repo") 21 + nameQuery = r.URL.Query().Get("name") 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 + return 28 + } 29 + 30 + if nameQuery == "" { 31 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing name parameter"}) 32 + return 33 + } 34 + branchName, _ := url.PathUnescape(nameQuery) 35 + 36 + l := x.logger.With("repo", repo, "branch", branchName) 37 + 38 + out, err := x.getBranch(r.Context(), repo, branchName) 39 + if err != nil { 40 + // TODO: better error return 41 + l.Error("failed to get branch", "err", err) 42 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get branch"}) 43 + return 44 + } 45 + writeJson(w, http.StatusOK, out) 46 + } 47 + 48 + func (x *Xrpc) getBranch(ctx context.Context, repo syntax.ATURI, branchName string) (*tangled.GitTempGetBranch_Output, error) { 49 + repoPath, err := x.makeRepoPath(ctx, repo) 50 + if err != nil { 51 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 52 + } 53 + 54 + gr, err := git.PlainOpen(repoPath) 55 + if err != nil { 56 + return nil, fmt.Errorf("failed to open git repo: %w", err) 57 + } 58 + 59 + ref, err := gr.Branch(branchName) 60 + if err != nil { 61 + return nil, fmt.Errorf("getting branch '%s': %w", branchName, err) 62 + } 63 + 64 + commit, err := gr.Commit(ref.Hash()) 65 + if err != nil { 66 + return nil, fmt.Errorf("getting commit '%s': %w", ref.Hash(), err) 67 + } 68 + 69 + out := tangled.GitTempGetBranch_Output{ 70 + Name: ref.Name().Short(), 71 + Hash: ref.Hash().String(), 72 + When: commit.Author.When.Format(time.RFC3339), 73 + Author: &tangled.GitTempDefs_Signature{ 74 + Name: commit.Author.Name, 75 + Email: commit.Author.Email, 76 + When: commit.Author.When.Format(time.RFC3339), 77 + }, 78 + } 79 + 80 + if commit.Message != "" { 81 + out.Message = &commit.Message 82 + } 83 + 84 + return &out, nil 85 + }
+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 + }
+98
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 + return 28 + } 29 + 30 + limit := 50 31 + if limitQuery != "" { 32 + limit, err = strconv.Atoi(limitQuery) 33 + if err != nil || limit < 1 || limit > 1000 { 34 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("limit parameter invalid: %s", limitQuery)}) 35 + return 36 + } 37 + } 38 + 39 + var cursor int64 40 + if cursorQuery != "" { 41 + cursor, err = strconv.ParseInt(cursorQuery, 10, 64) 42 + if err != nil || cursor < 0 { 43 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("cursor parameter invalid: %s", cursorQuery)}) 44 + return 45 + } 46 + } 47 + 48 + l := x.logger.With("repo", repo, "limit", limit, "cursor", cursor) 49 + 50 + out, err := x.listTags(r.Context(), repo, limit, cursor) 51 + if err != nil { 52 + // TODO: better error return 53 + l.Error("failed to list tags", "err", err) 54 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to list tags"}) 55 + return 56 + } 57 + writeJson(w, http.StatusOK, out) 58 + } 59 + 60 + func (x *Xrpc) listTags(ctx context.Context, repo syntax.ATURI, limit int, cursor int64) (*types.RepoTagsResponse, error) { 61 + repoPath, err := x.makeRepoPath(ctx, repo) 62 + if err != nil { 63 + return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err) 64 + } 65 + 66 + gr, err := git.PlainOpen(repoPath) 67 + if err != nil { 68 + return nil, fmt.Errorf("failed to open git repo: %w", err) 69 + } 70 + 71 + tags, err := gr.Tags(&git.TagsOptions{ 72 + Limit: limit, 73 + Offset: int(cursor), 74 + }) 75 + if err != nil { 76 + return nil, fmt.Errorf("failed to get git tags: %w", err) 77 + } 78 + 79 + rtags := make([]*types.TagReference, len(tags)) 80 + for i, tag := range tags { 81 + var target *object.Tag 82 + if tag.Target != plumbing.ZeroHash { 83 + target = &tag 84 + } 85 + rtags[i] = &types.TagReference{ 86 + Reference: types.Reference{ 87 + Name: tag.Name, 88 + Hash: tag.Hash.String(), 89 + }, 90 + Tag: target, 91 + Message: tag.Message, 92 + } 93 + } 94 + 95 + return &types.RepoTagsResponse{ 96 + Tags: rtags, 97 + }, nil 98 + }
+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 199 return io.ReadAll(reader) 200 200 } 201 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 + 202 216 // read and parse .gitmodules 203 217 func (g *GitRepo) Submodules() (*config.Modules, error) { 204 218 c, err := g.r.CommitObject(g.h)