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