1package repo
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "net/http"
8 "slices"
9 "sort"
10 "strings"
11
12 "tangled.sh/tangled.sh/core/appview/commitverify"
13 "tangled.sh/tangled.sh/core/appview/db"
14 "tangled.sh/tangled.sh/core/appview/oauth"
15 "tangled.sh/tangled.sh/core/appview/pages"
16 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
17 "tangled.sh/tangled.sh/core/appview/reporesolver"
18 "tangled.sh/tangled.sh/core/knotclient"
19 "tangled.sh/tangled.sh/core/types"
20
21 "github.com/go-chi/chi/v5"
22 "github.com/go-enry/go-enry/v2"
23)
24
25func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
26 ref := chi.URLParam(r, "ref")
27 f, err := rp.repoResolver.Resolve(r)
28 if err != nil {
29 log.Println("failed to fully resolve repo", err)
30 return
31 }
32
33 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
34 if err != nil {
35 log.Printf("failed to create unsigned client for %s", f.Knot)
36 rp.pages.Error503(w)
37 return
38 }
39
40 result, err := us.Index(f.OwnerDid(), f.RepoName, ref)
41 if err != nil {
42 rp.pages.Error503(w)
43 log.Println("failed to reach knotserver", err)
44 return
45 }
46
47 tagMap := make(map[string][]string)
48 for _, tag := range result.Tags {
49 hash := tag.Hash
50 if tag.Tag != nil {
51 hash = tag.Tag.Target.String()
52 }
53 tagMap[hash] = append(tagMap[hash], tag.Name)
54 }
55
56 for _, branch := range result.Branches {
57 hash := branch.Hash
58 tagMap[hash] = append(tagMap[hash], branch.Name)
59 }
60
61 sortFiles(result.Files)
62
63 slices.SortFunc(result.Branches, func(a, b types.Branch) int {
64 if a.Name == result.Ref {
65 return -1
66 }
67 if a.IsDefault {
68 return -1
69 }
70 if b.IsDefault {
71 return 1
72 }
73 if a.Commit != nil && b.Commit != nil {
74 if a.Commit.Committer.When.Before(b.Commit.Committer.When) {
75 return 1
76 } else {
77 return -1
78 }
79 }
80 return strings.Compare(a.Name, b.Name) * -1
81 })
82
83 commitCount := len(result.Commits)
84 branchCount := len(result.Branches)
85 tagCount := len(result.Tags)
86 fileCount := len(result.Files)
87
88 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
89 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
90 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
91 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))]
92
93 emails := uniqueEmails(commitsTrunc)
94 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
95 if err != nil {
96 log.Println("failed to get email to did map", err)
97 }
98
99 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
100 if err != nil {
101 log.Println(err)
102 }
103
104 user := rp.oauth.GetUser(r)
105 repoInfo := f.RepoInfo(user)
106
107 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
108 if err != nil {
109 log.Printf("failed to get registration key for %s: %s", f.Knot, err)
110 rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
111 }
112
113 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
114 if err != nil {
115 log.Printf("failed to create signed client for %s: %s", f.Knot, err)
116 return
117 }
118
119 var forkInfo *types.ForkInfo
120 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
121 forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient)
122 if err != nil {
123 log.Printf("Failed to fetch fork information: %v", err)
124 return
125 }
126 }
127
128 // TODO: a bit dirty
129 languageInfo, err := rp.getLanguageInfo(f, signedClient, chi.URLParam(r, "ref") == "")
130 if err != nil {
131 log.Printf("failed to compute language percentages: %s", err)
132 // non-fatal
133 }
134
135 var shas []string
136 for _, c := range commitsTrunc {
137 shas = append(shas, c.Hash.String())
138 }
139 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
140 if err != nil {
141 log.Printf("failed to fetch pipeline statuses: %s", err)
142 // non-fatal
143 }
144
145 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
146 LoggedInUser: user,
147 RepoInfo: repoInfo,
148 TagMap: tagMap,
149 RepoIndexResponse: *result,
150 CommitsTrunc: commitsTrunc,
151 TagsTrunc: tagsTrunc,
152 ForkInfo: forkInfo,
153 BranchesTrunc: branchesTrunc,
154 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
155 VerifiedCommits: vc,
156 Languages: languageInfo,
157 Pipelines: pipelines,
158 })
159}
160
161func (rp *Repo) getLanguageInfo(
162 f *reporesolver.ResolvedRepo,
163 signedClient *knotclient.SignedClient,
164 isDefaultRef bool,
165) ([]types.RepoLanguageDetails, error) {
166 // first attempt to fetch from db
167 langs, err := db.GetRepoLanguages(
168 rp.db,
169 db.FilterEq("repo_at", f.RepoAt),
170 db.FilterEq("ref", f.Ref),
171 )
172
173 if err != nil || langs == nil {
174 // non-fatal, fetch langs from ks
175 ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref)
176 if err != nil {
177 return nil, err
178 }
179 if ls == nil {
180 return nil, nil
181 }
182
183 for l, s := range ls.Languages {
184 langs = append(langs, db.RepoLanguage{
185 RepoAt: f.RepoAt,
186 Ref: f.Ref,
187 IsDefaultRef: isDefaultRef,
188 Language: l,
189 Bytes: s,
190 })
191 }
192
193 // update appview's cache
194 err = db.InsertRepoLanguages(rp.db, langs)
195 if err != nil {
196 // non-fatal
197 log.Println("failed to cache lang results", err)
198 }
199 }
200
201 var total int64
202 for _, l := range langs {
203 total += l.Bytes
204 }
205
206 var languageStats []types.RepoLanguageDetails
207 for _, l := range langs {
208 percentage := float32(l.Bytes) / float32(total) * 100
209 color := enry.GetColor(l.Language)
210 languageStats = append(languageStats, types.RepoLanguageDetails{
211 Name: l.Language,
212 Percentage: percentage,
213 Color: color,
214 })
215 }
216
217 sort.Slice(languageStats, func(i, j int) bool {
218 if languageStats[i].Name == enry.OtherLanguage {
219 return false
220 }
221 if languageStats[j].Name == enry.OtherLanguage {
222 return true
223 }
224 if languageStats[i].Percentage != languageStats[j].Percentage {
225 return languageStats[i].Percentage > languageStats[j].Percentage
226 }
227 return languageStats[i].Name < languageStats[j].Name
228 })
229
230 return languageStats, nil
231}
232
233func getForkInfo(
234 repoInfo repoinfo.RepoInfo,
235 rp *Repo,
236 f *reporesolver.ResolvedRepo,
237 user *oauth.User,
238 signedClient *knotclient.SignedClient,
239) (*types.ForkInfo, error) {
240 if user == nil {
241 return nil, nil
242 }
243
244 forkInfo := types.ForkInfo{
245 IsFork: repoInfo.Source != nil,
246 Status: types.UpToDate,
247 }
248
249 if !forkInfo.IsFork {
250 forkInfo.IsFork = false
251 return &forkInfo, nil
252 }
253
254 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)
255 if err != nil {
256 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
257 return nil, err
258 }
259
260 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name)
261 if err != nil {
262 log.Println("failed to reach knotserver", err)
263 return nil, err
264 }
265
266 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
267 return branch.Name == f.Ref
268 }) {
269 forkInfo.Status = types.MissingBranch
270 return &forkInfo, nil
271 }
272
273 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref)
274 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent {
275 log.Printf("failed to update tracking branch: %s", err)
276 return nil, err
277 }
278
279 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)
280
281 var status types.AncestorCheckResponse
282 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef)
283 if err != nil {
284 log.Printf("failed to check if fork is ahead/behind: %s", err)
285 return nil, err
286 }
287
288 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil {
289 log.Printf("failed to decode fork status: %s", err)
290 return nil, err
291 }
292
293 forkInfo.Status = status.Status
294 return &forkInfo, nil
295}