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