Monorepo for Tangled
tangled.org
1package reporesolver
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log"
9 "net/http"
10 "net/url"
11 "path"
12 "strings"
13
14 "github.com/bluesky-social/indigo/atproto/identity"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 securejoin "github.com/cyphar/filepath-securejoin"
17 "github.com/go-chi/chi/v5"
18 "tangled.sh/tangled.sh/core/appview"
19 "tangled.sh/tangled.sh/core/appview/db"
20 "tangled.sh/tangled.sh/core/appview/oauth"
21 "tangled.sh/tangled.sh/core/appview/pages"
22 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
23 "tangled.sh/tangled.sh/core/knotclient"
24 "tangled.sh/tangled.sh/core/rbac"
25)
26
27type ResolvedRepo struct {
28 Knot string
29 OwnerId identity.Identity
30 RepoName string
31 RepoAt syntax.ATURI
32 Description string
33 CreatedAt string
34 Ref string
35 CurrentDir string
36
37 rr *RepoResolver
38}
39
40type RepoResolver struct {
41 config *appview.Config
42 enforcer *rbac.Enforcer
43 resolver *appview.Resolver
44 execer db.Execer
45}
46
47func New(config *appview.Config, enforcer *rbac.Enforcer, resolver *appview.Resolver, execer db.Execer) *RepoResolver {
48 return &RepoResolver{config: config, enforcer: enforcer, resolver: resolver, execer: execer}
49}
50
51func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
52 repoName := chi.URLParam(r, "repo")
53 knot, ok := r.Context().Value("knot").(string)
54 if !ok {
55 log.Println("malformed middleware")
56 return nil, fmt.Errorf("malformed middleware")
57 }
58 id, ok := r.Context().Value("resolvedId").(identity.Identity)
59 if !ok {
60 log.Println("malformed middleware")
61 return nil, fmt.Errorf("malformed middleware")
62 }
63
64 repoAt, ok := r.Context().Value("repoAt").(string)
65 if !ok {
66 log.Println("malformed middleware")
67 return nil, fmt.Errorf("malformed middleware")
68 }
69
70 parsedRepoAt, err := syntax.ParseATURI(repoAt)
71 if err != nil {
72 log.Println("malformed repo at-uri")
73 return nil, fmt.Errorf("malformed middleware")
74 }
75
76 ref := chi.URLParam(r, "ref")
77
78 if ref == "" {
79 us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev)
80 if err != nil {
81 return nil, err
82 }
83
84 defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName)
85 if err != nil {
86 return nil, err
87 }
88
89 ref = defaultBranch.Branch
90 }
91
92 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref))
93
94 // pass through values from the middleware
95 description, ok := r.Context().Value("repoDescription").(string)
96 addedAt, ok := r.Context().Value("repoAddedAt").(string)
97
98 return &ResolvedRepo{
99 Knot: knot,
100 OwnerId: id,
101 RepoName: repoName,
102 RepoAt: parsedRepoAt,
103 Description: description,
104 CreatedAt: addedAt,
105 Ref: ref,
106 CurrentDir: currentDir,
107
108 rr: rr,
109 }, nil
110}
111
112func (f *ResolvedRepo) OwnerDid() string {
113 return f.OwnerId.DID.String()
114}
115
116func (f *ResolvedRepo) OwnerHandle() string {
117 return f.OwnerId.Handle.String()
118}
119
120func (f *ResolvedRepo) OwnerSlashRepo() string {
121 handle := f.OwnerId.Handle
122
123 var p string
124 if handle != "" && !handle.IsInvalidHandle() {
125 p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName)
126 } else {
127 p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
128 }
129
130 return p
131}
132
133func (f *ResolvedRepo) DidSlashRepo() string {
134 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
135 return p
136}
137
138func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) {
139 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
140 if err != nil {
141 return nil, err
142 }
143
144 var collaborators []pages.Collaborator
145 for _, item := range repoCollaborators {
146 // currently only two roles: owner and member
147 var role string
148 if item[3] == "repo:owner" {
149 role = "owner"
150 } else if item[3] == "repo:collaborator" {
151 role = "collaborator"
152 } else {
153 continue
154 }
155
156 did := item[0]
157
158 c := pages.Collaborator{
159 Did: did,
160 Handle: "",
161 Role: role,
162 }
163 collaborators = append(collaborators, c)
164 }
165
166 // populate all collborators with handles
167 identsToResolve := make([]string, len(collaborators))
168 for i, collab := range collaborators {
169 identsToResolve[i] = collab.Did
170 }
171
172 resolvedIdents := f.rr.resolver.ResolveIdents(ctx, identsToResolve)
173 for i, resolved := range resolvedIdents {
174 if resolved != nil {
175 collaborators[i].Handle = resolved.Handle.String()
176 }
177 }
178
179 return collaborators, nil
180}
181
182// this function is a bit weird since it now returns RepoInfo from an entirely different
183// package. we should refactor this or get rid of RepoInfo entirely.
184func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
185 isStarred := false
186 if user != nil {
187 isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt))
188 }
189
190 starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt)
191 if err != nil {
192 log.Println("failed to get star count for ", f.RepoAt)
193 }
194 issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt)
195 if err != nil {
196 log.Println("failed to get issue count for ", f.RepoAt)
197 }
198 pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt)
199 if err != nil {
200 log.Println("failed to get issue count for ", f.RepoAt)
201 }
202 source, err := db.GetRepoSource(f.rr.execer, f.RepoAt)
203 if errors.Is(err, sql.ErrNoRows) {
204 source = ""
205 } else if err != nil {
206 log.Println("failed to get repo source for ", f.RepoAt, err)
207 }
208
209 var sourceRepo *db.Repo
210 if source != "" {
211 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
212 if err != nil {
213 log.Println("failed to get repo by at uri", err)
214 }
215 }
216
217 var sourceHandle *identity.Identity
218 if sourceRepo != nil {
219 sourceHandle, err = f.rr.resolver.ResolveIdent(context.Background(), sourceRepo.Did)
220 if err != nil {
221 log.Println("failed to resolve source repo", err)
222 }
223 }
224
225 knot := f.Knot
226 var disableFork bool
227 us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev)
228 if err != nil {
229 log.Printf("failed to create unsigned client for %s: %v", knot, err)
230 } else {
231 result, err := us.Branches(f.OwnerDid(), f.RepoName)
232 if err != nil {
233 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
234 }
235
236 if len(result.Branches) == 0 {
237 disableFork = true
238 }
239 }
240
241 repoInfo := repoinfo.RepoInfo{
242 OwnerDid: f.OwnerDid(),
243 OwnerHandle: f.OwnerHandle(),
244 Name: f.RepoName,
245 RepoAt: f.RepoAt,
246 Description: f.Description,
247 Ref: f.Ref,
248 IsStarred: isStarred,
249 Knot: knot,
250 Roles: f.RolesInRepo(user),
251 Stats: db.RepoStats{
252 StarCount: starCount,
253 IssueCount: issueCount,
254 PullCount: pullCount,
255 },
256 DisableFork: disableFork,
257 CurrentDir: f.CurrentDir,
258 }
259
260 if sourceRepo != nil {
261 repoInfo.Source = sourceRepo
262 repoInfo.SourceHandle = sourceHandle.Handle.String()
263 }
264
265 return repoInfo
266}
267
268func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
269 if u != nil {
270 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
271 return repoinfo.RolesInRepo{r}
272 } else {
273 return repoinfo.RolesInRepo{}
274 }
275}
276
277// extractPathAfterRef gets the actual repository path
278// after the ref. for example:
279//
280// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
281func extractPathAfterRef(fullPath, ref string) string {
282 fullPath = strings.TrimPrefix(fullPath, "/")
283
284 ref = url.PathEscape(ref)
285
286 prefixes := []string{
287 fmt.Sprintf("blob/%s/", ref),
288 fmt.Sprintf("tree/%s/", ref),
289 fmt.Sprintf("raw/%s/", ref),
290 }
291
292 for _, prefix := range prefixes {
293 idx := strings.Index(fullPath, prefix)
294 if idx != -1 {
295 return fullPath[idx+len(prefix):]
296 }
297 }
298
299 return ""
300}