Monorepo for Tangled
tangled.org
1package reporesolver
2
3import (
4 "fmt"
5 "log"
6 "net/http"
7 "path"
8 "regexp"
9 "strings"
10
11 "github.com/bluesky-social/indigo/atproto/identity"
12 "github.com/go-chi/chi/v5"
13 "tangled.org/core/appview/cache"
14 "tangled.org/core/appview/config"
15 "tangled.org/core/appview/db"
16 "tangled.org/core/appview/knotacl"
17 "tangled.org/core/appview/models"
18 "tangled.org/core/appview/oauth"
19 "tangled.org/core/appview/pages/repoinfo"
20)
21
22var (
23 blobPattern = regexp.MustCompile(`blob/[^/]+/(.*)$`)
24 treePattern = regexp.MustCompile(`tree/[^/]+/(.*)$`)
25)
26
27type RepoResolver struct {
28 config *config.Config
29 acl *knotacl.Service
30 execer db.Execer
31 rdb *cache.Cache
32}
33
34func New(config *config.Config, acl *knotacl.Service, execer db.Execer, rdb *cache.Cache) *RepoResolver {
35 return &RepoResolver{config: config, acl: acl, execer: execer, rdb: rdb}
36}
37
38func CanonicalRepoPath(handle string, repo *models.Repo) string {
39 return path.Join(handle, repo.Slug())
40}
41
42func CanonicalRedirectTarget(req *http.Request, canonical string) string {
43 parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3)
44 target := "/" + canonical
45 if len(parts) == 3 {
46 target += "/" + parts[2]
47 }
48 if req.URL.RawQuery != "" {
49 target += "?" + req.URL.RawQuery
50 }
51 return target
52}
53
54// NOTE: this... should not even be here. the entire package will be removed in future refactor
55func GetBaseRepoPath(r *http.Request, repo *models.Repo) string {
56 if id, ok := r.Context().Value("resolvedId").(identity.Identity); ok && !id.Handle.IsInvalidHandle() {
57 if h := id.Handle.String(); h != "" {
58 return CanonicalRepoPath(h, repo)
59 }
60 }
61 var (
62 user = chi.URLParam(r, "user")
63 name = chi.URLParam(r, "repo")
64 )
65 if user != "" && name != "" {
66 return path.Join(user, name)
67 }
68 if repo.Name != "" {
69 return path.Join(repo.Did, repo.Name)
70 }
71 return repo.RepoIdentifier()
72}
73
74// TODO: move this out of `RepoResolver` struct
75func (rr *RepoResolver) Resolve(r *http.Request) (*models.Repo, error) {
76 repo, ok := r.Context().Value("repo").(*models.Repo)
77 if !ok {
78 log.Println("malformed middleware: `repo` not exist in context")
79 return nil, fmt.Errorf("malformed middleware")
80 }
81
82 return repo, nil
83}
84
85// 1. [x] replace `RepoInfo` to `reporesolver.GetRepoInfo(r *http.Request, repo, user)`
86// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
87// 3. [x] remove `ResolvedRepo`
88// 4. [ ] replace reporesolver to reposervice
89func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUser) repoinfo.RepoInfo {
90 ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
91 repo, rok := r.Context().Value("repo").(*models.Repo)
92 if !ook || !rok {
93 log.Println("malformed request, failed to get repo from context")
94 }
95
96 // get dir/ref
97 currentDir := extractCurrentDir(r.URL.EscapedPath())
98 ref := chi.URLParam(r, "ref")
99
100 repoDid := repo.RepoDid
101 isStarred := false
102 roles := repoinfo.RolesInRepo{}
103 if user != nil {
104 isStarred = db.GetStarStatus(rr.execer, user.Did, repoDid)
105 roles = rr.acl.RolesInRepo(r.Context(), repo, user.Did)
106 }
107
108 stats := repo.RepoStats
109 if stats == nil {
110 starCount, starErr := db.GetStarCount(rr.execer, models.StarSubjectRepo, repoDid)
111 if starErr != nil {
112 log.Println("failed to get star count for ", repoDid)
113 }
114 issueCount, err := db.GetIssueCount(rr.execer, repoDid)
115 if err != nil {
116 log.Println("failed to get issue count for ", repoDid)
117 }
118 pullCount, err := db.GetPullCount(rr.execer, repoDid)
119 if err != nil {
120 log.Println("failed to get pull count for ", repoDid)
121 }
122 stats = &models.RepoStats{
123 StarCount: starCount,
124 IssueCount: issueCount,
125 PullCount: pullCount,
126 }
127 }
128
129 var sourceRepo *models.Repo
130 var err error
131 if repo.Source != "" {
132 if strings.HasPrefix(repo.Source, "did:") {
133 sourceRepo, err = db.GetRepoByDid(rr.execer, repo.Source)
134 } else {
135 sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source)
136 }
137 if err != nil {
138 log.Println("failed to get source repo", err)
139 }
140 }
141
142 ownerHandle := ownerId.Handle.String()
143 if h := cache.LookupPreferredHandle(r.Context(), rr.rdb, rr.execer, ownerId.DID.String()); h != "" {
144 ownerHandle = h
145 }
146
147 repoInfo := repoinfo.RepoInfo{
148 // this is basically a models.Repo
149 OwnerDid: ownerId.DID.String(),
150 OwnerHandle: ownerHandle,
151 RepoDid: repo.RepoDid,
152 Name: repo.Name,
153 Rkey: repo.Rkey,
154 Description: repo.Description,
155 Website: repo.Website,
156 Topics: repo.Topics,
157 Knot: repo.Knot,
158 Spindle: repo.Spindle,
159 Stats: *stats,
160
161 // fork repo upstream
162 Source: sourceRepo,
163
164 // page context
165 CurrentDir: currentDir,
166 Ref: ref,
167
168 // info related to the session
169 IsStarred: isStarred,
170 Roles: roles,
171 }
172
173 return repoInfo
174}
175
176// extractCurrentDir gets the current directory for markdown link resolution.
177// for blob paths, returns the parent dir. for tree paths, returns the path itself.
178//
179// /@user/repo/blob/main/docs/README.md => docs
180// /@user/repo/tree/main/docs => docs
181func extractCurrentDir(fullPath string) string {
182 fullPath = strings.TrimPrefix(fullPath, "/")
183
184 if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 {
185 return path.Dir(matches[1])
186 }
187
188 if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 {
189 dir := strings.TrimSuffix(matches[1], "/")
190 if dir == "" {
191 return "."
192 }
193 return dir
194 }
195
196 return "."
197}