This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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