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.0 kB 345 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 languageInfo, 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 } 122 } 123 124 var shas []string 125 for _, c := range commitsTrunc { 126 shas = append(shas, c.Hash.String()) 127 } 128 pipelines, err := getPipelineStatuses(rp.db, f, shas) 129 if err != nil { 130 l.Error("failed to fetch pipeline statuses", "err", err) 131 // non-fatal 132 } 133 134 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 135 LoggedInUser: user, 136 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 137 TagMap: tagMap, 138 RepoIndexResponse: *result, 139 CommitsTrunc: commitsTrunc, 140 TagsTrunc: tagsTrunc, 141 // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands 142 BranchesTrunc: branchesTrunc, 143 EmailToDid: emailToDidMap, 144 VerifiedCommits: vc, 145 Languages: languageInfo, 146 Pipelines: pipelines, 147 }) 148} 149 150func (rp *Repo) getLanguageInfo( 151 ctx context.Context, 152 repoId syntax.DID, 153 ref string, 154) ([]types.RepoLanguageDetails, error) { 155 var langs []models.RepoLanguage 156 // non-fatal, fetch langs from knotmirror via XRPC 157 xrpcc := &indigoxrpc.Client{ 158 Host: rp.config.KnotMirror.Url, 159 Client: http.DefaultClient, 160 } 161 ls, err := tangled.GitTempListLanguages(ctx, xrpcc, ref, repoId.String()) 162 if err != nil { 163 return nil, fmt.Errorf("calling knotmirror git.listLanguages: %w", err) 164 } 165 166 if ls == nil || ls.Languages == nil { 167 return nil, nil 168 } 169 170 var total int64 171 for _, lang := range ls.Languages { 172 total += lang.Size 173 langs = append(langs, models.RepoLanguage{ 174 RepoDid: repoId, 175 Ref: ref, 176 Language: lang.Name, 177 Bytes: lang.Size, 178 }) 179 } 180 181 var languageStats []types.RepoLanguageDetails 182 for _, l := range langs { 183 percentage := float32(l.Bytes) / float32(total) * 100 184 color := enry.GetColor(l.Language) 185 languageStats = append(languageStats, types.RepoLanguageDetails{ 186 Name: l.Language, 187 Percentage: percentage, 188 Color: color, 189 }) 190 } 191 192 sort.Slice(languageStats, func(i, j int) bool { 193 if languageStats[i].Name == enry.OtherLanguage { 194 return false 195 } 196 if languageStats[j].Name == enry.OtherLanguage { 197 return true 198 } 199 if languageStats[i].Percentage != languageStats[j].Percentage { 200 return languageStats[i].Percentage > languageStats[j].Percentage 201 } 202 return languageStats[i].Name < languageStats[j].Name 203 }) 204 205 return languageStats, nil 206} 207 208// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 209func (rp *Repo) buildIndexResponse(ctx context.Context, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 210 xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 211 212 branchesBytes, err := tangled.GitTempListBranches(ctx, xrpcc, "", 0, repo.RepoDid) 213 if err != nil { 214 return nil, fmt.Errorf("calling knotmirror git.listBranches: %w", err) 215 } 216 217 var branchesResp types.RepoBranchesResponse 218 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 219 return nil, fmt.Errorf("failed to unmarshal branches response: %w", err) 220 } 221 222 // if no ref specified, use default branch or first available 223 if ref == "" { 224 for _, branch := range branchesResp.Branches { 225 if branch.IsDefault { 226 ref = branch.Name 227 break 228 } 229 } 230 } 231 232 // if ref is still empty, this means the default branch is not set 233 if ref == "" { 234 return &types.RepoIndexResponse{ 235 IsEmpty: true, 236 Branches: branchesResp.Branches, 237 }, nil 238 } 239 240 // now run the remaining queries in parallel 241 var wg sync.WaitGroup 242 var errs error 243 244 var ( 245 tagsResp types.RepoTagsResponse 246 treeResp *tangled.GitTempGetTree_Output 247 logResp types.RepoLogResponse 248 readmeContent string 249 readmeFileName string 250 ) 251 252 // tags 253 wg.Go(func() { 254 tagsBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, repo.RepoDid) 255 if err != nil { 256 errs = errors.Join(errs, fmt.Errorf("failed to call git.ListTags: %w", err)) 257 return 258 } 259 260 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 261 errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListTags: %w", err)) 262 } 263 }) 264 265 // tree/files 266 wg.Go(func() { 267 resp, err := tangled.GitTempGetTree(ctx, xrpcc, "", ref, repo.RepoDid) 268 if err != nil { 269 errs = errors.Join(errs, fmt.Errorf("failed to call git.GetTree: %w", err)) 270 return 271 } 272 treeResp = resp 273 274 for _, file := range resp.Files { 275 if markup.IsReadmeFile(file.Name, file.Mode) { 276 readmeFileName = file.Name 277 break 278 } 279 } 280 281 if readmeFileName != "" { 282 bytes, err := tangled.GitTempGetBlob(ctx, xrpcc, readmeFileName, ref, repo.RepoDid) 283 if err != nil { 284 errs = errors.Join(errs, fmt.Errorf("failed to call git.getBlob: %w", err)) 285 return 286 } 287 readmeContent = string(bytes) 288 } 289 }) 290 291 // commits 292 wg.Go(func() { 293 logBytes, err := tangled.GitTempListCommits(ctx, xrpcc, "", 50, ref, repo.RepoDid) 294 if err != nil { 295 errs = errors.Join(errs, fmt.Errorf("failed to call git.ListCommits: %w", err)) 296 return 297 } 298 299 if err := json.Unmarshal(logBytes, &logResp); err != nil { 300 errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListCommits: %w", err)) 301 } 302 }) 303 304 wg.Wait() 305 306 if errs != nil { 307 return nil, errs 308 } 309 310 var files []types.NiceTree 311 if treeResp != nil && treeResp.Files != nil { 312 for _, file := range treeResp.Files { 313 niceFile := types.NiceTree{ 314 Name: file.Name, 315 Mode: file.Mode, 316 Size: file.Size, 317 } 318 319 if file.Last_commit != nil { 320 when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 321 niceFile.LastCommit = &types.LastCommitInfo{ 322 Hash: plumbing.NewHash(file.Last_commit.Hash), 323 Message: file.Last_commit.Message, 324 When: when, 325 } 326 } 327 files = append(files, niceFile) 328 } 329 } 330 331 result := &types.RepoIndexResponse{ 332 IsEmpty: false, 333 Ref: ref, 334 Readme: readmeContent, 335 ReadmeFileName: readmeFileName, 336 Commits: logResp.Commits, 337 Description: "", 338 Files: files, 339 Branches: branchesResp.Branches, 340 Tags: tagsResp.Tags, 341 TotalCommits: logResp.Total, 342 } 343 344 return result, nil 345}