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