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