forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

feat(knotserver): add list repo endpoint

Changed files
+150
api
knotserver
+46
api/tangled/repolistRepos.go
···
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+
package tangled
+
+
// schema: sh.tangled.repo.listRepos
+
+
import (
+
"context"
+
+
"github.com/bluesky-social/indigo/lex/util"
+
)
+
+
const (
+
RepoListReposNSID = "sh.tangled.repo.listRepos"
+
)
+
+
// RepoListRepos_Output is the output of a sh.tangled.repo.listRepos call.
+
type RepoListRepos_Output struct {
+
Users []*RepoListRepos_User `json:"users" cborgen:"users"`
+
}
+
+
// RepoListRepos_User is a "user" in the sh.tangled.repo.listRepos schema.
+
type RepoListRepos_User struct {
+
Did string `json:"did" cborgen:"did"`
+
Repos []*RepoListRepos_RepoEntry `json:"repos" cborgen:"repos"`
+
}
+
+
// RepoListRepos_RepoEntry is a "repoEntry" in the sh.tangled.repo.listRepos schema.
+
type RepoListRepos_RepoEntry struct {
+
Name string `json:"name" cborgen:"name"`
+
Did string `json:"did" cborgen:"did"`
+
FullPath string `json:"fullPath" cborgen:"fullPath"`
+
DefaultBranch string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"`
+
}
+
+
// RepoListRepos calls the XRPC method "sh.tangled.repo.listRepos".
+
func RepoListRepos(ctx context.Context, c util.LexClient) (*RepoListRepos_Output, error) {
+
var out RepoListRepos_Output
+
+
params := map[string]interface{}{}
+
if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.listRepos", params, nil, &out); err != nil {
+
return nil, err
+
}
+
+
return &out, nil
+
}
+103
knotserver/xrpc/list_repos.go
···
+
package xrpc
+
+
import (
+
"net/http"
+
"os"
+
"path/filepath"
+
"strings"
+
+
securejoin "github.com/cyphar/filepath-securejoin"
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/knotserver/git"
+
xrpcerr "tangled.org/core/xrpc/errors"
+
)
+
+
// ListRepos lists all users (DIDs) and their repositories by scanning the repository directory
+
func (x *Xrpc) ListRepos(w http.ResponseWriter, r *http.Request) {
+
scanPath := x.Config.Repo.ScanPath
+
+
didEntries, err := os.ReadDir(scanPath)
+
if err != nil {
+
x.Logger.Error("failed to read scan path", "error", err, "path", scanPath)
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+
return
+
}
+
+
var users []*tangled.RepoListRepos_User
+
+
for _, didEntry := range didEntries {
+
if !didEntry.IsDir() {
+
continue
+
}
+
+
did := didEntry.Name()
+
+
// Validate DID format (basic check)
+
if !strings.HasPrefix(did, "did:") {
+
continue
+
}
+
+
didPath, err := securejoin.SecureJoin(scanPath, did)
+
if err != nil {
+
x.Logger.Warn("failed to join path for did", "did", did, "error", err)
+
continue
+
}
+
+
// Read repositories for this DID
+
repoEntries, err := os.ReadDir(didPath)
+
if err != nil {
+
x.Logger.Warn("failed to read did directory", "did", did, "error", err)
+
continue
+
}
+
+
var repos []*tangled.RepoListRepos_RepoEntry
+
+
for _, repoEntry := range repoEntries {
+
if !repoEntry.IsDir() {
+
continue
+
}
+
+
repoName := repoEntry.Name()
+
+
// Check if it's a valid git repository
+
repoPath, err := securejoin.SecureJoin(didPath, repoName)
+
if err != nil {
+
continue
+
}
+
+
repo, err := git.PlainOpen(repoPath)
+
if err != nil {
+
// Not a valid git repository, skip
+
continue
+
}
+
+
// Get default branch
+
defaultBranch := "master"
+
branch, err := repo.FindMainBranch()
+
if err == nil {
+
defaultBranch = branch
+
}
+
+
repos = append(repos, &tangled.RepoListRepos_RepoEntry{
+
Name: repoName,
+
Did: did,
+
FullPath: filepath.Join(did, repoName),
+
DefaultBranch: defaultBranch,
+
})
+
}
+
+
// Only add user if they have repositories
+
if len(repos) > 0 {
+
users = append(users, &tangled.RepoListRepos_User{
+
Did: did,
+
Repos: repos,
+
})
+
}
+
}
+
+
response := tangled.RepoListRepos_Output{
+
Users: users,
+
}
+
+
writeJson(w, response)
+
}
+1
knotserver/xrpc/xrpc.go
···
// service query endpoints (no auth required)
r.Get("/"+tangled.OwnerNSID, x.Owner)
+
r.Get("/"+tangled.RepoListReposNSID, x.ListRepos)
return r
}