this repo has no description
1package repo
2
3import (
4 "fmt"
5 "log"
6 "net/http"
7 "slices"
8 "sort"
9 "strings"
10 "sync"
11 "time"
12
13 "context"
14 "encoding/json"
15
16 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
17 "github.com/go-git/go-git/v5/plumbing"
18 "tangled.sh/tangled.sh/core/api/tangled"
19 "tangled.sh/tangled.sh/core/appview/commitverify"
20 "tangled.sh/tangled.sh/core/appview/db"
21 "tangled.sh/tangled.sh/core/appview/pages"
22 "tangled.sh/tangled.sh/core/appview/pages/markup"
23 "tangled.sh/tangled.sh/core/appview/reporesolver"
24 "tangled.sh/tangled.sh/core/appview/xrpcclient"
25 "tangled.sh/tangled.sh/core/types"
26
27 "github.com/go-chi/chi/v5"
28 "github.com/go-enry/go-enry/v2"
29)
30
31func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
32 ref := chi.URLParam(r, "ref")
33
34 f, err := rp.repoResolver.Resolve(r)
35 if err != nil {
36 log.Println("failed to fully resolve repo", err)
37 return
38 }
39
40 scheme := "http"
41 if !rp.config.Core.Dev {
42 scheme = "https"
43 }
44 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
45 xrpcc := &indigoxrpc.Client{
46 Host: host,
47 }
48
49 // Build index response from multiple XRPC calls
50 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
51 if err != nil {
52 rp.pages.Error503(w)
53 log.Println("failed to build index response", err)
54 return
55 }
56
57 tagMap := make(map[string][]string)
58 for _, tag := range result.Tags {
59 hash := tag.Hash
60 if tag.Tag != nil {
61 hash = tag.Tag.Target.String()
62 }
63 tagMap[hash] = append(tagMap[hash], tag.Name)
64 }
65
66 for _, branch := range result.Branches {
67 hash := branch.Hash
68 tagMap[hash] = append(tagMap[hash], branch.Name)
69 }
70
71 sortFiles(result.Files)
72
73 slices.SortFunc(result.Branches, func(a, b types.Branch) int {
74 if a.Name == result.Ref {
75 return -1
76 }
77 if a.IsDefault {
78 return -1
79 }
80 if b.IsDefault {
81 return 1
82 }
83 if a.Commit != nil && b.Commit != nil {
84 if a.Commit.Committer.When.Before(b.Commit.Committer.When) {
85 return 1
86 } else {
87 return -1
88 }
89 }
90 return strings.Compare(a.Name, b.Name) * -1
91 })
92
93 commitCount := len(result.Commits)
94 branchCount := len(result.Branches)
95 tagCount := len(result.Tags)
96 fileCount := len(result.Files)
97
98 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
99 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
100 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
101 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))]
102
103 emails := uniqueEmails(commitsTrunc)
104 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
105 if err != nil {
106 log.Println("failed to get email to did map", err)
107 }
108
109 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
110 if err != nil {
111 log.Println(err)
112 }
113
114 user := rp.oauth.GetUser(r)
115 repoInfo := f.RepoInfo(user)
116
117 // TODO: a bit dirty
118 languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "")
119 if err != nil {
120 log.Printf("failed to compute language percentages: %s", err)
121 // non-fatal
122 }
123
124 var shas []string
125 for _, c := range commitsTrunc {
126 shas = append(shas, c.Hash.String())
127 }
128 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
129 if err != nil {
130 log.Printf("failed to fetch pipeline statuses: %s", err)
131 // non-fatal
132 }
133
134 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
135 LoggedInUser: user,
136 RepoInfo: repoInfo,
137 TagMap: tagMap,
138 RepoIndexResponse: *result,
139 CommitsTrunc: commitsTrunc,
140 TagsTrunc: tagsTrunc,
141 // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands
142 BranchesTrunc: branchesTrunc,
143 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
144 VerifiedCommits: vc,
145 Languages: languageInfo,
146 Pipelines: pipelines,
147 })
148}
149
150func (rp *Repo) getLanguageInfo(
151 ctx context.Context,
152 f *reporesolver.ResolvedRepo,
153 xrpcc *indigoxrpc.Client,
154 currentRef string,
155 isDefaultRef bool,
156) ([]types.RepoLanguageDetails, error) {
157 // first attempt to fetch from db
158 langs, err := db.GetRepoLanguages(
159 rp.db,
160 db.FilterEq("repo_at", f.RepoAt()),
161 db.FilterEq("ref", currentRef),
162 )
163
164 if err != nil || langs == nil {
165 // non-fatal, fetch langs from ks via XRPC
166 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
167 ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo)
168 if err != nil {
169 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
170 log.Println("failed to call XRPC repo.languages", xrpcerr)
171 return nil, xrpcerr
172 }
173 return nil, err
174 }
175
176 if ls == nil || ls.Languages == nil {
177 return nil, nil
178 }
179
180 for _, lang := range ls.Languages {
181 langs = append(langs, db.RepoLanguage{
182 RepoAt: f.RepoAt(),
183 Ref: currentRef,
184 IsDefaultRef: isDefaultRef,
185 Language: lang.Name,
186 Bytes: lang.Size,
187 })
188 }
189
190 // update appview's cache
191 err = db.InsertRepoLanguages(rp.db, langs)
192 if err != nil {
193 // non-fatal
194 log.Println("failed to cache lang results", err)
195 }
196 }
197
198 var total int64
199 for _, l := range langs {
200 total += l.Bytes
201 }
202
203 var languageStats []types.RepoLanguageDetails
204 for _, l := range langs {
205 percentage := float32(l.Bytes) / float32(total) * 100
206 color := enry.GetColor(l.Language)
207 languageStats = append(languageStats, types.RepoLanguageDetails{
208 Name: l.Language,
209 Percentage: percentage,
210 Color: color,
211 })
212 }
213
214 sort.Slice(languageStats, func(i, j int) bool {
215 if languageStats[i].Name == enry.OtherLanguage {
216 return false
217 }
218 if languageStats[j].Name == enry.OtherLanguage {
219 return true
220 }
221 if languageStats[i].Percentage != languageStats[j].Percentage {
222 return languageStats[i].Percentage > languageStats[j].Percentage
223 }
224 return languageStats[i].Name < languageStats[j].Name
225 })
226
227 return languageStats, nil
228}
229
230// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
231func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) {
232 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
233
234 // first get branches to determine the ref if not specified
235 branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
236 if err != nil {
237 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
238 log.Println("failed to call XRPC repo.branches", xrpcerr)
239 return nil, xrpcerr
240 }
241 return nil, err
242 }
243
244 var branchesResp types.RepoBranchesResponse
245 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
246 return nil, err
247 }
248
249 // if no ref specified, use default branch or first available
250 if ref == "" && len(branchesResp.Branches) > 0 {
251 for _, branch := range branchesResp.Branches {
252 if branch.IsDefault {
253 ref = branch.Name
254 break
255 }
256 }
257 if ref == "" {
258 ref = branchesResp.Branches[0].Name
259 }
260 }
261
262 // check if repo is empty
263 if len(branchesResp.Branches) == 0 {
264 return &types.RepoIndexResponse{
265 IsEmpty: true,
266 Branches: branchesResp.Branches,
267 }, nil
268 }
269
270 // now run the remaining queries in parallel
271 var wg sync.WaitGroup
272 var mu sync.Mutex
273 var errs []error
274
275 var (
276 tagsResp types.RepoTagsResponse
277 treeResp *tangled.RepoTree_Output
278 logResp types.RepoLogResponse
279 readmeContent string
280 readmeFileName string
281 )
282
283 // tags
284 wg.Add(1)
285 go func() {
286 defer wg.Done()
287 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
288 if err != nil {
289 mu.Lock()
290 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
291 log.Println("failed to call XRPC repo.tags", xrpcerr)
292 errs = append(errs, xrpcerr)
293 } else {
294 errs = append(errs, err)
295 }
296 mu.Unlock()
297 return
298 }
299
300 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
301 mu.Lock()
302 errs = append(errs, err)
303 mu.Unlock()
304 }
305 }()
306
307 // tree/files
308 wg.Add(1)
309 go func() {
310 defer wg.Done()
311 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
312 if err != nil {
313 mu.Lock()
314 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
315 log.Println("failed to call XRPC repo.tree", xrpcerr)
316 errs = append(errs, xrpcerr)
317 } else {
318 errs = append(errs, err)
319 }
320 mu.Unlock()
321 return
322 }
323 treeResp = resp
324 }()
325
326 // commits
327 wg.Add(1)
328 go func() {
329 defer wg.Done()
330 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
331 if err != nil {
332 mu.Lock()
333 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
334 log.Println("failed to call XRPC repo.log", xrpcerr)
335 errs = append(errs, xrpcerr)
336 } else {
337 errs = append(errs, err)
338 }
339 mu.Unlock()
340 return
341 }
342
343 if err := json.Unmarshal(logBytes, &logResp); err != nil {
344 mu.Lock()
345 errs = append(errs, err)
346 mu.Unlock()
347 }
348 }()
349
350 // readme content
351 wg.Add(1)
352 go func() {
353 defer wg.Done()
354 for _, filename := range markup.ReadmeFilenames {
355 blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo)
356 if err != nil {
357 continue
358 }
359
360 if blobResp == nil {
361 continue
362 }
363
364 readmeContent = blobResp.Content
365 readmeFileName = filename
366 break
367 }
368 }()
369
370 wg.Wait()
371
372 if len(errs) > 0 {
373 return nil, errs[0] // return first error
374 }
375
376 var files []types.NiceTree
377 if treeResp != nil && treeResp.Files != nil {
378 for _, file := range treeResp.Files {
379 niceFile := types.NiceTree{
380 IsFile: file.Is_file,
381 IsSubtree: file.Is_subtree,
382 Name: file.Name,
383 Mode: file.Mode,
384 Size: file.Size,
385 }
386 if file.Last_commit != nil {
387 when, _ := time.Parse(time.RFC3339, file.Last_commit.When)
388 niceFile.LastCommit = &types.LastCommitInfo{
389 Hash: plumbing.NewHash(file.Last_commit.Hash),
390 Message: file.Last_commit.Message,
391 When: when,
392 }
393 }
394 files = append(files, niceFile)
395 }
396 }
397
398 result := &types.RepoIndexResponse{
399 IsEmpty: false,
400 Ref: ref,
401 Readme: readmeContent,
402 ReadmeFileName: readmeFileName,
403 Commits: logResp.Commits,
404 Description: logResp.Description,
405 Files: files,
406 Branches: branchesResp.Branches,
407 Tags: tagsResp.Tags,
408 TotalCommits: logResp.Total,
409 }
410
411 return result, nil
412}