+51
api/tangled/repolistRepos.go
+51
api/tangled/repolistRepos.go
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.repo.listRepos
6
+
7
+
import (
8
+
"context"
9
+
10
+
"github.com/bluesky-social/indigo/lex/util"
11
+
)
12
+
13
+
const (
14
+
RepoListReposNSID = "sh.tangled.repo.listRepos"
15
+
)
16
+
17
+
// RepoListRepos_Output is the output of a sh.tangled.repo.listRepos call.
18
+
type RepoListRepos_Output struct {
19
+
Users []*RepoListRepos_User `json:"users" cborgen:"users"`
20
+
}
21
+
22
+
// RepoListRepos_RepoEntry is a "repoEntry" in the sh.tangled.repo.listRepos schema.
23
+
type RepoListRepos_RepoEntry struct {
24
+
// defaultBranch: Default branch of the repository
25
+
DefaultBranch *string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"`
26
+
// did: DID of the repository owner
27
+
Did string `json:"did" cborgen:"did"`
28
+
// fullPath: Full path to the repository
29
+
FullPath string `json:"fullPath" cborgen:"fullPath"`
30
+
// name: Repository name
31
+
Name string `json:"name" cborgen:"name"`
32
+
}
33
+
34
+
// RepoListRepos_User is a "user" in the sh.tangled.repo.listRepos schema.
35
+
type RepoListRepos_User struct {
36
+
// did: DID of the user
37
+
Did string `json:"did" cborgen:"did"`
38
+
Repos []*RepoListRepos_RepoEntry `json:"repos" cborgen:"repos"`
39
+
}
40
+
41
+
// RepoListRepos calls the XRPC method "sh.tangled.repo.listRepos".
42
+
func RepoListRepos(ctx context.Context, c util.LexClient) (*RepoListRepos_Output, error) {
43
+
var out RepoListRepos_Output
44
+
45
+
params := map[string]interface{}{}
46
+
if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.listRepos", params, nil, &out); err != nil {
47
+
return nil, err
48
+
}
49
+
50
+
return &out, nil
51
+
}
-5
appview/knots/knots.go
-5
appview/knots/knots.go
···
666
666
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
667
667
return
668
668
}
669
-
if memberId.Handle.IsInvalidHandle() {
670
-
l.Error("failed to resolve member identity to handle")
671
-
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
672
-
return
673
-
}
674
669
675
670
// remove from enforcer
676
671
err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+1
appview/repo/archive.go
+1
appview/repo/archive.go
···
18
18
l := rp.logger.With("handler", "DownloadArchive")
19
19
ref := chi.URLParam(r, "ref")
20
20
ref, _ = url.PathUnescape(ref)
21
+
ref = strings.TrimSuffix(ref, ".tar.gz")
21
22
f, err := rp.repoResolver.Resolve(r)
22
23
if err != nil {
23
24
l.Error("failed to get repo and knot", "err", err)
-5
appview/spindles/spindles.go
-5
appview/spindles/spindles.go
···
653
653
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
654
654
return
655
655
}
656
-
if memberId.Handle.IsInvalidHandle() {
657
-
l.Error("failed to resolve member identity to handle")
658
-
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
659
-
return
660
-
}
661
656
662
657
tx, err := s.Db.Begin()
663
658
if err != nil {
+32
-7
docs/template.html
+32
-7
docs/template.html
···
43
43
$endfor$
44
44
45
45
$if(toc)$
46
-
<!-- mobile topbar toc -->
47
-
<details id="mobile-$idprefix$TOC" role="doc-toc" class="md:hidden bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 z-50 space-y-4 group px-6 py-4">
48
-
<summary class="cursor-pointer list-none text-sm font-semibold select-none flex gap-2 justify-between items-center dark:text-white">
46
+
<!-- mobile TOC trigger -->
47
+
<div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700">
48
+
<button
49
+
type="button"
50
+
popovertarget="mobile-toc-popover"
51
+
popovertargetaction="toggle"
52
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white"
53
+
>
54
+
${ menu.svg() }
55
+
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
56
+
</button>
57
+
</div>
58
+
59
+
<div
60
+
id="mobile-toc-popover"
61
+
popover
62
+
class="mobile-toc-popover
63
+
bg-white dark:bg-gray-800
64
+
border-b border-gray-200 dark:border-gray-700
65
+
h-full overflow-y-auto
66
+
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
67
+
>
68
+
<button
69
+
type="button"
70
+
popovertarget="mobile-toc-popover"
71
+
popovertargetaction="toggle"
72
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4">
73
+
${ x.svg() }
49
74
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
50
-
<span class="group-open:hidden inline">${ menu.svg() }</span>
51
-
<span class="hidden group-open:inline">${ x.svg() }</span>
52
-
</summary>
75
+
</button>
53
76
${ table-of-contents:toc.html() }
54
-
</details>
77
+
</div>
78
+
79
+
55
80
<!-- desktop sidebar toc -->
56
81
<nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50">
57
82
$if(toc-title)$
+1
input.css
+1
input.css
+103
knotserver/xrpc/list_repos.go
+103
knotserver/xrpc/list_repos.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"net/http"
5
+
"os"
6
+
"path/filepath"
7
+
"strings"
8
+
9
+
securejoin "github.com/cyphar/filepath-securejoin"
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/knotserver/git"
12
+
xrpcerr "tangled.org/core/xrpc/errors"
13
+
)
14
+
15
+
// ListRepos lists all users (DIDs) and their repositories by scanning the repository directory
16
+
func (x *Xrpc) ListRepos(w http.ResponseWriter, r *http.Request) {
17
+
scanPath := x.Config.Repo.ScanPath
18
+
19
+
didEntries, err := os.ReadDir(scanPath)
20
+
if err != nil {
21
+
x.Logger.Error("failed to read scan path", "error", err, "path", scanPath)
22
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
23
+
return
24
+
}
25
+
26
+
var users []*tangled.RepoListRepos_User
27
+
28
+
for _, didEntry := range didEntries {
29
+
if !didEntry.IsDir() {
30
+
continue
31
+
}
32
+
33
+
did := didEntry.Name()
34
+
35
+
// Validate DID format (basic check)
36
+
if !strings.HasPrefix(did, "did:") {
37
+
continue
38
+
}
39
+
40
+
didPath, err := securejoin.SecureJoin(scanPath, did)
41
+
if err != nil {
42
+
x.Logger.Warn("failed to join path for did", "did", did, "error", err)
43
+
continue
44
+
}
45
+
46
+
// Read repositories for this DID
47
+
repoEntries, err := os.ReadDir(didPath)
48
+
if err != nil {
49
+
x.Logger.Warn("failed to read did directory", "did", did, "error", err)
50
+
continue
51
+
}
52
+
53
+
var repos []*tangled.RepoListRepos_RepoEntry
54
+
55
+
for _, repoEntry := range repoEntries {
56
+
if !repoEntry.IsDir() {
57
+
continue
58
+
}
59
+
60
+
repoName := repoEntry.Name()
61
+
62
+
// Check if it's a valid git repository
63
+
repoPath, err := securejoin.SecureJoin(didPath, repoName)
64
+
if err != nil {
65
+
continue
66
+
}
67
+
68
+
repo, err := git.PlainOpen(repoPath)
69
+
if err != nil {
70
+
// Not a valid git repository, skip
71
+
continue
72
+
}
73
+
74
+
// Get default branch
75
+
defaultBranch := "master"
76
+
branch, err := repo.FindMainBranch()
77
+
if err == nil {
78
+
defaultBranch = branch
79
+
}
80
+
81
+
repos = append(repos, &tangled.RepoListRepos_RepoEntry{
82
+
Name: repoName,
83
+
Did: did,
84
+
FullPath: filepath.Join(did, repoName),
85
+
DefaultBranch: &defaultBranch,
86
+
})
87
+
}
88
+
89
+
// Only add user if they have repositories
90
+
if len(repos) > 0 {
91
+
users = append(users, &tangled.RepoListRepos_User{
92
+
Did: did,
93
+
Repos: repos,
94
+
})
95
+
}
96
+
}
97
+
98
+
response := tangled.RepoListRepos_Output{
99
+
Users: users,
100
+
}
101
+
102
+
writeJson(w, response)
103
+
}
+1
knotserver/xrpc/xrpc.go
+1
knotserver/xrpc/xrpc.go
+71
lexicons/repo/listRepos.json
+71
lexicons/repo/listRepos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.repo.listRepos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Lists all users (DIDs) and their repositories",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["users"],
17
+
"properties": {
18
+
"users": {
19
+
"type": "array",
20
+
"items": {
21
+
"type": "ref",
22
+
"ref": "#user"
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
28
+
},
29
+
"user": {
30
+
"type": "object",
31
+
"required": ["did", "repos"],
32
+
"properties": {
33
+
"did": {
34
+
"type": "string",
35
+
"format": "did",
36
+
"description": "DID of the user"
37
+
},
38
+
"repos": {
39
+
"type": "array",
40
+
"items": {
41
+
"type": "ref",
42
+
"ref": "#repoEntry"
43
+
}
44
+
}
45
+
}
46
+
},
47
+
"repoEntry": {
48
+
"type": "object",
49
+
"required": ["name", "did", "fullPath"],
50
+
"properties": {
51
+
"name": {
52
+
"type": "string",
53
+
"description": "Repository name"
54
+
},
55
+
"did": {
56
+
"type": "string",
57
+
"format": "did",
58
+
"description": "DID of the repository owner"
59
+
},
60
+
"fullPath": {
61
+
"type": "string",
62
+
"description": "Full path to the repository"
63
+
},
64
+
"defaultBranch": {
65
+
"type": "string",
66
+
"description": "Default branch of the repository"
67
+
}
68
+
}
69
+
}
70
+
}
71
+
}
+21
-3
spindle/server.go
+21
-3
spindle/server.go
···
8
8
"log/slog"
9
9
"maps"
10
10
"net/http"
11
+
"sync"
11
12
12
13
"github.com/go-chi/chi/v5"
13
14
"tangled.org/core/api/tangled"
···
30
31
)
31
32
32
33
//go:embed motd
33
-
var motd []byte
34
+
var defaultMotd []byte
34
35
35
36
const (
36
37
rbacDomain = "thisserver"
···
47
48
cfg *config.Config
48
49
ks *eventconsumer.Consumer
49
50
res *idresolver.Resolver
50
-
vault secrets.Manager
51
+
vault secrets.Manager
52
+
motd []byte
53
+
motdMu sync.RWMutex
51
54
}
52
55
53
56
// New creates a new Spindle server with the provided configuration and engines.
···
128
131
cfg: cfg,
129
132
res: resolver,
130
133
vault: vault,
134
+
motd: defaultMotd,
131
135
}
132
136
133
137
err = e.AddSpindle(rbacDomain)
···
201
205
return s.e
202
206
}
203
207
208
+
// SetMotdContent sets custom MOTD content, replacing the embedded default.
209
+
func (s *Spindle) SetMotdContent(content []byte) {
210
+
s.motdMu.Lock()
211
+
defer s.motdMu.Unlock()
212
+
s.motd = content
213
+
}
214
+
215
+
// GetMotdContent returns the current MOTD content.
216
+
func (s *Spindle) GetMotdContent() []byte {
217
+
s.motdMu.RLock()
218
+
defer s.motdMu.RUnlock()
219
+
return s.motd
220
+
}
221
+
204
222
// Start starts the Spindle server (blocking).
205
223
func (s *Spindle) Start(ctx context.Context) error {
206
224
// starts a job queue runner in the background
···
246
264
mux := chi.NewRouter()
247
265
248
266
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
249
-
w.Write(motd)
267
+
w.Write(s.GetMotdContent())
250
268
})
251
269
mux.HandleFunc("/events", s.Events)
252
270
mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)