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