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