this repo has no description
1package reporesolver
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log"
9 "net/http"
10 "path"
11 "regexp"
12 "strings"
13
14 "github.com/bluesky-social/indigo/atproto/identity"
15 "github.com/go-chi/chi/v5"
16 "tangled.org/core/appview/config"
17 "tangled.org/core/appview/db"
18 "tangled.org/core/appview/models"
19 "tangled.org/core/appview/oauth"
20 "tangled.org/core/appview/pages"
21 "tangled.org/core/appview/pages/repoinfo"
22 "tangled.org/core/idresolver"
23 "tangled.org/core/rbac"
24)
25
26type ResolvedRepo struct {
27 models.Repo
28 OwnerId identity.Identity
29 CurrentDir string
30 Ref string
31
32 rr *RepoResolver
33}
34
35type RepoResolver struct {
36 config *config.Config
37 enforcer *rbac.Enforcer
38 idResolver *idresolver.Resolver
39 execer db.Execer
40}
41
42func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Resolver, execer db.Execer) *RepoResolver {
43 return &RepoResolver{config: config, enforcer: enforcer, idResolver: resolver, execer: execer}
44}
45
46// NOTE: this... should not even be here. the entire package will be removed in future refactor
47func GetBaseRepoPath(r *http.Request, repo *models.Repo) string {
48 var (
49 user = chi.URLParam(r, "user")
50 name = chi.URLParam(r, "repo")
51 )
52 if user == "" || name == "" {
53 return repo.DidSlashRepo()
54 }
55 return path.Join(user, name)
56}
57
58func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
59 repo, ok := r.Context().Value("repo").(*models.Repo)
60 if !ok {
61 log.Println("malformed middleware: `repo` not exist in context")
62 return nil, fmt.Errorf("malformed middleware")
63 }
64 id, ok := r.Context().Value("resolvedId").(identity.Identity)
65 if !ok {
66 log.Println("malformed middleware")
67 return nil, fmt.Errorf("malformed middleware")
68 }
69
70 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
71 ref := chi.URLParam(r, "ref")
72
73 return &ResolvedRepo{
74 Repo: *repo,
75 OwnerId: id,
76 CurrentDir: currentDir,
77 Ref: ref,
78
79 rr: rr,
80 }, nil
81}
82
83func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) {
84 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
85 if err != nil {
86 return nil, err
87 }
88
89 var collaborators []pages.Collaborator
90 for _, item := range repoCollaborators {
91 // currently only two roles: owner and member
92 var role string
93 switch item[3] {
94 case "repo:owner":
95 role = "owner"
96 case "repo:collaborator":
97 role = "collaborator"
98 default:
99 continue
100 }
101
102 did := item[0]
103
104 c := pages.Collaborator{
105 Did: did,
106 Role: role,
107 }
108 collaborators = append(collaborators, c)
109 }
110
111 return collaborators, nil
112}
113
114// this function is a bit weird since it now returns RepoInfo from an entirely different
115// package. we should refactor this or get rid of RepoInfo entirely.
116func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
117 repoAt := f.RepoAt()
118 isStarred := false
119 if user != nil {
120 isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt)
121 }
122
123 starCount, err := db.GetStarCount(f.rr.execer, repoAt)
124 if err != nil {
125 log.Println("failed to get star count for ", repoAt)
126 }
127 issueCount, err := db.GetIssueCount(f.rr.execer, repoAt)
128 if err != nil {
129 log.Println("failed to get issue count for ", repoAt)
130 }
131 pullCount, err := db.GetPullCount(f.rr.execer, repoAt)
132 if err != nil {
133 log.Println("failed to get issue count for ", repoAt)
134 }
135 source, err := db.GetRepoSource(f.rr.execer, repoAt)
136 if errors.Is(err, sql.ErrNoRows) {
137 source = ""
138 } else if err != nil {
139 log.Println("failed to get repo source for ", repoAt, err)
140 }
141
142 var sourceRepo *models.Repo
143 if source != "" {
144 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
145 if err != nil {
146 log.Println("failed to get repo by at uri", err)
147 }
148 }
149
150 var sourceHandle *identity.Identity
151 if sourceRepo != nil {
152 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did)
153 if err != nil {
154 log.Println("failed to resolve source repo", err)
155 }
156 }
157
158 knot := f.Knot
159
160 repoInfo := repoinfo.RepoInfo{
161 OwnerDid: f.OwnerId.DID.String(),
162 OwnerHandle: f.OwnerId.Handle.String(),
163 Name: f.Name,
164 Rkey: f.Rkey,
165 RepoAt: repoAt,
166 Description: f.Description,
167 Website: f.Website,
168 Topics: f.Topics,
169 IsStarred: isStarred,
170 Knot: knot,
171 Spindle: f.Spindle,
172 Roles: f.RolesInRepo(user),
173 Stats: models.RepoStats{
174 StarCount: starCount,
175 IssueCount: issueCount,
176 PullCount: pullCount,
177 },
178 CurrentDir: f.CurrentDir,
179 Ref: f.Ref,
180 }
181
182 if sourceRepo != nil {
183 repoInfo.Source = sourceRepo
184 repoInfo.SourceHandle = sourceHandle.Handle.String()
185 }
186
187 return repoInfo
188}
189
190func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
191 if u != nil {
192 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
193 return repoinfo.RolesInRepo{Roles: r}
194 } else {
195 return repoinfo.RolesInRepo{}
196 }
197}
198
199// extractPathAfterRef gets the actual repository path
200// after the ref. for example:
201//
202// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
203func extractPathAfterRef(fullPath string) string {
204 fullPath = strings.TrimPrefix(fullPath, "/")
205
206 // match blob/, tree/, or raw/ followed by any ref and then a slash
207 //
208 // captures everything after the final slash
209 pattern := `(?:blob|tree|raw)/[^/]+/(.*)$`
210
211 re := regexp.MustCompile(pattern)
212 matches := re.FindStringSubmatch(fullPath)
213
214 if len(matches) > 1 {
215 return matches[1]
216 }
217
218 return ""
219}