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