1package state
2
3import (
4 "context"
5 "crypto/rand"
6 "fmt"
7 "log"
8 "math/big"
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 "github.com/go-chi/chi/v5"
17 "github.com/go-git/go-git/v5/plumbing/object"
18 "tangled.sh/tangled.sh/core/appview/db"
19 "tangled.sh/tangled.sh/core/appview/knotclient"
20 "tangled.sh/tangled.sh/core/appview/oauth"
21 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
22)
23
24func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
25 repoName := chi.URLParam(r, "repo")
26 knot, ok := r.Context().Value("knot").(string)
27 if !ok {
28 log.Println("malformed middleware")
29 return nil, fmt.Errorf("malformed middleware")
30 }
31 id, ok := r.Context().Value("resolvedId").(identity.Identity)
32 if !ok {
33 log.Println("malformed middleware")
34 return nil, fmt.Errorf("malformed middleware")
35 }
36
37 repoAt, ok := r.Context().Value("repoAt").(string)
38 if !ok {
39 log.Println("malformed middleware")
40 return nil, fmt.Errorf("malformed middleware")
41 }
42
43 parsedRepoAt, err := syntax.ParseATURI(repoAt)
44 if err != nil {
45 log.Println("malformed repo at-uri")
46 return nil, fmt.Errorf("malformed middleware")
47 }
48
49 ref := chi.URLParam(r, "ref")
50
51 if ref == "" {
52 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
53 if err != nil {
54 return nil, err
55 }
56
57 defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName)
58 if err != nil {
59 return nil, err
60 }
61
62 ref = defaultBranch.Branch
63 }
64
65 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref))
66
67 // pass through values from the middleware
68 description, ok := r.Context().Value("repoDescription").(string)
69 addedAt, ok := r.Context().Value("repoAddedAt").(string)
70
71 return &FullyResolvedRepo{
72 Knot: knot,
73 OwnerId: id,
74 RepoName: repoName,
75 RepoAt: parsedRepoAt,
76 Description: description,
77 CreatedAt: addedAt,
78 Ref: ref,
79 CurrentDir: currentDir,
80 }, nil
81}
82
83func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
84 if u != nil {
85 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
86 return repoinfo.RolesInRepo{r}
87 } else {
88 return repoinfo.RolesInRepo{}
89 }
90}
91
92// extractPathAfterRef gets the actual repository path
93// after the ref. for example:
94//
95// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
96func extractPathAfterRef(fullPath, ref string) string {
97 fullPath = strings.TrimPrefix(fullPath, "/")
98
99 ref = url.PathEscape(ref)
100
101 prefixes := []string{
102 fmt.Sprintf("blob/%s/", ref),
103 fmt.Sprintf("tree/%s/", ref),
104 fmt.Sprintf("raw/%s/", ref),
105 }
106
107 for _, prefix := range prefixes {
108 idx := strings.Index(fullPath, prefix)
109 if idx != -1 {
110 return fullPath[idx+len(prefix):]
111 }
112 }
113
114 return ""
115}
116
117func uniqueEmails(commits []*object.Commit) []string {
118 emails := make(map[string]struct{})
119 for _, commit := range commits {
120 if commit.Author.Email != "" {
121 emails[commit.Author.Email] = struct{}{}
122 }
123 if commit.Committer.Email != "" {
124 emails[commit.Committer.Email] = struct{}{}
125 }
126 }
127 var uniqueEmails []string
128 for email := range emails {
129 uniqueEmails = append(uniqueEmails, email)
130 }
131 return uniqueEmails
132}
133
134func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
135 if commitCount == 0 && tagCount == 0 && branchCount == 0 {
136 return
137 }
138
139 // typically 1 item on right side = 2 files in height
140 availableSpace := fileCount / 2
141
142 // clamp tagcount
143 if tagCount > 0 {
144 tagsTrunc = 1
145 availableSpace -= 1 // an extra subtracted for headers etc.
146 }
147
148 // clamp branchcount
149 if branchCount > 0 {
150 branchesTrunc = min(max(branchCount, 1), 2)
151 availableSpace -= branchesTrunc // an extra subtracted for headers etc.
152 }
153
154 // show
155 if commitCount > 0 {
156 commitsTrunc = max(availableSpace, 3)
157 }
158
159 return
160}
161
162func EmailToDidOrHandle(s *State, emails []string) map[string]string {
163 emailToDid, err := db.GetEmailToDid(s.db, emails, true) // only get verified emails for mapping
164 if err != nil {
165 log.Printf("error fetching dids for emails: %v", err)
166 return nil
167 }
168
169 var dids []string
170 for _, v := range emailToDid {
171 dids = append(dids, v)
172 }
173 resolvedIdents := s.resolver.ResolveIdents(context.Background(), dids)
174
175 didHandleMap := make(map[string]string)
176 for _, identity := range resolvedIdents {
177 if !identity.Handle.IsInvalidHandle() {
178 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
179 } else {
180 didHandleMap[identity.DID.String()] = identity.DID.String()
181 }
182 }
183
184 // Create map of email to didOrHandle for commit display
185 emailToDidOrHandle := make(map[string]string)
186 for email, did := range emailToDid {
187 if didOrHandle, ok := didHandleMap[did]; ok {
188 emailToDidOrHandle[email] = didOrHandle
189 }
190 }
191
192 return emailToDidOrHandle
193}
194
195func randomString(n int) string {
196 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
197 result := make([]byte, n)
198
199 for i := 0; i < n; i++ {
200 n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
201 result[i] = letters[n.Int64()]
202 }
203
204 return string(result)
205}